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:
@@ -238,6 +238,8 @@ public class AiCatalogPriceCheckService : IAiCatalogPriceCheckService
|
|||||||
sb.AppendLine("- \"medium\" — reasonable assumptions were possible");
|
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("- \"low\" — item is too vague to estimate reliably (e.g., 'Custom Part', 'Job Special')");
|
||||||
sb.AppendLine();
|
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("If the item already has an ApproximateArea or EstimatedMinutes, use those instead of guessing.");
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine("IMPORTANT: Keep responses concise to avoid truncation. Limit assumptions to 20 words max. Limit reasoning to 25 words max.");
|
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);
|
r => r.CompanyId == currentUser.CompanyId);
|
||||||
var report = existing.OrderByDescending(r => r.RunAt).FirstOrDefault();
|
var report = existing.OrderByDescending(r => r.RunAt).FirstOrDefault();
|
||||||
|
|
||||||
var activeItems = await _unitOfWork.CatalogItems.FindAsync(ci => ci.IsActive);
|
var pricedItems = await _unitOfWork.CatalogItems.FindAsync(ci => ci.IsActive && ci.DefaultPrice > 0);
|
||||||
ViewBag.ActiveItemCount = activeItems.Count();
|
ViewBag.ActiveItemCount = pricedItems.Count();
|
||||||
|
|
||||||
CatalogPriceCheckReportDto? dto = null;
|
CatalogPriceCheckReportDto? dto = null;
|
||||||
if (report != null)
|
if (report != null)
|
||||||
@@ -990,16 +990,23 @@ namespace PowderCoating.Web.Controllers
|
|||||||
|
|
||||||
try
|
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(
|
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)
|
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));
|
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
|
// Load company operating costs
|
||||||
var costs = (await _unitOfWork.CompanyOperatingCosts.FindAsync(
|
var costs = (await _unitOfWork.CompanyOperatingCosts.FindAsync(
|
||||||
c => c.CompanyId == currentUser.CompanyId)).FirstOrDefault();
|
c => c.CompanyId == currentUser.CompanyId)).FirstOrDefault();
|
||||||
@@ -1012,7 +1019,7 @@ namespace PowderCoating.Web.Controllers
|
|||||||
Id = i.Id,
|
Id = i.Id,
|
||||||
Name = i.Name,
|
Name = i.Name,
|
||||||
Description = i.Description,
|
Description = i.Description,
|
||||||
CategoryName = i.Category?.Name ?? "Uncategorized",
|
CategoryName = BuildCategoryPath(i.CategoryId, allCategories),
|
||||||
CurrentPrice = i.DefaultPrice,
|
CurrentPrice = i.DefaultPrice,
|
||||||
ApproximateAreaSqFt = i.ApproximateArea,
|
ApproximateAreaSqFt = i.ApproximateArea,
|
||||||
EstimatedMinutes = i.DefaultEstimatedMinutes,
|
EstimatedMinutes = i.DefaultEstimatedMinutes,
|
||||||
@@ -1083,6 +1090,25 @@ namespace PowderCoating.Web.Controllers
|
|||||||
$"Blaster ${c.SandblasterCostPerHour:F2}/hr | Booth ${c.CoatingBoothCostPerHour:F2}/hr | " +
|
$"Blaster ${c.SandblasterCostPerHour:F2}/hr | Booth ${c.CoatingBoothCostPerHour:F2}/hr | " +
|
||||||
$"Powder ${c.PowderCostPerSqFt:F2}/sqft | " +
|
$"Powder ${c.PowderCostPerSqFt:F2}/sqft | " +
|
||||||
$"{(c.PricingMode == "margin" ? "Margin" : "Markup")} {c.MarkupOrMarginPercent:F1}%";
|
$"{(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
|
// Helper class for hierarchical display
|
||||||
|
|||||||
Reference in New Issue
Block a user