Lazily enrich catalog specs from TDS on first use
Specific gravity, coverage, and ~55% of cure specs aren't in the Columbia feed. Rather than read 2,400 TDS PDFs up front, enrich a catalog item the first time it's actually used: - FetchTdsCureSpecsAsync now also extracts specific gravity from the TDS. - New EnsureCatalogTdsSpecsAsync fills a catalog item's specific gravity (and any missing cure temp/time) from its TDS, then derives theoretical coverage (192.3 / (SG x mils)). No-op once specific gravity is known or when there's no TDS; persists to the catalog so the work is done once and benefits everyone. - Hooked into the catalog->inventory paths (CreateIncomingFromCatalog, the custom-powder receive enrichment, and ReceivePowderFromCatalog) so a powder's full specs land on both the catalog and the new inventory record. DashboardController gains the AI lookup service for this. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ using Anthropic.SDK.Messaging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PowderCoating.Application.Interfaces;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
|
||||
namespace PowderCoating.Infrastructure.Services;
|
||||
@@ -541,18 +542,20 @@ Rules:
|
||||
|
||||
// Targeted prompt: we only need cure specs from this document
|
||||
const string curePrompt = @"You are reading a Technical Data Sheet (TDS) for a powder coating product.
|
||||
Extract ONLY the cure schedule. Respond with a valid JSON object — no markdown, no explanation:
|
||||
Extract the cure schedule and the specific gravity. Respond with a valid JSON object — no markdown, no explanation:
|
||||
|
||||
{
|
||||
""cureTemperatureF"": number or null,
|
||||
""cureTimeMinutes"": number or null,
|
||||
""reasoning"": ""one sentence: what cure schedule you found""
|
||||
""specificGravity"": number or null,
|
||||
""reasoning"": ""one sentence: what cure schedule and specific gravity you found""
|
||||
}
|
||||
|
||||
Rules:
|
||||
- cureTemperatureF: the recommended cure temperature in °F. Convert from °C if needed (multiply by 1.8 + 32). If a range is given use the midpoint. Most powders cure 325–400 °F.
|
||||
- cureTimeMinutes: the hold time at cure temperature in minutes (NOT total oven time). Typically 10–20 min.
|
||||
- If neither value can be found in the document, return null for both.";
|
||||
- specificGravity: the specific gravity / density value from the TDS (often labeled ""Specific Gravity"" or ""Density""). Typically 1.2–1.8. Null if not stated.
|
||||
- Return null for any value not found in the document.";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Technical Data Sheet content:");
|
||||
@@ -603,11 +606,12 @@ Rules:
|
||||
Success = true,
|
||||
CureTemperatureF = GetDecimal(parsed, "cureTemperatureF"),
|
||||
CureTimeMinutes = GetInt(parsed, "cureTimeMinutes"),
|
||||
SpecificGravity = GetDecimal(parsed, "specificGravity"),
|
||||
Reasoning = GetString(parsed, "reasoning"),
|
||||
};
|
||||
|
||||
_logger.LogInformation("TDS cure lookup for {Url}: temp={Temp}°F, time={Time}min ({Reasoning})",
|
||||
tdsUrl, result.CureTemperatureF, result.CureTimeMinutes, result.Reasoning);
|
||||
_logger.LogInformation("TDS spec lookup for {Url}: temp={Temp}°F, time={Time}min, sg={Sg} ({Reasoning})",
|
||||
tdsUrl, result.CureTemperatureF, result.CureTimeMinutes, result.SpecificGravity, result.Reasoning);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -617,6 +621,58 @@ Rules:
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> EnsureCatalogTdsSpecsAsync(PowderCatalogItem catalog)
|
||||
{
|
||||
// Already enriched, or nothing to read from. Specific gravity is the trigger: it's never in
|
||||
// the API feed, so its absence means this item hasn't been TDS-enriched yet.
|
||||
if (catalog == null || catalog.SpecificGravity.HasValue || string.IsNullOrWhiteSpace(catalog.TdsUrl))
|
||||
return false;
|
||||
|
||||
var tds = await FetchTdsCureSpecsAsync(catalog.TdsUrl, catalog.ColorName);
|
||||
if (!tds.Success)
|
||||
return false;
|
||||
|
||||
var changed = false;
|
||||
|
||||
if (tds.SpecificGravity is > 0)
|
||||
{
|
||||
catalog.SpecificGravity = tds.SpecificGravity;
|
||||
changed = true;
|
||||
}
|
||||
if (!catalog.CureTemperatureF.HasValue && tds.CureTemperatureF.HasValue)
|
||||
{
|
||||
catalog.CureTemperatureF = tds.CureTemperatureF;
|
||||
changed = true;
|
||||
}
|
||||
if (!catalog.CureTimeMinutes.HasValue && tds.CureTimeMinutes.HasValue)
|
||||
{
|
||||
catalog.CureTimeMinutes = tds.CureTimeMinutes;
|
||||
changed = true;
|
||||
}
|
||||
// Derive theoretical coverage once specific gravity is known.
|
||||
if (!catalog.CoverageSqFtPerLb.HasValue && catalog.SpecificGravity is > 0)
|
||||
{
|
||||
catalog.CoverageSqFtPerLb = Math.Round(
|
||||
TheoreticalCoverageConstant / (catalog.SpecificGravity.Value * DefaultCoverageThicknessMils),
|
||||
2, MidpointRounding.AwayFromZero);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
catalog.UpdatedAt = DateTime.UtcNow;
|
||||
await _unitOfWork.PowderCatalog.UpdateAsync(catalog);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
_logger.LogInformation(
|
||||
"Lazily enriched catalog item {Vendor} {Sku} from TDS: sg={Sg}, cure={Temp}F/{Time}min, coverage={Cov}",
|
||||
catalog.VendorName, catalog.Sku, catalog.SpecificGravity, catalog.CureTemperatureF,
|
||||
catalog.CureTimeMinutes, catalog.CoverageSqFtPerLb);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ── Manufacturer URL pattern: build direct product page URL ───────────────
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user