Fix AI price check truncation and JSON parse errors
Root cause: MaxTokens=4096 was too low — 25 items at ~250 tokens each hit the limit mid-array (logged error showed Path: $[17]). - MaxTokens: 4096 → 8192 - BatchSize: 25 → 15 items (keeps each response well under the limit) - StripJsonFences → ExtractJsonArray: now also handles prose before/after the JSON array, and recovers truncated responses by finding the last complete object and closing the array — so partial batches return whatever Claude finished rather than nothing - GET action: added try-catch around ResultsJson deserialization so a bad DB row shows a friendly "re-run" warning instead of a raw error page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ public class AiCatalogPriceCheckService : IAiCatalogPriceCheckService
|
|||||||
private readonly ILogger<AiCatalogPriceCheckService> _logger;
|
private readonly ILogger<AiCatalogPriceCheckService> _logger;
|
||||||
|
|
||||||
private const string Model = "claude-sonnet-4-6";
|
private const string Model = "claude-sonnet-4-6";
|
||||||
private const int BatchSize = 25;
|
private const int BatchSize = 15; // 25 items at ~250 tokens each exceeds 4096 output tokens
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true };
|
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true };
|
||||||
|
|
||||||
@@ -37,18 +37,44 @@ public class AiCatalogPriceCheckService : IAiCatalogPriceCheckService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strips optional ```json ... ``` fences that Claude sometimes adds despite instructions.
|
/// Extracts a JSON array from Claude's response, handling three common failure modes:
|
||||||
|
/// (1) ```json ... ``` fences wrapping the array,
|
||||||
|
/// (2) prose text before or after the JSON array,
|
||||||
|
/// (3) truncated responses where the closing ] is missing — in that case we close any
|
||||||
|
/// open string and append ]} to produce a parseable (though incomplete) array so
|
||||||
|
/// we recover whatever items Claude did finish.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string StripJsonFences(string text)
|
private static string ExtractJsonArray(string text)
|
||||||
{
|
{
|
||||||
var trimmed = text.Trim();
|
var trimmed = text.Trim();
|
||||||
|
|
||||||
|
// Strip code fences
|
||||||
if (trimmed.StartsWith("```"))
|
if (trimmed.StartsWith("```"))
|
||||||
{
|
{
|
||||||
var firstNewline = trimmed.IndexOf('\n');
|
var firstNewline = trimmed.IndexOf('\n');
|
||||||
if (firstNewline >= 0) trimmed = trimmed[(firstNewline + 1)..];
|
if (firstNewline >= 0) trimmed = trimmed[(firstNewline + 1)..];
|
||||||
if (trimmed.EndsWith("```")) trimmed = trimmed[..^3];
|
if (trimmed.EndsWith("```")) trimmed = trimmed[..^3];
|
||||||
|
trimmed = trimmed.Trim();
|
||||||
}
|
}
|
||||||
return trimmed.Trim();
|
|
||||||
|
// Find the outermost [ ... ] even when Claude adds prose around it
|
||||||
|
var arrayStart = trimmed.IndexOf('[');
|
||||||
|
if (arrayStart < 0) return "[]";
|
||||||
|
trimmed = trimmed[arrayStart..];
|
||||||
|
|
||||||
|
var arrayEnd = trimmed.LastIndexOf(']');
|
||||||
|
if (arrayEnd >= 0)
|
||||||
|
return trimmed[..(arrayEnd + 1)];
|
||||||
|
|
||||||
|
// No closing bracket — response was truncated. Patch it so we can recover
|
||||||
|
// whatever complete objects Claude did return.
|
||||||
|
// Strategy: find the last complete }, and close the array after it.
|
||||||
|
var lastComplete = trimmed.LastIndexOf("},");
|
||||||
|
if (lastComplete < 0) lastComplete = trimmed.LastIndexOf('}');
|
||||||
|
if (lastComplete >= 0)
|
||||||
|
return trimmed[..(lastComplete + 1)] + "]";
|
||||||
|
|
||||||
|
return "[]";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<MessageResponse> SendAsync(AnthropicClient client, MessageParameters parameters)
|
private static async Task<MessageResponse> SendAsync(AnthropicClient client, MessageParameters parameters)
|
||||||
@@ -98,7 +124,7 @@ public class AiCatalogPriceCheckService : IAiCatalogPriceCheckService
|
|||||||
var parameters = new MessageParameters
|
var parameters = new MessageParameters
|
||||||
{
|
{
|
||||||
Model = Model,
|
Model = Model,
|
||||||
MaxTokens = 4096,
|
MaxTokens = 8192,
|
||||||
SystemMessage = systemPrompt,
|
SystemMessage = systemPrompt,
|
||||||
Messages = new List<Message>
|
Messages = new List<Message>
|
||||||
{
|
{
|
||||||
@@ -110,7 +136,7 @@ public class AiCatalogPriceCheckService : IAiCatalogPriceCheckService
|
|||||||
{
|
{
|
||||||
var response = await SendAsync(client, parameters);
|
var response = await SendAsync(client, parameters);
|
||||||
var raw = response.Content.OfType<TextContent>().FirstOrDefault()?.Text ?? "[]";
|
var raw = response.Content.OfType<TextContent>().FirstOrDefault()?.Text ?? "[]";
|
||||||
var json = StripJsonFences(raw);
|
var json = ExtractJsonArray(raw);
|
||||||
|
|
||||||
var claudeItems = JsonSerializer.Deserialize<List<ClaudePriceCheckItem>>(json, JsonOpts) ?? new();
|
var claudeItems = JsonSerializer.Deserialize<List<ClaudePriceCheckItem>>(json, JsonOpts) ?? new();
|
||||||
|
|
||||||
|
|||||||
@@ -944,9 +944,19 @@ namespace PowderCoating.Web.Controllers
|
|||||||
CatalogPriceCheckReportDto? dto = null;
|
CatalogPriceCheckReportDto? dto = null;
|
||||||
if (report != null)
|
if (report != null)
|
||||||
{
|
{
|
||||||
var results = JsonSerializer.Deserialize<List<CatalogItemPriceVerdict>>(
|
List<CatalogItemPriceVerdict> results;
|
||||||
report.ResultsJson,
|
try
|
||||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new();
|
{
|
||||||
|
results = JsonSerializer.Deserialize<List<CatalogItemPriceVerdict>>(
|
||||||
|
report.ResultsJson,
|
||||||
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to deserialize stored price check report {ReportId}", report.Id);
|
||||||
|
TempData["Warning"] = "The previous report could not be loaded. Please re-run the price check.";
|
||||||
|
results = new();
|
||||||
|
}
|
||||||
|
|
||||||
dto = new CatalogPriceCheckReportDto
|
dto = new CatalogPriceCheckReportDto
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user