Fall back to TDS sheet for cure specs when main lookup returns none
After the main AI lookup and catalog search, if CureTemperatureF or CureTimeMinutes is still null but a TDS URL was found, fetch that page and ask Claude to extract just the cure schedule. - IInventoryAiLookupService.FetchTdsCureSpecsAsync: new interface method - InventoryAiLookupService.FetchTdsCureSpecsAsync: fetches the TDS URL via the existing FetchPageAsync pipeline (JSON-LD + doc-link extraction, HTML stripping). If the page is a PDF or unreachable, returns Success=false silently so no error surfaces in the UI. Otherwise sends a small targeted prompt that asks only for cureTemperatureF and cureTimeMinutes and uses MaxTokens=256 so the call is fast and cheap. - InventoryController.ScanLabel: after catalog lookup, computes the resolved cure values (catalog preferred over AI result). If either is null and a TDS URL exists, calls FetchTdsCureSpecsAsync and merges any newly found values back into aiResult before building the JSON response. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -54,4 +54,11 @@ public interface IInventoryAiLookupService
|
||||
/// using Claude vision. Used by the in-browser label scanner.
|
||||
/// </summary>
|
||||
Task<InventoryAiLookupResult> ScanLabelAsync(string base64Image, string mediaType);
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a Technical Data Sheet URL and extracts cure temperature and cure time.
|
||||
/// Called when the main lookup found a TDS URL but cure specs are still missing.
|
||||
/// Returns Success=false silently (no UI error) when the TDS is a PDF or unreachable.
|
||||
/// </summary>
|
||||
Task<InventoryAiLookupResult> FetchTdsCureSpecsAsync(string tdsUrl, string? colorName);
|
||||
}
|
||||
|
||||
@@ -478,6 +478,108 @@ Rules:
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a TDS URL (if it is an HTML page — PDFs are silently skipped) and asks Claude
|
||||
/// to extract cure temperature and cure time only. Uses the same <see cref="FetchPageAsync"/>
|
||||
/// pipeline so JSON-LD and document links are still extracted before stripping HTML.
|
||||
/// Returns <see cref="InventoryAiLookupResult.Success"/> = <c>false</c> without an error
|
||||
/// message when the page could not be fetched (PDF, redirect loop, etc.) so the caller
|
||||
/// can merge results without surfacing a user-visible error.
|
||||
/// </summary>
|
||||
public async Task<InventoryAiLookupResult> FetchTdsCureSpecsAsync(string tdsUrl, string? colorName)
|
||||
{
|
||||
var apiKey = _config["AI:Anthropic:ApiKey"];
|
||||
if (string.IsNullOrWhiteSpace(apiKey) || apiKey.StartsWith("your-"))
|
||||
return new InventoryAiLookupResult { Success = false };
|
||||
|
||||
try
|
||||
{
|
||||
var (pageContent, _) = await FetchPageAsync(tdsUrl);
|
||||
if (string.IsNullOrWhiteSpace(pageContent))
|
||||
{
|
||||
// PDF or unreachable — nothing to send to Claude
|
||||
_logger.LogInformation("TDS cure lookup skipped (PDF or unreachable): {Url}", tdsUrl);
|
||||
return new InventoryAiLookupResult { Success = false };
|
||||
}
|
||||
|
||||
// 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:
|
||||
|
||||
{
|
||||
""cureTemperatureF"": number or null,
|
||||
""cureTimeMinutes"": number or null,
|
||||
""reasoning"": ""one sentence: what cure schedule 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.";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Technical Data Sheet content:");
|
||||
if (!string.IsNullOrWhiteSpace(colorName)) sb.AppendLine($"Product: {colorName}");
|
||||
sb.AppendLine($"Source: {tdsUrl}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(pageContent);
|
||||
|
||||
var client = new AnthropicClient(apiKey);
|
||||
var messageRequest = new MessageParameters
|
||||
{
|
||||
Model = "claude-sonnet-4-6",
|
||||
MaxTokens = 256,
|
||||
SystemMessage = curePrompt,
|
||||
Messages = new List<Message>
|
||||
{
|
||||
new Message
|
||||
{
|
||||
Role = RoleType.User,
|
||||
Content = new List<ContentBase> { new TextContent { Text = sb.ToString() } }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var response = await client.Messages.GetClaudeMessageAsync(messageRequest);
|
||||
var rawText = (response.FirstMessage?.Text
|
||||
?? response.Content.OfType<TextContent>().FirstOrDefault()?.Text
|
||||
?? string.Empty).Trim();
|
||||
|
||||
if (rawText.StartsWith("```"))
|
||||
{
|
||||
var start = rawText.IndexOf('\n') + 1;
|
||||
var end = rawText.LastIndexOf("```");
|
||||
rawText = rawText[start..end].Trim();
|
||||
}
|
||||
|
||||
if (!rawText.StartsWith("{"))
|
||||
{
|
||||
var j = rawText.IndexOf('{');
|
||||
var k = rawText.LastIndexOf('}');
|
||||
if (j >= 0 && k > j) rawText = rawText[j..(k + 1)];
|
||||
else return new InventoryAiLookupResult { Success = false };
|
||||
}
|
||||
|
||||
var parsed = JsonSerializer.Deserialize<JsonElement>(rawText);
|
||||
var result = new InventoryAiLookupResult
|
||||
{
|
||||
Success = true,
|
||||
CureTemperatureF = GetDecimal(parsed, "cureTemperatureF"),
|
||||
CureTimeMinutes = GetInt(parsed, "cureTimeMinutes"),
|
||||
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);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "TDS cure spec fetch failed for {Url}", tdsUrl);
|
||||
return new InventoryAiLookupResult { Success = false };
|
||||
}
|
||||
}
|
||||
|
||||
// ── Manufacturer URL pattern: build direct product page URL ───────────────
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -834,6 +834,23 @@ public class InventoryController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
// If cure specs are still missing but we have a TDS URL, fetch it and try to extract
|
||||
// cure temperature and cure time. Most TDS pages are HTML; PDFs fail silently.
|
||||
var resolvedCureTemp = catalogMatch?.CureTemperatureF ?? aiResult.CureTemperatureF;
|
||||
var resolvedCureTime = catalogMatch?.CureTimeMinutes ?? aiResult.CureTimeMinutes;
|
||||
var resolvedTdsUrl = catalogMatch?.TdsUrl ?? aiResult.TdsUrl;
|
||||
|
||||
if ((resolvedCureTemp == null || resolvedCureTime == null) && !string.IsNullOrEmpty(resolvedTdsUrl))
|
||||
{
|
||||
_logger.LogInformation("Cure specs missing after main lookup; trying TDS at {Url}", resolvedTdsUrl);
|
||||
var tdsResult = await _aiLookupService.FetchTdsCureSpecsAsync(resolvedTdsUrl, colorName);
|
||||
if (tdsResult.Success)
|
||||
{
|
||||
if (resolvedCureTemp == null) aiResult.CureTemperatureF = tdsResult.CureTemperatureF;
|
||||
if (resolvedCureTime == null) aiResult.CureTimeMinutes = tdsResult.CureTimeMinutes;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this product already exists in the tenant's inventory.
|
||||
// Match by ManufacturerPartNumber first (most precise); fall back to color name + manufacturer.
|
||||
// Returns the first active match so the UI can prompt to add stock inline.
|
||||
|
||||
Reference in New Issue
Block a user