@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.
@@ -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: ${analysis.matched_rules.length} matched, ${ruleEvaluations.length - analysis.matched_rules.length} failed`,
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
- await eventLogger.info('Rule Evaluation', `Evaluated ${ruleEvaluations.length} rules: ${analysis.matched_rules.length} matched, ${ruleEvaluations.length - analysis.matched_rules.length} failed`, {
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