@realtimex/email-automator 2.24.0 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/services/processor.ts +150 -2
- package/dist/api/src/services/processor.js +126 -2
- package/dist/assets/{index-FRMyxVih.js → index-BHMeV1Oy.js} +64 -64
- package/dist/assets/index-fVuZ4_5V.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/assets/index-otwjpYTB.css +0 -1
|
@@ -813,10 +813,22 @@ export class EmailProcessorService {
|
|
|
813
813
|
|
|
814
814
|
if (eventLogger) await eventLogger.info('Processing', `Background processing: ${email.subject}`, undefined, email.id);
|
|
815
815
|
|
|
816
|
+
// Timeline tracking for performance metrics
|
|
817
|
+
const timeline = {
|
|
818
|
+
start: Date.now(),
|
|
819
|
+
parsed: 0,
|
|
820
|
+
metadata_extracted: 0,
|
|
821
|
+
llm_analysis: 0,
|
|
822
|
+
validation: 0,
|
|
823
|
+
actions: 0,
|
|
824
|
+
end: 0
|
|
825
|
+
};
|
|
826
|
+
|
|
816
827
|
// 2. Read content from disk and parse with mailparser
|
|
817
828
|
if (!email.file_path) throw new Error('No file path found for email');
|
|
818
829
|
const rawMime = await this.storageService.readEmail(email.file_path);
|
|
819
830
|
const parsed = await simpleParser(rawMime);
|
|
831
|
+
timeline.parsed = Date.now();
|
|
820
832
|
|
|
821
833
|
// Extract clean content (prioritize text)
|
|
822
834
|
const cleanContent = parsed.text || parsed.textAsHtml || '';
|
|
@@ -835,6 +847,40 @@ export class EmailProcessorService {
|
|
|
835
847
|
sender_priority: email.sender_priority || undefined,
|
|
836
848
|
thread_id: email.thread_id || undefined,
|
|
837
849
|
};
|
|
850
|
+
timeline.metadata_extracted = Date.now();
|
|
851
|
+
|
|
852
|
+
// Calculate email age
|
|
853
|
+
const emailAge = email.date ? Math.floor((Date.now() - new Date(email.date).getTime()) / (1000 * 60 * 60 * 24)) : 0;
|
|
854
|
+
|
|
855
|
+
// Check for VIP sender and learned patterns (for metadata event)
|
|
856
|
+
const senderDomain = email.sender?.split('@')[1];
|
|
857
|
+
const learnedCategory = senderDomain && settings?.category_patterns?.[senderDomain];
|
|
858
|
+
const isVIP = email.sender && settings?.vip_senders?.includes(email.sender);
|
|
859
|
+
|
|
860
|
+
// Log Email Metadata event for trace UI
|
|
861
|
+
if (eventLogger) {
|
|
862
|
+
await eventLogger.info('Email Context',
|
|
863
|
+
`Email metadata extracted: ${emailAge} days old, ${metadata.recipient_type || 'TO'} recipient`,
|
|
864
|
+
{
|
|
865
|
+
email_age_days: emailAge,
|
|
866
|
+
email_date: email.date,
|
|
867
|
+
recipient_type: metadata.recipient_type || 'to',
|
|
868
|
+
is_automated: metadata.is_automated || false,
|
|
869
|
+
has_unsubscribe: metadata.has_unsubscribe || false,
|
|
870
|
+
is_reply: metadata.is_reply || false,
|
|
871
|
+
is_thread: !!metadata.thread_id,
|
|
872
|
+
sender_priority: metadata.sender_priority || 'normal',
|
|
873
|
+
mailer: metadata.mailer || 'unknown',
|
|
874
|
+
vip_sender: isVIP || false,
|
|
875
|
+
vip_sender_email: isVIP ? email.sender : null,
|
|
876
|
+
learned_category: learnedCategory || null,
|
|
877
|
+
learned_domain: learnedCategory ? senderDomain : null,
|
|
878
|
+
sender: email.sender,
|
|
879
|
+
subject: email.subject
|
|
880
|
+
},
|
|
881
|
+
email.id
|
|
882
|
+
);
|
|
883
|
+
}
|
|
838
884
|
|
|
839
885
|
// 3. Fetch account for action execution
|
|
840
886
|
const { data: account } = await this.supabase
|
|
@@ -971,9 +1017,13 @@ export class EmailProcessorService {
|
|
|
971
1017
|
if (!analysis) {
|
|
972
1018
|
throw new Error('AI analysis returned no result');
|
|
973
1019
|
}
|
|
1020
|
+
timeline.llm_analysis = Date.now();
|
|
974
1021
|
|
|
975
1022
|
// PHASE 2: Post-LLM Validation - Filter out incorrectly matched rules
|
|
976
1023
|
// This catches any LLM hallucinations or fuzzy matches that don't meet actual conditions
|
|
1024
|
+
// Track detailed validation results for trace UI
|
|
1025
|
+
const validationDetails: any[] = [];
|
|
1026
|
+
|
|
977
1027
|
if (analysis.matched_rules && analysis.matched_rules.length > 0 && rules) {
|
|
978
1028
|
const emailAge = email.date ? Math.floor((Date.now() - new Date(email.date).getTime()) / (1000 * 60 * 60 * 24)) : 0;
|
|
979
1029
|
const validatedMatches = [];
|
|
@@ -1012,6 +1062,17 @@ export class EmailProcessorService {
|
|
|
1012
1062
|
min_confidence: minConfidence,
|
|
1013
1063
|
email_id: email.id
|
|
1014
1064
|
});
|
|
1065
|
+
|
|
1066
|
+
// Track validation failure for trace UI
|
|
1067
|
+
validationDetails.push({
|
|
1068
|
+
rule_name: rule.name,
|
|
1069
|
+
rule_id: rule.id,
|
|
1070
|
+
status: 'FILTERED_CONFIDENCE',
|
|
1071
|
+
confidence: match.confidence,
|
|
1072
|
+
min_confidence: minConfidence,
|
|
1073
|
+
reason: `Confidence ${(match.confidence * 100).toFixed(0)}% below threshold ${(minConfidence * 100).toFixed(0)}%`
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1015
1076
|
if (eventLogger) {
|
|
1016
1077
|
await eventLogger.info('Validation',
|
|
1017
1078
|
`Rule "${rule.name}" below confidence threshold (${(match.confidence * 100).toFixed(0)}% < ${(minConfidence * 100).toFixed(0)}%)`,
|
|
@@ -1049,6 +1110,18 @@ export class EmailProcessorService {
|
|
|
1049
1110
|
negative_condition: rule.negative_condition,
|
|
1050
1111
|
email_id: email.id
|
|
1051
1112
|
});
|
|
1113
|
+
|
|
1114
|
+
// Track validation failure for trace UI
|
|
1115
|
+
validationDetails.push({
|
|
1116
|
+
rule_name: rule.name,
|
|
1117
|
+
rule_id: rule.id,
|
|
1118
|
+
status: 'FILTERED_NEGATIVE_CONDITION',
|
|
1119
|
+
confidence: match.confidence,
|
|
1120
|
+
min_confidence: minConfidence,
|
|
1121
|
+
negative_condition: rule.negative_condition,
|
|
1122
|
+
reason: 'Excluded by negative condition'
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1052
1125
|
if (eventLogger) {
|
|
1053
1126
|
await eventLogger.info('Validation',
|
|
1054
1127
|
`Rule "${rule.name}" excluded by negative condition`,
|
|
@@ -1063,8 +1136,28 @@ export class EmailProcessorService {
|
|
|
1063
1136
|
}
|
|
1064
1137
|
|
|
1065
1138
|
if (isValid && !isExcluded) {
|
|
1139
|
+
// Track successful validation for trace UI
|
|
1140
|
+
validationDetails.push({
|
|
1141
|
+
rule_name: rule.name,
|
|
1142
|
+
rule_id: rule.id,
|
|
1143
|
+
status: 'MATCHED',
|
|
1144
|
+
confidence: match.confidence,
|
|
1145
|
+
min_confidence: minConfidence,
|
|
1146
|
+
reasoning: match.reasoning,
|
|
1147
|
+
reason: 'All conditions met, confidence above threshold'
|
|
1148
|
+
});
|
|
1066
1149
|
validatedMatches.push(match);
|
|
1067
|
-
} else {
|
|
1150
|
+
} else if (!isExcluded) {
|
|
1151
|
+
// Track condition failure for trace UI
|
|
1152
|
+
validationDetails.push({
|
|
1153
|
+
rule_name: rule.name,
|
|
1154
|
+
rule_id: rule.id,
|
|
1155
|
+
status: 'FILTERED_CONDITIONS',
|
|
1156
|
+
confidence: match.confidence,
|
|
1157
|
+
min_confidence: minConfidence,
|
|
1158
|
+
reason: 'LLM matched but rule conditions not met'
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1068
1161
|
logger.info('Filtered out invalid LLM rule match', {
|
|
1069
1162
|
rule_name: rule.name,
|
|
1070
1163
|
rule_id: rule.id,
|
|
@@ -1090,6 +1183,9 @@ export class EmailProcessorService {
|
|
|
1090
1183
|
analysis.matched_rules = validatedMatches;
|
|
1091
1184
|
}
|
|
1092
1185
|
|
|
1186
|
+
// Mark validation complete
|
|
1187
|
+
timeline.validation = Date.now();
|
|
1188
|
+
|
|
1093
1189
|
// Log detailed rule evaluation for debugging
|
|
1094
1190
|
if (eventLogger && rules) {
|
|
1095
1191
|
const emailAge = email.date ? Math.floor((Date.now() - new Date(email.date).getTime()) / (1000 * 60 * 60 * 24)) : 0;
|
|
@@ -1162,8 +1258,17 @@ export class EmailProcessorService {
|
|
|
1162
1258
|
const learnedCategory = senderDomain && settings?.category_patterns?.[senderDomain];
|
|
1163
1259
|
const isVIP = email.sender && settings?.vip_senders?.includes(email.sender);
|
|
1164
1260
|
|
|
1261
|
+
// Count validation results
|
|
1262
|
+
const validationSummary = {
|
|
1263
|
+
llm_matched: validationDetails.length,
|
|
1264
|
+
final_matched: validationDetails.filter(v => v.status === 'MATCHED').length,
|
|
1265
|
+
filtered_confidence: validationDetails.filter(v => v.status === 'FILTERED_CONFIDENCE').length,
|
|
1266
|
+
filtered_negative: validationDetails.filter(v => v.status === 'FILTERED_NEGATIVE_CONDITION').length,
|
|
1267
|
+
filtered_conditions: validationDetails.filter(v => v.status === 'FILTERED_CONDITIONS').length
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1165
1270
|
await eventLogger.info('Rule Evaluation',
|
|
1166
|
-
`Evaluated ${ruleEvaluations.length} rules: ${
|
|
1271
|
+
`Evaluated ${ruleEvaluations.length} rules: ${validationSummary.final_matched} matched, ${validationSummary.llm_matched - validationSummary.final_matched} filtered`,
|
|
1167
1272
|
{
|
|
1168
1273
|
ai_analysis: {
|
|
1169
1274
|
category: analysis.category,
|
|
@@ -1171,6 +1276,8 @@ export class EmailProcessorService {
|
|
|
1171
1276
|
email_age_days: emailAge,
|
|
1172
1277
|
summary: analysis.summary
|
|
1173
1278
|
},
|
|
1279
|
+
validation_summary: validationSummary,
|
|
1280
|
+
validation_details: validationDetails,
|
|
1174
1281
|
learned_patterns_applied: {
|
|
1175
1282
|
category_override: learnedCategory ? {
|
|
1176
1283
|
domain: senderDomain,
|
|
@@ -1373,6 +1480,47 @@ export class EmailProcessorService {
|
|
|
1373
1480
|
);
|
|
1374
1481
|
}
|
|
1375
1482
|
|
|
1483
|
+
// Mark actions complete
|
|
1484
|
+
timeline.actions = Date.now();
|
|
1485
|
+
timeline.end = Date.now();
|
|
1486
|
+
|
|
1487
|
+
// Log performance summary
|
|
1488
|
+
if (eventLogger) {
|
|
1489
|
+
const performanceMetrics = {
|
|
1490
|
+
total_time_ms: timeline.end - timeline.start,
|
|
1491
|
+
parse_time_ms: timeline.parsed - timeline.start,
|
|
1492
|
+
metadata_extraction_ms: timeline.metadata_extracted - timeline.parsed,
|
|
1493
|
+
llm_analysis_ms: timeline.llm_analysis - timeline.metadata_extracted,
|
|
1494
|
+
validation_ms: timeline.validation - timeline.llm_analysis,
|
|
1495
|
+
actions_ms: timeline.actions - timeline.validation,
|
|
1496
|
+
finalization_ms: timeline.end - timeline.actions
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1499
|
+
const breakdown = [
|
|
1500
|
+
`Parse: ${performanceMetrics.parse_time_ms}ms`,
|
|
1501
|
+
`Metadata: ${performanceMetrics.metadata_extraction_ms}ms`,
|
|
1502
|
+
`LLM: ${performanceMetrics.llm_analysis_ms}ms`,
|
|
1503
|
+
`Validation: ${performanceMetrics.validation_ms}ms`,
|
|
1504
|
+
`Actions: ${performanceMetrics.actions_ms}ms`
|
|
1505
|
+
].join(', ');
|
|
1506
|
+
|
|
1507
|
+
await eventLogger.info('Performance',
|
|
1508
|
+
`Completed in ${performanceMetrics.total_time_ms}ms (${breakdown})`,
|
|
1509
|
+
{
|
|
1510
|
+
timeline: performanceMetrics,
|
|
1511
|
+
stages: {
|
|
1512
|
+
parse: { duration_ms: performanceMetrics.parse_time_ms, percent: ((performanceMetrics.parse_time_ms / performanceMetrics.total_time_ms) * 100).toFixed(1) },
|
|
1513
|
+
metadata: { duration_ms: performanceMetrics.metadata_extraction_ms, percent: ((performanceMetrics.metadata_extraction_ms / performanceMetrics.total_time_ms) * 100).toFixed(1) },
|
|
1514
|
+
llm: { duration_ms: performanceMetrics.llm_analysis_ms, percent: ((performanceMetrics.llm_analysis_ms / performanceMetrics.total_time_ms) * 100).toFixed(1) },
|
|
1515
|
+
validation: { duration_ms: performanceMetrics.validation_ms, percent: ((performanceMetrics.validation_ms / performanceMetrics.total_time_ms) * 100).toFixed(1) },
|
|
1516
|
+
actions: { duration_ms: performanceMetrics.actions_ms, percent: ((performanceMetrics.actions_ms / performanceMetrics.total_time_ms) * 100).toFixed(1) },
|
|
1517
|
+
finalization: { duration_ms: performanceMetrics.finalization_ms, percent: ((performanceMetrics.finalization_ms / performanceMetrics.total_time_ms) * 100).toFixed(1) }
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
email.id
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1376
1524
|
// Mark log as success
|
|
1377
1525
|
if (log) {
|
|
1378
1526
|
await this.supabase
|
|
@@ -679,11 +679,22 @@ export class EmailProcessorService {
|
|
|
679
679
|
.eq('id', email.id);
|
|
680
680
|
if (eventLogger)
|
|
681
681
|
await eventLogger.info('Processing', `Background processing: ${email.subject}`, undefined, email.id);
|
|
682
|
+
// Timeline tracking for performance metrics
|
|
683
|
+
const timeline = {
|
|
684
|
+
start: Date.now(),
|
|
685
|
+
parsed: 0,
|
|
686
|
+
metadata_extracted: 0,
|
|
687
|
+
llm_analysis: 0,
|
|
688
|
+
validation: 0,
|
|
689
|
+
actions: 0,
|
|
690
|
+
end: 0
|
|
691
|
+
};
|
|
682
692
|
// 2. Read content from disk and parse with mailparser
|
|
683
693
|
if (!email.file_path)
|
|
684
694
|
throw new Error('No file path found for email');
|
|
685
695
|
const rawMime = await this.storageService.readEmail(email.file_path);
|
|
686
696
|
const parsed = await simpleParser(rawMime);
|
|
697
|
+
timeline.parsed = Date.now();
|
|
687
698
|
// Extract clean content (prioritize text)
|
|
688
699
|
const cleanContent = parsed.text || parsed.textAsHtml || '';
|
|
689
700
|
// Extract metadata signals from headers (legacy fields + enhanced header metadata)
|
|
@@ -700,6 +711,33 @@ export class EmailProcessorService {
|
|
|
700
711
|
sender_priority: email.sender_priority || undefined,
|
|
701
712
|
thread_id: email.thread_id || undefined,
|
|
702
713
|
};
|
|
714
|
+
timeline.metadata_extracted = Date.now();
|
|
715
|
+
// Calculate email age
|
|
716
|
+
const emailAge = email.date ? Math.floor((Date.now() - new Date(email.date).getTime()) / (1000 * 60 * 60 * 24)) : 0;
|
|
717
|
+
// Check for VIP sender and learned patterns (for metadata event)
|
|
718
|
+
const senderDomain = email.sender?.split('@')[1];
|
|
719
|
+
const learnedCategory = senderDomain && settings?.category_patterns?.[senderDomain];
|
|
720
|
+
const isVIP = email.sender && settings?.vip_senders?.includes(email.sender);
|
|
721
|
+
// Log Email Metadata event for trace UI
|
|
722
|
+
if (eventLogger) {
|
|
723
|
+
await eventLogger.info('Email Context', `Email metadata extracted: ${emailAge} days old, ${metadata.recipient_type || 'TO'} recipient`, {
|
|
724
|
+
email_age_days: emailAge,
|
|
725
|
+
email_date: email.date,
|
|
726
|
+
recipient_type: metadata.recipient_type || 'to',
|
|
727
|
+
is_automated: metadata.is_automated || false,
|
|
728
|
+
has_unsubscribe: metadata.has_unsubscribe || false,
|
|
729
|
+
is_reply: metadata.is_reply || false,
|
|
730
|
+
is_thread: !!metadata.thread_id,
|
|
731
|
+
sender_priority: metadata.sender_priority || 'normal',
|
|
732
|
+
mailer: metadata.mailer || 'unknown',
|
|
733
|
+
vip_sender: isVIP || false,
|
|
734
|
+
vip_sender_email: isVIP ? email.sender : null,
|
|
735
|
+
learned_category: learnedCategory || null,
|
|
736
|
+
learned_domain: learnedCategory ? senderDomain : null,
|
|
737
|
+
sender: email.sender,
|
|
738
|
+
subject: email.subject
|
|
739
|
+
}, email.id);
|
|
740
|
+
}
|
|
703
741
|
// 3. Fetch account for action execution
|
|
704
742
|
const { data: account } = await this.supabase
|
|
705
743
|
.from('email_accounts')
|
|
@@ -834,8 +872,11 @@ export class EmailProcessorService {
|
|
|
834
872
|
if (!analysis) {
|
|
835
873
|
throw new Error('AI analysis returned no result');
|
|
836
874
|
}
|
|
875
|
+
timeline.llm_analysis = Date.now();
|
|
837
876
|
// PHASE 2: Post-LLM Validation - Filter out incorrectly matched rules
|
|
838
877
|
// This catches any LLM hallucinations or fuzzy matches that don't meet actual conditions
|
|
878
|
+
// Track detailed validation results for trace UI
|
|
879
|
+
const validationDetails = [];
|
|
839
880
|
if (analysis.matched_rules && analysis.matched_rules.length > 0 && rules) {
|
|
840
881
|
const emailAge = email.date ? Math.floor((Date.now() - new Date(email.date).getTime()) / (1000 * 60 * 60 * 24)) : 0;
|
|
841
882
|
const validatedMatches = [];
|
|
@@ -870,6 +911,15 @@ export class EmailProcessorService {
|
|
|
870
911
|
min_confidence: minConfidence,
|
|
871
912
|
email_id: email.id
|
|
872
913
|
});
|
|
914
|
+
// Track validation failure for trace UI
|
|
915
|
+
validationDetails.push({
|
|
916
|
+
rule_name: rule.name,
|
|
917
|
+
rule_id: rule.id,
|
|
918
|
+
status: 'FILTERED_CONFIDENCE',
|
|
919
|
+
confidence: match.confidence,
|
|
920
|
+
min_confidence: minConfidence,
|
|
921
|
+
reason: `Confidence ${(match.confidence * 100).toFixed(0)}% below threshold ${(minConfidence * 100).toFixed(0)}%`
|
|
922
|
+
});
|
|
873
923
|
if (eventLogger) {
|
|
874
924
|
await eventLogger.info('Validation', `Rule "${rule.name}" below confidence threshold (${(match.confidence * 100).toFixed(0)}% < ${(minConfidence * 100).toFixed(0)}%)`, {
|
|
875
925
|
rule_id: rule.id,
|
|
@@ -901,6 +951,16 @@ export class EmailProcessorService {
|
|
|
901
951
|
negative_condition: rule.negative_condition,
|
|
902
952
|
email_id: email.id
|
|
903
953
|
});
|
|
954
|
+
// Track validation failure for trace UI
|
|
955
|
+
validationDetails.push({
|
|
956
|
+
rule_name: rule.name,
|
|
957
|
+
rule_id: rule.id,
|
|
958
|
+
status: 'FILTERED_NEGATIVE_CONDITION',
|
|
959
|
+
confidence: match.confidence,
|
|
960
|
+
min_confidence: minConfidence,
|
|
961
|
+
negative_condition: rule.negative_condition,
|
|
962
|
+
reason: 'Excluded by negative condition'
|
|
963
|
+
});
|
|
904
964
|
if (eventLogger) {
|
|
905
965
|
await eventLogger.info('Validation', `Rule "${rule.name}" excluded by negative condition`, {
|
|
906
966
|
rule_id: rule.id,
|
|
@@ -910,9 +970,28 @@ export class EmailProcessorService {
|
|
|
910
970
|
}
|
|
911
971
|
}
|
|
912
972
|
if (isValid && !isExcluded) {
|
|
973
|
+
// Track successful validation for trace UI
|
|
974
|
+
validationDetails.push({
|
|
975
|
+
rule_name: rule.name,
|
|
976
|
+
rule_id: rule.id,
|
|
977
|
+
status: 'MATCHED',
|
|
978
|
+
confidence: match.confidence,
|
|
979
|
+
min_confidence: minConfidence,
|
|
980
|
+
reasoning: match.reasoning,
|
|
981
|
+
reason: 'All conditions met, confidence above threshold'
|
|
982
|
+
});
|
|
913
983
|
validatedMatches.push(match);
|
|
914
984
|
}
|
|
915
|
-
else {
|
|
985
|
+
else if (!isExcluded) {
|
|
986
|
+
// Track condition failure for trace UI
|
|
987
|
+
validationDetails.push({
|
|
988
|
+
rule_name: rule.name,
|
|
989
|
+
rule_id: rule.id,
|
|
990
|
+
status: 'FILTERED_CONDITIONS',
|
|
991
|
+
confidence: match.confidence,
|
|
992
|
+
min_confidence: minConfidence,
|
|
993
|
+
reason: 'LLM matched but rule conditions not met'
|
|
994
|
+
});
|
|
916
995
|
logger.info('Filtered out invalid LLM rule match', {
|
|
917
996
|
rule_name: rule.name,
|
|
918
997
|
rule_id: rule.id,
|
|
@@ -932,6 +1011,8 @@ export class EmailProcessorService {
|
|
|
932
1011
|
// Replace with validated matches
|
|
933
1012
|
analysis.matched_rules = validatedMatches;
|
|
934
1013
|
}
|
|
1014
|
+
// Mark validation complete
|
|
1015
|
+
timeline.validation = Date.now();
|
|
935
1016
|
// Log detailed rule evaluation for debugging
|
|
936
1017
|
if (eventLogger && rules) {
|
|
937
1018
|
const emailAge = email.date ? Math.floor((Date.now() - new Date(email.date).getTime()) / (1000 * 60 * 60 * 24)) : 0;
|
|
@@ -995,13 +1076,23 @@ export class EmailProcessorService {
|
|
|
995
1076
|
const senderDomain = email.sender?.split('@')[1];
|
|
996
1077
|
const learnedCategory = senderDomain && settings?.category_patterns?.[senderDomain];
|
|
997
1078
|
const isVIP = email.sender && settings?.vip_senders?.includes(email.sender);
|
|
998
|
-
|
|
1079
|
+
// Count validation results
|
|
1080
|
+
const validationSummary = {
|
|
1081
|
+
llm_matched: validationDetails.length,
|
|
1082
|
+
final_matched: validationDetails.filter(v => v.status === 'MATCHED').length,
|
|
1083
|
+
filtered_confidence: validationDetails.filter(v => v.status === 'FILTERED_CONFIDENCE').length,
|
|
1084
|
+
filtered_negative: validationDetails.filter(v => v.status === 'FILTERED_NEGATIVE_CONDITION').length,
|
|
1085
|
+
filtered_conditions: validationDetails.filter(v => v.status === 'FILTERED_CONDITIONS').length
|
|
1086
|
+
};
|
|
1087
|
+
await eventLogger.info('Rule Evaluation', `Evaluated ${ruleEvaluations.length} rules: ${validationSummary.final_matched} matched, ${validationSummary.llm_matched - validationSummary.final_matched} filtered`, {
|
|
999
1088
|
ai_analysis: {
|
|
1000
1089
|
category: analysis.category,
|
|
1001
1090
|
confidence: analysis.matched_rules[0]?.confidence || 0,
|
|
1002
1091
|
email_age_days: emailAge,
|
|
1003
1092
|
summary: analysis.summary
|
|
1004
1093
|
},
|
|
1094
|
+
validation_summary: validationSummary,
|
|
1095
|
+
validation_details: validationDetails,
|
|
1005
1096
|
learned_patterns_applied: {
|
|
1006
1097
|
category_override: learnedCategory ? {
|
|
1007
1098
|
domain: senderDomain,
|
|
@@ -1136,6 +1227,39 @@ export class EmailProcessorService {
|
|
|
1136
1227
|
else if (eventLogger && rules && rules.length > 0) {
|
|
1137
1228
|
await eventLogger.info('No Match', 'No rules matched this email', { category: analysis.category }, email.id);
|
|
1138
1229
|
}
|
|
1230
|
+
// Mark actions complete
|
|
1231
|
+
timeline.actions = Date.now();
|
|
1232
|
+
timeline.end = Date.now();
|
|
1233
|
+
// Log performance summary
|
|
1234
|
+
if (eventLogger) {
|
|
1235
|
+
const performanceMetrics = {
|
|
1236
|
+
total_time_ms: timeline.end - timeline.start,
|
|
1237
|
+
parse_time_ms: timeline.parsed - timeline.start,
|
|
1238
|
+
metadata_extraction_ms: timeline.metadata_extracted - timeline.parsed,
|
|
1239
|
+
llm_analysis_ms: timeline.llm_analysis - timeline.metadata_extracted,
|
|
1240
|
+
validation_ms: timeline.validation - timeline.llm_analysis,
|
|
1241
|
+
actions_ms: timeline.actions - timeline.validation,
|
|
1242
|
+
finalization_ms: timeline.end - timeline.actions
|
|
1243
|
+
};
|
|
1244
|
+
const breakdown = [
|
|
1245
|
+
`Parse: ${performanceMetrics.parse_time_ms}ms`,
|
|
1246
|
+
`Metadata: ${performanceMetrics.metadata_extraction_ms}ms`,
|
|
1247
|
+
`LLM: ${performanceMetrics.llm_analysis_ms}ms`,
|
|
1248
|
+
`Validation: ${performanceMetrics.validation_ms}ms`,
|
|
1249
|
+
`Actions: ${performanceMetrics.actions_ms}ms`
|
|
1250
|
+
].join(', ');
|
|
1251
|
+
await eventLogger.info('Performance', `Completed in ${performanceMetrics.total_time_ms}ms (${breakdown})`, {
|
|
1252
|
+
timeline: performanceMetrics,
|
|
1253
|
+
stages: {
|
|
1254
|
+
parse: { duration_ms: performanceMetrics.parse_time_ms, percent: ((performanceMetrics.parse_time_ms / performanceMetrics.total_time_ms) * 100).toFixed(1) },
|
|
1255
|
+
metadata: { duration_ms: performanceMetrics.metadata_extraction_ms, percent: ((performanceMetrics.metadata_extraction_ms / performanceMetrics.total_time_ms) * 100).toFixed(1) },
|
|
1256
|
+
llm: { duration_ms: performanceMetrics.llm_analysis_ms, percent: ((performanceMetrics.llm_analysis_ms / performanceMetrics.total_time_ms) * 100).toFixed(1) },
|
|
1257
|
+
validation: { duration_ms: performanceMetrics.validation_ms, percent: ((performanceMetrics.validation_ms / performanceMetrics.total_time_ms) * 100).toFixed(1) },
|
|
1258
|
+
actions: { duration_ms: performanceMetrics.actions_ms, percent: ((performanceMetrics.actions_ms / performanceMetrics.total_time_ms) * 100).toFixed(1) },
|
|
1259
|
+
finalization: { duration_ms: performanceMetrics.finalization_ms, percent: ((performanceMetrics.finalization_ms / performanceMetrics.total_time_ms) * 100).toFixed(1) }
|
|
1260
|
+
}
|
|
1261
|
+
}, email.id);
|
|
1262
|
+
}
|
|
1139
1263
|
// Mark log as success
|
|
1140
1264
|
if (log) {
|
|
1141
1265
|
await this.supabase
|