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
@@ -1,5 +1,53 @@
import 'admin_ai_trace.dart';
class AdminAiWarning {
final String code;
final String kind;
final String title;
final String message;
final String severity;
final String? location;
final int? itemIndex;
const AdminAiWarning({
required this.code,
required this.kind,
required this.title,
required this.message,
required this.severity,
required this.location,
required this.itemIndex,
});
factory AdminAiWarning.fromJson(Map<String, dynamic> json) {
return AdminAiWarning(
code: (json['code'] ?? '').toString(),
kind: (json['kind'] ?? '').toString(),
title: (json['title'] ?? '').toString(),
message: (json['message'] ?? '').toString(),
severity: (json['severity'] ?? '').toString(),
location: json['location']?.toString(),
itemIndex: (json['itemIndex'] as num?)?.toInt(),
);
}
factory AdminAiWarning.fromLegacy(String value) {
final trimmed = value.trim();
final parts = trimmed.split(':');
final kind = parts.isNotEmpty ? parts.first : 'parse';
final code = parts.length > 1 ? parts.sublist(1).join(':') : trimmed;
return AdminAiWarning(
code: code,
kind: kind,
title: trimmed,
message: trimmed,
severity: 'warning',
location: null,
itemIndex: null,
);
}
}
class AdminAiTraceDetail {
final String id;
final AdminAiTraceSource source;
@@ -13,7 +61,8 @@ class AdminAiTraceDetail {
final int? durationMs;
final int? retryCount;
final int? chunkCount;
final List<String> warnings;
final List<AdminAiWarning> warnings;
final List<String> legacyWarnings;
final String? error;
final String? prompt;
final String? rawOutput;
@@ -34,6 +83,7 @@ class AdminAiTraceDetail {
required this.retryCount,
required this.chunkCount,
required this.warnings,
required this.legacyWarnings,
required this.error,
required this.prompt,
required this.rawOutput,
@@ -43,6 +93,7 @@ class AdminAiTraceDetail {
factory AdminAiTraceDetail.fromJson(Map<String, dynamic> json) {
final warningsRaw = (json['warnings'] as List<dynamic>?) ?? const [];
final legacyWarningsRaw = (json['legacyWarnings'] as List<dynamic>?) ?? const [];
final normalizedOutputMap = json['normalizedOutput'] is Map
? Map<String, dynamic>.from(json['normalizedOutput'] as Map)
: null;
@@ -64,7 +115,15 @@ class AdminAiTraceDetail {
durationMs: (json['durationMs'] as num?)?.toInt(),
retryCount: (json['retryCount'] as num?)?.toInt(),
chunkCount: (json['chunkCount'] as num?)?.toInt(),
warnings: warningsRaw.map((entry) => entry.toString()).toList(),
warnings: warningsRaw
.map((entry) {
if (entry is Map) {
return AdminAiWarning.fromJson(Map<String, dynamic>.from(entry));
}
return AdminAiWarning.fromLegacy(entry.toString());
})
.toList(),
legacyWarnings: legacyWarningsRaw.map((entry) => entry.toString()).toList(),
error: json['error']?.toString(),
prompt: json['prompt']?.toString(),
rawOutput: json['rawOutput']?.toString(),