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:
@@ -1,4 +1,6 @@
|
||||
class FlyerImportItem {
|
||||
import 'flyer_reason_descriptor.dart';
|
||||
|
||||
class FlyerImportItem {
|
||||
final int? flyerItemId;
|
||||
final String rawName;
|
||||
final String normalizedName;
|
||||
@@ -12,11 +14,14 @@ class FlyerImportItem {
|
||||
final double? comparisonPrice;
|
||||
final String? comparisonUnit;
|
||||
final double? parseConfidence;
|
||||
final List<String> parseReasons;
|
||||
final int? matchedProductId;
|
||||
final String? matchedProductName;
|
||||
final String? matchedVia;
|
||||
final double? matchConfidence;
|
||||
final List<String> parseReasons;
|
||||
final List<FlyerReasonDescriptor> parseReasonsDetailed;
|
||||
final int? matchedProductId;
|
||||
final String? matchedProductName;
|
||||
final String? matchedVia;
|
||||
final double? matchConfidence;
|
||||
final List<String> matchReasons;
|
||||
final List<FlyerReasonDescriptor> matchReasonsDetailed;
|
||||
|
||||
FlyerImportItem({
|
||||
required this.flyerItemId,
|
||||
@@ -31,13 +36,16 @@ class FlyerImportItem {
|
||||
this.offerLimitText,
|
||||
this.comparisonPrice,
|
||||
this.comparisonUnit,
|
||||
this.parseConfidence,
|
||||
this.parseReasons = const [],
|
||||
this.matchedProductId,
|
||||
this.matchedProductName,
|
||||
this.matchedVia,
|
||||
this.matchConfidence,
|
||||
});
|
||||
this.parseConfidence,
|
||||
this.parseReasons = const [],
|
||||
this.parseReasonsDetailed = const [],
|
||||
this.matchedProductId,
|
||||
this.matchedProductName,
|
||||
this.matchedVia,
|
||||
this.matchConfidence,
|
||||
this.matchReasons = const [],
|
||||
this.matchReasonsDetailed = const [],
|
||||
});
|
||||
|
||||
factory FlyerImportItem.fromJson(Map<String, dynamic> json) {
|
||||
return FlyerImportItem(
|
||||
@@ -53,12 +61,25 @@ class FlyerImportItem {
|
||||
offerLimitText: json['offerLimitText'] as String?,
|
||||
comparisonPrice: (json['comparisonPrice'] as num?)?.toDouble(),
|
||||
comparisonUnit: json['comparisonUnit'] as String?,
|
||||
parseConfidence: (json['parseConfidence'] as num?)?.toDouble(),
|
||||
parseReasons: (json['parseReasons'] as List?)?.map((e) => e.toString()).toList() ?? const [],
|
||||
matchedProductId: (json['matchedProductId'] as num?)?.toInt(),
|
||||
matchedProductName: json['matchedProductName'] as String?,
|
||||
matchedVia: json['matchedVia'] as String?,
|
||||
matchConfidence: (json['matchConfidence'] as num?)?.toDouble(),
|
||||
parseConfidence: (json['parseConfidence'] as num?)?.toDouble(),
|
||||
parseReasons: (json['parseReasons'] as List?)?.map((e) => e.toString()).toList() ?? const [],
|
||||
parseReasonsDetailed:
|
||||
(json['parseReasonsDetailed'] as List?)
|
||||
?.whereType<Map>()
|
||||
.map((e) => FlyerReasonDescriptor.fromJson(Map<String, dynamic>.from(e)))
|
||||
.toList() ??
|
||||
const [],
|
||||
matchedProductId: (json['matchedProductId'] as num?)?.toInt(),
|
||||
matchedProductName: json['matchedProductName'] as String?,
|
||||
matchedVia: json['matchedVia'] as String?,
|
||||
matchConfidence: (json['matchConfidence'] as num?)?.toDouble(),
|
||||
matchReasons: (json['matchReasons'] as List?)?.map((e) => e.toString()).toList() ?? const [],
|
||||
matchReasonsDetailed:
|
||||
(json['matchReasonsDetailed'] as List?)
|
||||
?.whereType<Map>()
|
||||
.map((e) => FlyerReasonDescriptor.fromJson(Map<String, dynamic>.from(e)))
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,10 +99,15 @@ class FlyerImportItem {
|
||||
'comparisonUnit': comparisonUnit,
|
||||
'parseConfidence': parseConfidence,
|
||||
'parseReasons': parseReasons,
|
||||
'parseReasonsDetailed':
|
||||
parseReasonsDetailed.map((reason) => reason.toJson()).toList(),
|
||||
'matchedProductId': matchedProductId,
|
||||
'matchedProductName': matchedProductName,
|
||||
'matchedVia': matchedVia,
|
||||
'matchConfidence': matchConfidence,
|
||||
'matchReasons': matchReasons,
|
||||
'matchReasonsDetailed':
|
||||
matchReasonsDetailed.map((reason) => reason.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,10 +131,13 @@ class FlyerImportItem {
|
||||
comparisonUnit: comparisonUnit,
|
||||
parseConfidence: parseConfidence,
|
||||
parseReasons: parseReasons,
|
||||
parseReasonsDetailed: parseReasonsDetailed,
|
||||
matchedProductId: matchedProductId,
|
||||
matchedProductName: matchedProductName,
|
||||
matchedVia: matchedVia,
|
||||
matchConfidence: matchConfidence,
|
||||
matchReasons: matchReasons,
|
||||
matchReasonsDetailed: matchReasonsDetailed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
class FlyerReasonDescriptor {
|
||||
final String code;
|
||||
final String kind;
|
||||
final String title;
|
||||
final String message;
|
||||
final String severity;
|
||||
final String? location;
|
||||
|
||||
const FlyerReasonDescriptor({
|
||||
required this.code,
|
||||
required this.kind,
|
||||
required this.title,
|
||||
required this.message,
|
||||
required this.severity,
|
||||
required this.location,
|
||||
});
|
||||
|
||||
factory FlyerReasonDescriptor.fromJson(Map<String, dynamic> json) {
|
||||
return FlyerReasonDescriptor(
|
||||
code: (json['code'] ?? '').toString(),
|
||||
kind: (json['kind'] ?? '').toString(),
|
||||
title: (json['title'] ?? '').toString(),
|
||||
message: (json['message'] ?? '').toString(),
|
||||
severity: (json['severity'] ?? '').toString(),
|
||||
location: json['location']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'code': code,
|
||||
'kind': kind,
|
||||
'title': title,
|
||||
'message': message,
|
||||
'severity': severity,
|
||||
'location': location,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user