feat(ai): enhance AI trace warnings and reason codes system
- Added structured warning system with `AdminAiWarning` type in backend and Flutter - Implemented detailed reason descriptors with `FlyerReasonDescriptor` for parse and match operations - Added `legacyWarnings` field to maintain backward compatibility - Enhanced AI trace service to collect and format warnings with item-level context - Updated flyer import services to include detailed reason descriptions in responses - Added Swedish diacritic preservation for cheese variants (Prästost, Herrgårdsost, Grevéost) - Implemented UTF-8 content validation for AI responses - Added new reason code definitions in `reason-codes.ts` - Updated Flutter UI to display structured warnings with severity indicators - Added error report generation and copy functionality in admin panel - Added comprehensive test coverage for new warning system and cheese normalization BREAKING CHANGE: AI trace warnings are now structured objects instead of simple strings
This commit is contained in:
@@ -143,6 +143,71 @@ describe('AiTraceService receipt masking', () => {
|
||||
expect(result.rawOutput).toContain('{"ok":true}');
|
||||
expect(result.retryCount).toBe(2);
|
||||
expect(result.chunkCount).toBe(4);
|
||||
expect(result.warnings).toContain('parse:low_confidence');
|
||||
expect(result.warnings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: 'parse',
|
||||
code: 'low_confidence',
|
||||
title: 'Låg parsningskvalitet',
|
||||
severity: 'warning',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(result.legacyWarnings).toContain('parse:low_confidence');
|
||||
});
|
||||
|
||||
it('keeps multiple token_overlap warnings for same row', async () => {
|
||||
prismaMock.flyerSession.findUnique.mockResolvedValue({
|
||||
id: 202,
|
||||
userId: 9,
|
||||
createdAt: new Date('2026-05-23T09:00:00.000Z'),
|
||||
sourceFileName: 'willys-v21.pdf',
|
||||
sourceMimeType: 'application/pdf',
|
||||
sourceFileSize: 2222,
|
||||
user: { username: 'admin', email: 'admin@example.com' },
|
||||
items: [
|
||||
{
|
||||
id: 11,
|
||||
rawName: 'Tomatmix',
|
||||
normalizedName: 'tomatmix',
|
||||
brand: null,
|
||||
categoryHint: 'Grönsaker',
|
||||
categoryId: null,
|
||||
price: null,
|
||||
priceUnit: null,
|
||||
comparisonPrice: null,
|
||||
comparisonUnit: null,
|
||||
weight: null,
|
||||
bundleWeight: null,
|
||||
isBundle: false,
|
||||
bundleItems: [],
|
||||
offerText: null,
|
||||
parseConfidence: 0.9,
|
||||
parseReasons: [],
|
||||
matchedProductId: null,
|
||||
matchedProductName: null,
|
||||
matchedVia: 'token',
|
||||
matchConfidence: 0.7,
|
||||
matchReasons: ['token_overlap:0.42', 'token_overlap:0.73'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
prismaMock.aiTrace.findMany.mockResolvedValue([
|
||||
{
|
||||
sessionId: 202,
|
||||
prompt: 'prompt',
|
||||
rawOutput: '{"ok":true}',
|
||||
normalizedOutput: { retryCount: 0, chunkCount: 1 },
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await service.getTraceById('flyer-202');
|
||||
|
||||
const tokenWarnings = result.warnings.filter((warning) => warning.code === 'token_overlap');
|
||||
expect(tokenWarnings).toHaveLength(2);
|
||||
expect(result.legacyWarnings).toEqual(
|
||||
expect.arrayContaining(['match:token_overlap:0.42', 'match:token_overlap:0.73']),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user