@realtimex/email-automator 2.6.4 → 2.6.5

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.
@@ -1,96 +1,111 @@
1
1
  export class ContentCleaner {
2
2
  /**
3
3
  * Cleans email body by removing noise, quoted replies, and footers.
4
- * Ported from Python ContentCleaner.
4
+ * optimized for LLM processing.
5
5
  */
6
6
  static cleanEmailBody(text: string): string {
7
7
  if (!text) return "";
8
8
  const originalText = text;
9
9
 
10
- // 0. Lightweight HTML -> Markdown Conversion
11
-
12
- // Structure: <br>, <p> -> Newlines
13
- text = text.replace(/<br\s*\/?>/gi, '\n');
14
- text = text.replace(/<\/p>/gi, '\n\n');
15
- text = text.replace(/<p.*?>/gi, ''); // Open p tags just gone
16
-
17
- // Structure: Headers <h1>-<h6> -> # Title
18
- text = text.replace(/<h[1-6].*?>(.*?)<\/h[1-6]>/gsi, (match, p1) => `\n# ${p1}\n`);
19
-
20
- // Structure: Lists <li> -> - Item
21
- text = text.replace(/<li.*?>(.*?)<\/li>/gsi, (match, p1) => `\n- ${p1}`);
22
- text = text.replace(/<ul.*?>/gi, '');
23
- text = text.replace(/<\/ul>/gi, '\n');
24
-
25
- // Links: <a href=\"...\">text</a> -> [text](href)
26
- text = text.replace(/<a\s+(?:[^>]*?\s+)?href=\"([^\"]*)\"[^>]*>(.*?)<\/a>/gsi, (match, href, content) => `[${content}](${href})`);
27
-
28
- // Images: <img src=\"...\" alt=\"...\"> -> ![alt](src)
29
- text = text.replace(/<img\s+(?:[^>]*?\s+)?src=\"([^\"]*)\"(?:[^>]*?\s+)?alt=\"([^\"]*)\"[^>]*>/gsi, (match, src, alt) => `![${alt}](${src})`);
10
+ // 1. Detect if content is actually HTML
11
+ const isHtml = /<[a-z][\s\S]*>/i.test(text);
30
12
 
31
- // Style/Script removal (strictly remove content)
32
- text = text.replace(/<script.*?>.*?<\/script>/gsi, '');
33
- text = text.replace(/<style.*?>.*?<\/style>/gsi, '');
34
-
35
- // Final Strip of remaining tags
36
- text = text.replace(/<[^>]+>/g, ' ');
37
-
38
- // Entity decoding (Basic)
39
- text = text.replace(/&nbsp;/gi, ' ');
40
- text = text.replace(/&amp;/gi, '&');
41
- text = text.replace(/&lt;/gi, '<');
42
- text = text.replace(/&gt;/gi, '>');
43
- text = text.replace(/&quot;/gi, '"');
44
- text = text.replace(/&#39;/gi, "'");
13
+ if (isHtml) {
14
+ // Lightweight HTML -> Markdown Conversion
15
+ // Structure: <br>, <p> -> Newlines
16
+ text = text.replace(/<br\s*\/?>/gi, '\n');
17
+ text = text.replace(/<\/p>/gi, '\n\n');
18
+ text = text.replace(/<p.*?>/gi, '');
19
+
20
+ // Structure: Headers <h1>-<h6> -> # Title
21
+ text = text.replace(/<h[1-6].*?>(.*?)<\/h[1-6]>/gsi, (match, p1) => `\n# ${p1}\n`);
22
+
23
+ // Structure: Lists <li> -> - Item
24
+ text = text.replace(/<li.*?>(.*?)<\/li>/gsi, (match, p1) => `\n- ${p1}`);
25
+ text = text.replace(/<ul.*?>/gi, '');
26
+ text = text.replace(/<\/ul>/gi, '\n');
27
+
28
+ // Links: <a href=\"...\">text</a> -> [text](href)
29
+ text = text.replace(/<a\s+(?:[^>]*?\s+)?href=\"([^\"]*)\"[^>]*>(.*?)<\/a>/gsi, (match, href, content) => `[${content}](${href})`);
30
+
31
+ // Images: <img src=\"...\" alt=\"...\"> -> ![alt](src)
32
+ text = text.replace(/<img\s+(?:[^>]*?\s+)?src=\"([^\"]*)\"(?:[^>]*?\s+)?alt=\"([^\"]*)\"[^>]*>/gsi, (match, src, alt) => `![${alt}](${src})`);
33
+
34
+ // Style/Script removal (strictly remove content)
35
+ text = text.replace(/<script.*?>.*?<\/script>/gsi, '');
36
+ text = text.replace(/<style.*?>.*?<\/style>/gsi, '');
37
+
38
+ // Final Strip of remaining tags
39
+ text = text.replace(/<[^>]+>/g, ' ');
40
+
41
+ // Entity decoding (Basic)
42
+ text = text.replace(/&nbsp;/gi, ' ');
43
+ text = text.replace(/&amp;/gi, '&');
44
+ text = text.replace(/&lt;/gi, '<');
45
+ text = text.replace(/&gt;/gi, '>');
46
+ text = text.replace(/&quot;/gi, '"');
47
+ text = text.replace(/&#39;/gi, "'");
48
+ }
45
49
 
46
50
  const lines = text.split('\n');
47
51
  const cleanedLines: string[] = [];
48
52
 
49
- // Heuristics for reply headers
50
- const replyHeaderPatterns = [
53
+ // Patterns that usually mark the START of a reply chain or a generic footer
54
+ const truncationPatterns = [
51
55
  /^On .* wrote:$/i,
52
- /^From: .*$/i,
53
- /^Sent: .*$/i,
54
- /^To: .*$/i,
55
- /^Subject: .*$/i
56
+ /^From: .* <.*>$/i,
57
+ /^-----Original Message-----$/i,
58
+ /^________________________________$/i,
59
+ /^Sent from my iPhone$/i,
60
+ /^Sent from my Android$/i,
61
+ /^Get Outlook for/i,
62
+ /^--$/ // Standard signature separator
56
63
  ];
57
64
 
58
- // Heuristics for footers
59
- const footerPatterns = [
60
- /unsubscribe/i,
65
+ // Patterns for lines that should be stripped but NOT truncate the whole email
66
+ const noisePatterns = [
67
+ /view in browser/i,
68
+ /click here to view/i,
69
+ /legal notice/i,
70
+ /all rights reserved/i,
61
71
  /privacy policy/i,
62
72
  /terms of service/i,
63
- /view in browser/i,
64
- /copyright \d{4}/i
73
+ /unsubscribe/i
65
74
  ];
66
75
 
67
76
  for (let line of lines) {
68
77
  let lineStripped = line.trim();
78
+ if (!lineStripped) {
79
+ cleanedLines.push("");
80
+ continue;
81
+ }
69
82
 
70
83
  // 2. Quoted text removal (lines starting with >)
71
84
  if (lineStripped.startsWith('>')) {
72
85
  continue;
73
86
  }
74
87
 
75
- // 3. Check for specific reply separators
76
- // If we hit a reply header, we truncate the rest
77
- if (/^On .* wrote:$/i.test(lineStripped)) {
78
- break;
88
+ // 3. Truncation check: If we hit a reply header, we stop entirely
89
+ let shouldTruncate = false;
90
+ for (const pattern of truncationPatterns) {
91
+ if (pattern.test(lineStripped)) {
92
+ shouldTruncate = true;
93
+ break;
94
+ }
79
95
  }
96
+ if (shouldTruncate) break;
80
97
 
81
- // 4. Footer removal (only on very short lines to avoid stripping body content)
82
- if (lineStripped.length < 60) {
83
- let isFooter = false;
84
- for (const pattern of footerPatterns) {
98
+ // 4. Noise check: Strip boilerplate lines
99
+ let isNoise = false;
100
+ if (lineStripped.length < 100) {
101
+ for (const pattern of noisePatterns) {
85
102
  if (pattern.test(lineStripped)) {
86
- isFooter = true;
103
+ isNoise = true;
87
104
  break;
88
105
  }
89
106
  }
90
- if (isFooter) {
91
- continue;
92
- }
93
107
  }
108
+ if (isNoise) continue;
94
109
 
95
110
  cleanedLines.push(line);
96
111
  }
@@ -98,21 +113,20 @@ export class ContentCleaner {
98
113
  // Reassemble
99
114
  text = cleanedLines.join('\n');
100
115
 
101
- // Safety Fallback: If cleaning stripped everything, return original (truncated)
102
- if (!text.trim() || text.length < 10) {
103
- text = originalText.substring(0, 3000);
104
- }
105
-
106
- // Collapse multiple newlines
116
+ // Collapse whitespace
107
117
  text = text.replace(/\n{3,}/g, '\n\n');
118
+ text = text.replace(/[ \t]{2,}/g, ' ');
108
119
 
120
+ // Safety Fallback: If cleaning stripped too much, return original text truncated
121
+ if (text.trim().length < 20 && originalText.trim().length > 20) {
122
+ return originalText.substring(0, 3000).trim();
123
+ }
124
+
109
125
  // Sanitize LLM Special Tokens
110
126
  text = text.replace(/<\|/g, '< |');
111
127
  text = text.replace(/\|>/g, '| >');
112
128
  text = text.replace(/\[INST\]/gi, '[ INST ]');
113
129
  text = text.replace(/\[\/INST\]/gi, '[ /INST ]');
114
- text = text.replace(/<s>/gi, '&lt;s&gt;');
115
- text = text.replace(/<\/s>/gi, '&lt;/s&gt;');
116
130
 
117
131
  return text.trim();
118
132
  }
@@ -82,8 +82,12 @@ export const schemas = {
82
82
  accessToken: z.string().optional(),
83
83
  }),
84
84
  // Rule schemas - supports both single action (legacy) and actions array
85
+ // Now includes description and intent for context-aware AI matching
85
86
  createRule: z.object({
86
87
  name: z.string().min(1).max(100),
88
+ description: z.string().max(500).optional(),
89
+ intent: z.string().max(200).optional(),
90
+ priority: z.number().int().min(0).max(100).optional(),
87
91
  condition: z.record(z.unknown()),
88
92
  action: z.enum(['delete', 'archive', 'draft', 'star', 'read']).optional(),
89
93
  actions: z.array(z.enum(['delete', 'archive', 'draft', 'star', 'read'])).optional(),
@@ -94,6 +98,9 @@ export const schemas = {
94
98
  }),
95
99
  updateRule: z.object({
96
100
  name: z.string().min(1).max(100).optional(),
101
+ description: z.string().max(500).optional(),
102
+ intent: z.string().max(200).optional(),
103
+ priority: z.number().int().min(0).max(100).optional(),
97
104
  condition: z.record(z.unknown()).optional(),
98
105
  action: z.enum(['delete', 'archive', 'draft', 'star', 'read']).optional(),
99
106
  actions: z.array(z.enum(['delete', 'archive', 'draft', 'star', 'read'])).optional(),
@@ -24,6 +24,28 @@ export const EmailAnalysisSchema = z.object({
24
24
  action_items: z.array(z.string()).optional()
25
25
  .describe('Action items mentioned in the email'),
26
26
  });
27
+ // Context-Aware Analysis Schema - AI evaluates email against user's rules
28
+ export const ContextAwareAnalysisSchema = z.object({
29
+ // Classification (kept for UI/logging)
30
+ summary: z.string().describe('A brief summary of the email content'),
31
+ category: z.enum(['spam', 'newsletter', 'promotional', 'transactional', 'social', 'support', 'client', 'internal', 'personal', 'other'])
32
+ .describe('The category of the email'),
33
+ priority: z.enum(['High', 'Medium', 'Low'])
34
+ .describe('The urgency of the email'),
35
+ // Rule Matching (core of context-aware engine)
36
+ matched_rule: z.object({
37
+ rule_id: z.string().nullable().describe('ID of the matched rule, or null if no match'),
38
+ rule_name: z.string().nullable().describe('Name of the matched rule'),
39
+ confidence: z.number().min(0).max(1).describe('Confidence score for the match (0-1)'),
40
+ reasoning: z.string().describe('Explanation of why this rule was matched or why no rule matched'),
41
+ }),
42
+ // Actions to execute (derived from matched rule)
43
+ actions_to_execute: z.array(z.enum(['none', 'delete', 'archive', 'draft', 'read', 'star']))
44
+ .describe('Actions to execute based on the matched rule'),
45
+ // Intent-aware draft content (if draft action is included)
46
+ draft_content: z.string().optional()
47
+ .describe('Generated draft reply if the action includes drafting'),
48
+ });
27
49
  export class IntelligenceService {
28
50
  client = null;
29
51
  model = 'gpt-4o-mini';
@@ -210,6 +232,171 @@ Please write a reply.`,
210
232
  return null;
211
233
  }
212
234
  }
235
+ /**
236
+ * Context-Aware Analysis: AI evaluates email against user's rules semantically
237
+ * This is the core of the new automation engine
238
+ *
239
+ * @param compiledRulesContext - Pre-compiled rules context string (from user_settings.compiled_rule_context)
240
+ * OR RuleContext[] for backwards compatibility
241
+ */
242
+ async analyzeEmailWithRules(content, context, compiledRulesContext, eventLogger, emailId) {
243
+ console.log('[Intelligence] analyzeEmailWithRules called for:', context.subject);
244
+ if (!this.isReady()) {
245
+ console.log('[Intelligence] Not ready, skipping');
246
+ logger.warn('Intelligence service not ready, skipping analysis');
247
+ if (eventLogger) {
248
+ await eventLogger.info('Skipped', 'AI Analysis skipped: Model not configured.', undefined, emailId);
249
+ }
250
+ return null;
251
+ }
252
+ // Prepare content
253
+ const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
254
+ // Use pre-compiled context if string, otherwise build from RuleContext[] (backwards compat)
255
+ let rulesContext;
256
+ let rulesCount;
257
+ if (typeof compiledRulesContext === 'string') {
258
+ // Fast path: use pre-compiled context
259
+ rulesContext = compiledRulesContext || '\n[No rules defined - analyze email but take no actions]\n';
260
+ rulesCount = (rulesContext.match(/Rule \d+/g) || []).length;
261
+ }
262
+ else {
263
+ // Backwards compatibility: build from RuleContext[]
264
+ const rules = compiledRulesContext;
265
+ rulesCount = rules.length;
266
+ rulesContext = rules.length > 0
267
+ ? rules.map((r, i) => `
268
+ ### Rule ${i + 1}: "${r.name}" (ID: ${r.id})
269
+ - Description: ${r.description || 'No description provided'}
270
+ - Intent: ${r.intent || 'General automation'}
271
+ - Actions: ${r.actions.join(', ')}
272
+ ${r.draft_instructions ? `- Draft Instructions: "${r.draft_instructions}"` : ''}
273
+ `).join('\n')
274
+ : '\n[No rules defined - analyze email but take no actions]\n';
275
+ }
276
+ const systemPrompt = `You are an AI Email Automation Agent.
277
+
278
+ ## Your Operating Rules
279
+ The user has defined the following automation rules. Your job is to:
280
+ 1. Analyze the incoming email
281
+ 2. Determine if ANY rule semantically matches this email's context
282
+ 3. Match based on INTENT, not just keywords
283
+
284
+ ${rulesContext}
285
+
286
+ ## Category Definitions (choose the most accurate)
287
+ - **client**: Business inquiries, RFPs, quote requests, project discussions, potential customers reaching out
288
+ - **support**: Help requests, bug reports, technical questions from existing users
289
+ - **internal**: Messages from colleagues, team communications
290
+ - **transactional**: Receipts, confirmations, shipping updates, account notifications
291
+ - **newsletter**: Subscribed content, digests, updates from services you signed up for
292
+ - **promotional**: UNSOLICITED marketing, cold sales pitches, ads - NOT legitimate business inquiries
293
+ - **spam**: Scams, phishing, junk mail
294
+ - **social**: Social media notifications, friend requests
295
+ - **personal**: Friends, family, personal matters
296
+ - **other**: Anything that doesn't fit above
297
+
298
+ ## Matching Guidelines
299
+ - A "decline sales" rule should match ANY sales pitch, not just ones with "sales" in the subject
300
+ - Match the rule that best fits the USER'S INTENT
301
+ - Only match if you are confident (>= 0.7 confidence)
302
+ - If no rule clearly matches, return null for rule_id
303
+ - If a matched rule includes "draft" action, generate an appropriate draft using the rule's intent
304
+
305
+ ## CRITICAL: Distinguish Between Inbound vs Outbound
306
+ **INBOUND (Client Inquiries - NOT promotional):**
307
+ - User is RECEIVING a request for quote/proposal/service
308
+ - Examples: "Please send me a quote", "RFP: [project]", "Can you provide pricing", "I need a quote asap"
309
+ - Category: client, support, or transactional (NEVER promotional)
310
+
311
+ **OUTBOUND (Sales/Marketing - IS promotional):**
312
+ - User is RECEIVING a sales pitch or marketing message
313
+ - Examples: "Get a FREE quote today!", "Limited offer", "Don't miss out", "Special discount"
314
+ - Category: promotional, spam, or newsletter
315
+
316
+ **Key Distinction:** If someone is ASKING the user for something (quote, proposal, service), it's a CLIENT INQUIRY, not promotional content.
317
+
318
+ ## Email Context
319
+ - Current Date: ${new Date().toISOString()}
320
+ - Subject: ${context.subject}
321
+ - From: ${context.sender}
322
+ - Date: ${context.date}
323
+
324
+ ## Required JSON Response
325
+ {
326
+ "summary": "Brief summary of the email",
327
+ "category": "spam|newsletter|promotional|transactional|social|support|client|internal|personal|other",
328
+ "priority": "High|Medium|Low",
329
+ "matched_rule": {
330
+ "rule_id": "UUID or null",
331
+ "rule_name": "Rule name or null",
332
+ "confidence": 0.0-1.0,
333
+ "reasoning": "Why this rule was or wasn't matched"
334
+ },
335
+ "actions_to_execute": ["none"] or ["archive", "read", etc.],
336
+ "draft_content": "Optional: draft reply if action includes 'draft'"
337
+ }
338
+
339
+ Return ONLY valid JSON.`;
340
+ // Log thinking phase
341
+ if (eventLogger) {
342
+ try {
343
+ await eventLogger.info('Thinking', `Context-aware analysis: ${context.subject}`, {
344
+ model: this.model,
345
+ system_prompt: systemPrompt,
346
+ content_preview: cleanedContent,
347
+ rules_count: rulesCount,
348
+ }, emailId);
349
+ }
350
+ catch (err) {
351
+ console.error('[Intelligence] Failed to log thinking event:', err);
352
+ }
353
+ }
354
+ let rawResponse = '';
355
+ try {
356
+ const response = await this.client.chat.completions.create({
357
+ model: this.model,
358
+ messages: [
359
+ { role: 'system', content: systemPrompt },
360
+ { role: 'user', content: cleanedContent || '[Empty email body]' },
361
+ ],
362
+ temperature: 0.1,
363
+ });
364
+ rawResponse = response.choices[0]?.message?.content || '';
365
+ console.log('[Intelligence] Context-aware response received (length:', rawResponse.length, ')');
366
+ // Parse JSON from response
367
+ let jsonStr = rawResponse.trim();
368
+ const startIdx = jsonStr.indexOf('{');
369
+ const endIdx = jsonStr.lastIndexOf('}');
370
+ if (startIdx === -1 || endIdx === -1) {
371
+ throw new Error('Response did not contain a valid JSON object');
372
+ }
373
+ jsonStr = jsonStr.substring(startIdx, endIdx + 1);
374
+ const parsed = JSON.parse(jsonStr);
375
+ const validated = ContextAwareAnalysisSchema.parse(parsed);
376
+ logger.debug('Context-aware analysis complete', {
377
+ matched_rule: validated.matched_rule.rule_name,
378
+ confidence: validated.matched_rule.confidence,
379
+ actions: validated.actions_to_execute,
380
+ });
381
+ if (eventLogger && emailId) {
382
+ await eventLogger.analysis('Decided', emailId, {
383
+ ...validated,
384
+ _raw_response: rawResponse
385
+ });
386
+ }
387
+ return validated;
388
+ }
389
+ catch (error) {
390
+ console.error('[Intelligence] Context-aware analysis failed:', error);
391
+ if (eventLogger) {
392
+ await eventLogger.error('Error', {
393
+ error: error instanceof Error ? error.message : String(error),
394
+ raw_response: rawResponse || 'No response received from LLM'
395
+ }, emailId);
396
+ }
397
+ return null;
398
+ }
399
+ }
213
400
  async testConnection() {
214
401
  if (!this.isReady()) {
215
402
  return { success: false, message: 'Intelligence service not initialized. Check your API Key.' };
@@ -454,7 +454,33 @@ export class EmailProcessorService {
454
454
  autoSubmitted: parsed.headers.get('auto-submitted')?.toString(),
455
455
  mailer: parsed.headers.get('x-mailer')?.toString()
456
456
  };
457
- // 3. Analyze with AI
457
+ // 3. Fetch account for action execution
458
+ const { data: account } = await this.supabase
459
+ .from('email_accounts')
460
+ .select('*')
461
+ .eq('id', email.account_id)
462
+ .single();
463
+ // 4. Fetch pre-compiled rule context (fast path - no loop/formatting)
464
+ // Falls back to building context if not cached
465
+ let compiledContext = settings?.compiled_rule_context || null;
466
+ // Fetch rules for action execution (need attachments, instructions)
467
+ const { data: rules } = await this.supabase
468
+ .from('rules')
469
+ .select('*')
470
+ .eq('user_id', userId)
471
+ .eq('is_enabled', true)
472
+ .order('priority', { ascending: false });
473
+ // Fallback: build context if not pre-compiled
474
+ if (!compiledContext && rules && rules.length > 0) {
475
+ compiledContext = rules.map((r, i) => `Rule ${i + 1} [ID: ${r.id}]\n` +
476
+ ` Name: ${r.name}\n` +
477
+ (r.description ? ` Description: ${r.description}\n` : '') +
478
+ (r.intent ? ` Intent: ${r.intent}\n` : '') +
479
+ ` Actions: ${r.actions?.join(', ') || r.action || 'none'}\n` +
480
+ (r.instructions ? ` Draft Instructions: ${r.instructions}\n` : '') +
481
+ '\n').join('');
482
+ }
483
+ // 5. Context-Aware Analysis: AI evaluates email against user's rules
458
484
  const intelligenceService = getIntelligenceService(settings?.llm_model || settings?.llm_base_url || settings?.llm_api_key
459
485
  ? {
460
486
  model: settings.llm_model,
@@ -462,7 +488,7 @@ export class EmailProcessorService {
462
488
  apiKey: settings.llm_api_key,
463
489
  }
464
490
  : undefined);
465
- const analysis = await intelligenceService.analyzeEmail(cleanContent, {
491
+ const analysis = await intelligenceService.analyzeEmailWithRules(cleanContent, {
466
492
  subject: email.subject || '',
467
493
  sender: email.sender || '',
468
494
  date: email.date || '',
@@ -471,39 +497,41 @@ export class EmailProcessorService {
471
497
  autoTrashSpam: settings?.auto_trash_spam,
472
498
  smartDrafts: settings?.smart_drafts,
473
499
  },
474
- }, eventLogger || undefined, email.id);
500
+ }, compiledContext || '', // Pre-compiled context (fast path)
501
+ eventLogger || undefined, email.id);
475
502
  if (!analysis) {
476
503
  throw new Error('AI analysis returned no result');
477
504
  }
478
- // 4. Update the email record with results
505
+ // 6. Update the email record with context-aware results
479
506
  await this.supabase
480
507
  .from('emails')
481
508
  .update({
482
509
  category: analysis.category,
483
- is_useless: analysis.is_useless,
484
510
  ai_analysis: analysis,
485
- suggested_actions: analysis.suggested_actions || [],
486
- suggested_action: analysis.suggested_actions?.[0] || 'none',
511
+ suggested_actions: analysis.actions_to_execute || [],
512
+ suggested_action: analysis.actions_to_execute?.[0] || 'none',
513
+ matched_rule_id: analysis.matched_rule.rule_id,
514
+ matched_rule_confidence: analysis.matched_rule.confidence,
487
515
  processing_status: 'completed'
488
516
  })
489
517
  .eq('id', email.id);
490
- // 5. Execute automation rules
491
- // Fetch account and rules needed for execution
492
- const { data: account } = await this.supabase
493
- .from('email_accounts')
494
- .select('*')
495
- .eq('id', email.account_id)
496
- .single();
497
- const { data: rules } = await this.supabase
498
- .from('rules')
499
- .select('*')
500
- .eq('user_id', userId)
501
- .eq('is_enabled', true);
502
- if (account && rules) {
503
- const tempResult = { processed: 0, deleted: 0, drafted: 0, errors: 0 };
504
- // Ensure email object for rules has the analysis fields merged in
505
- const emailForRules = { ...email, ...analysis };
506
- await this.executeRules(account, emailForRules, analysis, rules, settings, tempResult, eventLogger);
518
+ // 7. Execute actions if rule matched with sufficient confidence
519
+ if (account && analysis.matched_rule.rule_id && analysis.matched_rule.confidence >= 0.7) {
520
+ const matchedRule = rules?.find(r => r.id === analysis.matched_rule.rule_id);
521
+ if (eventLogger) {
522
+ await eventLogger.info('Rule Matched', `"${analysis.matched_rule.rule_name}" matched with ${(analysis.matched_rule.confidence * 100).toFixed(0)}% confidence`, { reasoning: analysis.matched_rule.reasoning }, email.id);
523
+ }
524
+ // Execute each action from the AI's decision
525
+ for (const action of analysis.actions_to_execute) {
526
+ if (action === 'none')
527
+ continue;
528
+ // Use AI-generated draft content if available
529
+ const draftContent = action === 'draft' ? analysis.draft_content : undefined;
530
+ await this.executeAction(account, email, action, draftContent, eventLogger, `Rule: ${matchedRule?.name || analysis.matched_rule.rule_name}`, matchedRule?.attachments);
531
+ }
532
+ }
533
+ else if (eventLogger && rules && rules.length > 0) {
534
+ await eventLogger.info('No Match', analysis.matched_rule.reasoning, { confidence: analysis.matched_rule.confidence }, email.id);
507
535
  }
508
536
  // Mark log as success
509
537
  if (log) {