Add full category path to AI price check for coating-type context

- Skip $0-priced items (placeholders/category headers) in RunAiPriceCheck
- Build full category path (e.g. "Cerakote > Firearms") via BuildCategoryPath
  so Claude receives coating-type context — Cerakote pricing differs significantly
  from standard powder coat
- Update AI system prompt to instruct Claude to use the category path when
  determining process type, equipment, cure times, and market rates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 20:35:41 -04:00
parent 19cc03ad1c
commit 560a2c76b8
2 changed files with 34 additions and 6 deletions
@@ -238,6 +238,8 @@ public class AiCatalogPriceCheckService : IAiCatalogPriceCheckService
sb.AppendLine("- \"medium\" — reasonable assumptions were possible");
sb.AppendLine("- \"low\" — item is too vague to estimate reliably (e.g., 'Custom Part', 'Job Special')");
sb.AppendLine();
sb.AppendLine("The \"category\" field contains the full path, e.g. \"Cerakote > Firearms\" or \"Powder Coat > Wheels\". Use this to determine the coating process — Cerakote items have a very different cost profile than standard powder coat (different equipment, cure times, and market rates). Price accordingly.");
sb.AppendLine();
sb.AppendLine("If the item already has an ApproximateArea or EstimatedMinutes, use those instead of guessing.");
sb.AppendLine();
sb.AppendLine("IMPORTANT: Keep responses concise to avoid truncation. Limit assumptions to 20 words max. Limit reasoning to 25 words max.");
@@ -938,8 +938,8 @@ namespace PowderCoating.Web.Controllers
r => r.CompanyId == currentUser.CompanyId);
var report = existing.OrderByDescending(r => r.RunAt).FirstOrDefault();
var activeItems = await _unitOfWork.CatalogItems.FindAsync(ci => ci.IsActive);
ViewBag.ActiveItemCount = activeItems.Count();
var pricedItems = await _unitOfWork.CatalogItems.FindAsync(ci => ci.IsActive && ci.DefaultPrice > 0);
ViewBag.ActiveItemCount = pricedItems.Count();
CatalogPriceCheckReportDto? dto = null;
if (report != null)
@@ -990,16 +990,23 @@ namespace PowderCoating.Web.Controllers
try
{
// Load all active catalog items with categories
// Load active catalog items with a real price — skip $0 items (placeholders,
// category headers, etc.) since there's no pricing to evaluate.
var items = (await _unitOfWork.CatalogItems.FindAsync(
ci => ci.IsActive, false, ci => ci.Category)).ToList();
ci => ci.IsActive && ci.DefaultPrice > 0, false, ci => ci.Category)).ToList();
if (items.Count == 0)
{
TempData["Warning"] = "No active catalog items to analyze.";
TempData["Warning"] = "No priced catalog items to analyze. Add prices to your catalog items first.";
return RedirectToAction(nameof(AiPriceCheck));
}
// Load all categories so we can build full paths (e.g. "Cerakote > Firearms").
// The full path gives Claude the coating-type context it needs — an item in
// "Firearms" under "Cerakote" costs very differently than one under "Powder Coat".
var allCategories = (await _unitOfWork.CatalogCategories.GetAllAsync())
.ToDictionary(c => c.Id);
// Load company operating costs
var costs = (await _unitOfWork.CompanyOperatingCosts.FindAsync(
c => c.CompanyId == currentUser.CompanyId)).FirstOrDefault();
@@ -1012,7 +1019,7 @@ namespace PowderCoating.Web.Controllers
Id = i.Id,
Name = i.Name,
Description = i.Description,
CategoryName = i.Category?.Name ?? "Uncategorized",
CategoryName = BuildCategoryPath(i.CategoryId, allCategories),
CurrentPrice = i.DefaultPrice,
ApproximateAreaSqFt = i.ApproximateArea,
EstimatedMinutes = i.DefaultEstimatedMinutes,
@@ -1083,6 +1090,25 @@ namespace PowderCoating.Web.Controllers
$"Blaster ${c.SandblasterCostPerHour:F2}/hr | Booth ${c.CoatingBoothCostPerHour:F2}/hr | " +
$"Powder ${c.PowderCostPerSqFt:F2}/sqft | " +
$"{(c.PricingMode == "margin" ? "Margin" : "Markup")} {c.MarkupOrMarginPercent:F1}%";
/// <summary>
/// Walks up the category parent chain to produce a full path like "Cerakote > Firearms",
/// giving Claude the coating-type context it needs for accurate pricing analysis.
/// </summary>
private static string BuildCategoryPath(int? categoryId, Dictionary<int, CatalogCategory> all)
{
if (categoryId == null) return "Uncategorized";
var parts = new List<string>();
var current = all.GetValueOrDefault(categoryId.Value);
while (current != null)
{
parts.Insert(0, current.Name);
current = current.ParentCategoryId.HasValue
? all.GetValueOrDefault(current.ParentCategoryId.Value)
: null;
}
return parts.Count > 0 ? string.Join(" > ", parts) : "Uncategorized";
}
}
// Helper class for hierarchical display