feat(ai): enhance AI trace warnings and reason codes system
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 4m21s
Test Suite / flutter-quality (push) Failing after 1m38s

- 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:
Nils-Johan Gynther
2026-05-23 21:11:46 +02:00
parent 0fb507f247
commit d9f992ca9a
18 changed files with 1308 additions and 81 deletions
@@ -9,14 +9,15 @@ import {
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { normalizeName } from '../common/utils/normalize-name';
import {
FlyerImportItem,
FlyerImportMatchVia,
FlyerImportResponse,
} from './dto/flyer-import.response';
import {
FlyerImportItem,
FlyerImportMatchVia,
FlyerImportResponse,
} from './dto/flyer-import.response';
import { TextExtractorService } from './services/text-extractor.service';
import { AiFlyerParserService } from './services/ai-flyer-parser.service';
import { FlyerNormalizerService } from './services/flyer-normalizer.service';
import { FlyerNormalizerService } from './services/flyer-normalizer.service';
import { describeMatchReason, describeParseReason } from './services/reason-codes';
type FlyerParseItem = {
rawName: string;
@@ -135,13 +136,15 @@ export class FlyerImportService {
offerLimitText,
parseConfidence: item.confidence,
parseReasons: item.reasonCodes,
matchedProductId: match.product?.id ?? null,
matchedProductName: match.product?.name ?? null,
matchedVia: match.via,
matchConfidence: match.confidence,
matchReasons: match.reasons,
};
});
parseReasonsDetailed: this.describeParseReasons(item.reasonCodes),
matchedProductId: match.product?.id ?? null,
matchedProductName: match.product?.name ?? null,
matchedVia: match.via,
matchConfidence: match.confidence,
matchReasons: match.reasons,
matchReasonsDetailed: this.describeMatchReasons(match.reasons),
};
});
const persistedItems = await this.persistSessionWithItems(userId, parsed.retailer, items, file);
@@ -790,14 +793,24 @@ export class FlyerImportService {
offerLimitText,
parseConfidence: item.parseConfidence,
parseReasons: toStringArray(item.parseReasons),
parseReasonsDetailed: this.describeParseReasons(toStringArray(item.parseReasons)),
matchedProductId: item.matchedProductId,
matchedProductName: item.matchedProductName,
matchedVia: normalizedMatchVia,
matchConfidence: item.matchConfidence ?? 0,
matchReasons: toStringArray(item.matchReasons),
matchReasonsDetailed: this.describeMatchReasons(toStringArray(item.matchReasons)),
};
}
private describeParseReasons(codes: string[]) {
return codes.map((code) => describeParseReason(code));
}
private describeMatchReasons(codes: string[]) {
return codes.map((code) => describeMatchReason(code));
}
private buildCategoryPath(categoryRef?: {
name: string;
parent?: {