@realtimex/email-automator 2.6.2 → 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.
- package/api/src/middleware/validation.ts +7 -0
- package/api/src/routes/rules.ts +7 -2
- package/api/src/services/intelligence.ts +227 -6
- package/api/src/services/processor.ts +88 -33
- package/api/src/services/supabase.ts +5 -2
- package/api/src/utils/contentCleaner.ts +80 -66
- package/dist/api/src/middleware/validation.js +7 -0
- package/dist/api/src/routes/rules.js +6 -2
- package/dist/api/src/services/intelligence.js +187 -0
- package/dist/api/src/services/processor.js +52 -24
- package/dist/api/src/utils/contentCleaner.js +74 -58
- package/dist/assets/index-6zjdTO-X.css +1 -0
- package/dist/assets/index-8DZIcZie.js +97 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/supabase/migrations/20260119000000_context_aware_rules.sql +44 -0
- package/supabase/migrations/20260119000001_compiled_rule_context.sql +128 -0
- package/dist/assets/index-BaETRzrd.js +0 -97
- package/dist/assets/index-Bi5YqX3d.css +0 -1
|
@@ -87,8 +87,12 @@ export const schemas = {
|
|
|
87
87
|
}),
|
|
88
88
|
|
|
89
89
|
// Rule schemas - supports both single action (legacy) and actions array
|
|
90
|
+
// Now includes description and intent for context-aware AI matching
|
|
90
91
|
createRule: z.object({
|
|
91
92
|
name: z.string().min(1).max(100),
|
|
93
|
+
description: z.string().max(500).optional(),
|
|
94
|
+
intent: z.string().max(200).optional(),
|
|
95
|
+
priority: z.number().int().min(0).max(100).optional(),
|
|
92
96
|
condition: z.record(z.unknown()),
|
|
93
97
|
action: z.enum(['delete', 'archive', 'draft', 'star', 'read']).optional(),
|
|
94
98
|
actions: z.array(z.enum(['delete', 'archive', 'draft', 'star', 'read'])).optional(),
|
|
@@ -100,6 +104,9 @@ export const schemas = {
|
|
|
100
104
|
|
|
101
105
|
updateRule: z.object({
|
|
102
106
|
name: z.string().min(1).max(100).optional(),
|
|
107
|
+
description: z.string().max(500).optional(),
|
|
108
|
+
intent: z.string().max(200).optional(),
|
|
109
|
+
priority: z.number().int().min(0).max(100).optional(),
|
|
103
110
|
condition: z.record(z.unknown()).optional(),
|
|
104
111
|
action: z.enum(['delete', 'archive', 'draft', 'star', 'read']).optional(),
|
|
105
112
|
actions: z.array(z.enum(['delete', 'archive', 'draft', 'star', 'read'])).optional(),
|
package/api/src/routes/rules.ts
CHANGED
|
@@ -68,7 +68,12 @@ router.patch('/:ruleId',
|
|
|
68
68
|
validateBody(schemas.updateRule),
|
|
69
69
|
asyncHandler(async (req, res) => {
|
|
70
70
|
const { ruleId } = req.params;
|
|
71
|
-
const updates = req.body;
|
|
71
|
+
const updates = { ...req.body };
|
|
72
|
+
|
|
73
|
+
// Ensure legacy action is in sync if actions array is provided
|
|
74
|
+
if (updates.actions && Array.isArray(updates.actions) && updates.actions.length > 0) {
|
|
75
|
+
updates.action = updates.actions[0];
|
|
76
|
+
}
|
|
72
77
|
|
|
73
78
|
const { data, error } = await req.supabase!
|
|
74
79
|
.from('rules')
|
|
@@ -81,7 +86,7 @@ router.patch('/:ruleId',
|
|
|
81
86
|
if (error) throw error;
|
|
82
87
|
if (!data) throw new NotFoundError('Rule');
|
|
83
88
|
|
|
84
|
-
logger.info('Rule updated', { ruleId, userId: req.user!.id });
|
|
89
|
+
logger.info('Rule updated', { ruleId, actions: data.actions, userId: req.user!.id });
|
|
85
90
|
|
|
86
91
|
res.json({ rule: data });
|
|
87
92
|
})
|
|
@@ -30,6 +30,44 @@ export const EmailAnalysisSchema = z.object({
|
|
|
30
30
|
|
|
31
31
|
export type EmailAnalysis = z.infer<typeof EmailAnalysisSchema>;
|
|
32
32
|
|
|
33
|
+
// Context-Aware Analysis Schema - AI evaluates email against user's rules
|
|
34
|
+
export const ContextAwareAnalysisSchema = z.object({
|
|
35
|
+
// Classification (kept for UI/logging)
|
|
36
|
+
summary: z.string().describe('A brief summary of the email content'),
|
|
37
|
+
category: z.enum(['spam', 'newsletter', 'promotional', 'transactional', 'social', 'support', 'client', 'internal', 'personal', 'other'])
|
|
38
|
+
.describe('The category of the email'),
|
|
39
|
+
priority: z.enum(['High', 'Medium', 'Low'])
|
|
40
|
+
.describe('The urgency of the email'),
|
|
41
|
+
|
|
42
|
+
// Rule Matching (core of context-aware engine)
|
|
43
|
+
matched_rule: z.object({
|
|
44
|
+
rule_id: z.string().nullable().describe('ID of the matched rule, or null if no match'),
|
|
45
|
+
rule_name: z.string().nullable().describe('Name of the matched rule'),
|
|
46
|
+
confidence: z.number().min(0).max(1).describe('Confidence score for the match (0-1)'),
|
|
47
|
+
reasoning: z.string().describe('Explanation of why this rule was matched or why no rule matched'),
|
|
48
|
+
}),
|
|
49
|
+
|
|
50
|
+
// Actions to execute (derived from matched rule)
|
|
51
|
+
actions_to_execute: z.array(z.enum(['none', 'delete', 'archive', 'draft', 'read', 'star']))
|
|
52
|
+
.describe('Actions to execute based on the matched rule'),
|
|
53
|
+
|
|
54
|
+
// Intent-aware draft content (if draft action is included)
|
|
55
|
+
draft_content: z.string().optional()
|
|
56
|
+
.describe('Generated draft reply if the action includes drafting'),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export type ContextAwareAnalysis = z.infer<typeof ContextAwareAnalysisSchema>;
|
|
60
|
+
|
|
61
|
+
// Rule context for AI matching
|
|
62
|
+
export interface RuleContext {
|
|
63
|
+
id: string;
|
|
64
|
+
name: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
intent?: string;
|
|
67
|
+
actions: string[];
|
|
68
|
+
draft_instructions?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
33
71
|
export interface EmailContext {
|
|
34
72
|
subject: string;
|
|
35
73
|
sender: string;
|
|
@@ -87,7 +125,7 @@ export class IntelligenceService {
|
|
|
87
125
|
|
|
88
126
|
async analyzeEmail(content: string, context: EmailContext, eventLogger?: EventLogger, emailId?: string): Promise<EmailAnalysis | null> {
|
|
89
127
|
console.log('[Intelligence] analyzeEmail called for:', context.subject);
|
|
90
|
-
|
|
128
|
+
|
|
91
129
|
if (!this.isReady()) {
|
|
92
130
|
console.log('[Intelligence] Not ready, skipping');
|
|
93
131
|
logger.warn('Intelligence service not ready, skipping analysis');
|
|
@@ -99,7 +137,7 @@ export class IntelligenceService {
|
|
|
99
137
|
|
|
100
138
|
// 1. Prepare Content and Signals
|
|
101
139
|
const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
|
|
102
|
-
|
|
140
|
+
|
|
103
141
|
const metadataSignals = [];
|
|
104
142
|
if (context.metadata?.listUnsubscribe) metadataSignals.push('- Contains Unsubscribe header (High signal for Newsletter/Promo)');
|
|
105
143
|
if (context.metadata?.autoSubmitted && context.metadata.autoSubmitted !== 'no') metadataSignals.push(`- Auto-Submitted: ${context.metadata.autoSubmitted}`);
|
|
@@ -148,7 +186,7 @@ REQUIRED JSON STRUCTURE:
|
|
|
148
186
|
if (eventLogger) {
|
|
149
187
|
console.log('[Intelligence] Logging "Thinking" event');
|
|
150
188
|
try {
|
|
151
|
-
await eventLogger.info('Thinking', `Analyzing email: ${context.subject}`, {
|
|
189
|
+
await eventLogger.info('Thinking', `Analyzing email: ${context.subject}`, {
|
|
152
190
|
model: this.model,
|
|
153
191
|
system_prompt: systemPrompt,
|
|
154
192
|
content_preview: cleanedContent,
|
|
@@ -174,12 +212,12 @@ REQUIRED JSON STRUCTURE:
|
|
|
174
212
|
|
|
175
213
|
rawResponse = response.choices[0]?.message?.content || '';
|
|
176
214
|
console.log('[Intelligence] Raw LLM Response received (length:', rawResponse.length, ')');
|
|
177
|
-
|
|
215
|
+
|
|
178
216
|
// Clean the response: Find first '{' and last '}'
|
|
179
217
|
let jsonStr = rawResponse.trim();
|
|
180
218
|
const startIdx = jsonStr.indexOf('{');
|
|
181
219
|
const endIdx = jsonStr.lastIndexOf('}');
|
|
182
|
-
|
|
220
|
+
|
|
183
221
|
if (startIdx === -1 || endIdx === -1) {
|
|
184
222
|
throw new Error('Response did not contain a valid JSON object (missing curly braces)');
|
|
185
223
|
}
|
|
@@ -197,7 +235,7 @@ REQUIRED JSON STRUCTURE:
|
|
|
197
235
|
if (eventLogger && emailId) {
|
|
198
236
|
await eventLogger.analysis('Decided', emailId, {
|
|
199
237
|
...validated,
|
|
200
|
-
_raw_response: rawResponse
|
|
238
|
+
_raw_response: rawResponse
|
|
201
239
|
});
|
|
202
240
|
}
|
|
203
241
|
|
|
@@ -251,6 +289,189 @@ Please write a reply.`,
|
|
|
251
289
|
}
|
|
252
290
|
}
|
|
253
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Context-Aware Analysis: AI evaluates email against user's rules semantically
|
|
294
|
+
* This is the core of the new automation engine
|
|
295
|
+
*
|
|
296
|
+
* @param compiledRulesContext - Pre-compiled rules context string (from user_settings.compiled_rule_context)
|
|
297
|
+
* OR RuleContext[] for backwards compatibility
|
|
298
|
+
*/
|
|
299
|
+
async analyzeEmailWithRules(
|
|
300
|
+
content: string,
|
|
301
|
+
context: EmailContext,
|
|
302
|
+
compiledRulesContext: string | RuleContext[],
|
|
303
|
+
eventLogger?: EventLogger,
|
|
304
|
+
emailId?: string
|
|
305
|
+
): Promise<ContextAwareAnalysis | null> {
|
|
306
|
+
console.log('[Intelligence] analyzeEmailWithRules called for:', context.subject);
|
|
307
|
+
|
|
308
|
+
if (!this.isReady()) {
|
|
309
|
+
console.log('[Intelligence] Not ready, skipping');
|
|
310
|
+
logger.warn('Intelligence service not ready, skipping analysis');
|
|
311
|
+
if (eventLogger) {
|
|
312
|
+
await eventLogger.info('Skipped', 'AI Analysis skipped: Model not configured.', undefined, emailId);
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Prepare content
|
|
318
|
+
const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
|
|
319
|
+
|
|
320
|
+
// Use pre-compiled context if string, otherwise build from RuleContext[] (backwards compat)
|
|
321
|
+
let rulesContext: string;
|
|
322
|
+
let rulesCount: number;
|
|
323
|
+
|
|
324
|
+
if (typeof compiledRulesContext === 'string') {
|
|
325
|
+
// Fast path: use pre-compiled context
|
|
326
|
+
rulesContext = compiledRulesContext || '\n[No rules defined - analyze email but take no actions]\n';
|
|
327
|
+
rulesCount = (rulesContext.match(/Rule \d+/g) || []).length;
|
|
328
|
+
} else {
|
|
329
|
+
// Backwards compatibility: build from RuleContext[]
|
|
330
|
+
const rules = compiledRulesContext;
|
|
331
|
+
rulesCount = rules.length;
|
|
332
|
+
rulesContext = rules.length > 0
|
|
333
|
+
? rules.map((r, i) => `
|
|
334
|
+
### Rule ${i + 1}: "${r.name}" (ID: ${r.id})
|
|
335
|
+
- Description: ${r.description || 'No description provided'}
|
|
336
|
+
- Intent: ${r.intent || 'General automation'}
|
|
337
|
+
- Actions: ${r.actions.join(', ')}
|
|
338
|
+
${r.draft_instructions ? `- Draft Instructions: "${r.draft_instructions}"` : ''}
|
|
339
|
+
`).join('\n')
|
|
340
|
+
: '\n[No rules defined - analyze email but take no actions]\n';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const systemPrompt = `You are an AI Email Automation Agent.
|
|
344
|
+
|
|
345
|
+
## Your Operating Rules
|
|
346
|
+
The user has defined the following automation rules. Your job is to:
|
|
347
|
+
1. Analyze the incoming email
|
|
348
|
+
2. Determine if ANY rule semantically matches this email's context
|
|
349
|
+
3. Match based on INTENT, not just keywords
|
|
350
|
+
|
|
351
|
+
${rulesContext}
|
|
352
|
+
|
|
353
|
+
## Category Definitions (choose the most accurate)
|
|
354
|
+
- **client**: Business inquiries, RFPs, quote requests, project discussions, potential customers reaching out
|
|
355
|
+
- **support**: Help requests, bug reports, technical questions from existing users
|
|
356
|
+
- **internal**: Messages from colleagues, team communications
|
|
357
|
+
- **transactional**: Receipts, confirmations, shipping updates, account notifications
|
|
358
|
+
- **newsletter**: Subscribed content, digests, updates from services you signed up for
|
|
359
|
+
- **promotional**: UNSOLICITED marketing, cold sales pitches, ads - NOT legitimate business inquiries
|
|
360
|
+
- **spam**: Scams, phishing, junk mail
|
|
361
|
+
- **social**: Social media notifications, friend requests
|
|
362
|
+
- **personal**: Friends, family, personal matters
|
|
363
|
+
- **other**: Anything that doesn't fit above
|
|
364
|
+
|
|
365
|
+
## Matching Guidelines
|
|
366
|
+
- A "decline sales" rule should match ANY sales pitch, not just ones with "sales" in the subject
|
|
367
|
+
- Match the rule that best fits the USER'S INTENT
|
|
368
|
+
- Only match if you are confident (>= 0.7 confidence)
|
|
369
|
+
- If no rule clearly matches, return null for rule_id
|
|
370
|
+
- If a matched rule includes "draft" action, generate an appropriate draft using the rule's intent
|
|
371
|
+
|
|
372
|
+
## CRITICAL: Distinguish Between Inbound vs Outbound
|
|
373
|
+
**INBOUND (Client Inquiries - NOT promotional):**
|
|
374
|
+
- User is RECEIVING a request for quote/proposal/service
|
|
375
|
+
- Examples: "Please send me a quote", "RFP: [project]", "Can you provide pricing", "I need a quote asap"
|
|
376
|
+
- Category: client, support, or transactional (NEVER promotional)
|
|
377
|
+
|
|
378
|
+
**OUTBOUND (Sales/Marketing - IS promotional):**
|
|
379
|
+
- User is RECEIVING a sales pitch or marketing message
|
|
380
|
+
- Examples: "Get a FREE quote today!", "Limited offer", "Don't miss out", "Special discount"
|
|
381
|
+
- Category: promotional, spam, or newsletter
|
|
382
|
+
|
|
383
|
+
**Key Distinction:** If someone is ASKING the user for something (quote, proposal, service), it's a CLIENT INQUIRY, not promotional content.
|
|
384
|
+
|
|
385
|
+
## Email Context
|
|
386
|
+
- Current Date: ${new Date().toISOString()}
|
|
387
|
+
- Subject: ${context.subject}
|
|
388
|
+
- From: ${context.sender}
|
|
389
|
+
- Date: ${context.date}
|
|
390
|
+
|
|
391
|
+
## Required JSON Response
|
|
392
|
+
{
|
|
393
|
+
"summary": "Brief summary of the email",
|
|
394
|
+
"category": "spam|newsletter|promotional|transactional|social|support|client|internal|personal|other",
|
|
395
|
+
"priority": "High|Medium|Low",
|
|
396
|
+
"matched_rule": {
|
|
397
|
+
"rule_id": "UUID or null",
|
|
398
|
+
"rule_name": "Rule name or null",
|
|
399
|
+
"confidence": 0.0-1.0,
|
|
400
|
+
"reasoning": "Why this rule was or wasn't matched"
|
|
401
|
+
},
|
|
402
|
+
"actions_to_execute": ["none"] or ["archive", "read", etc.],
|
|
403
|
+
"draft_content": "Optional: draft reply if action includes 'draft'"
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
Return ONLY valid JSON.`;
|
|
407
|
+
|
|
408
|
+
// Log thinking phase
|
|
409
|
+
if (eventLogger) {
|
|
410
|
+
try {
|
|
411
|
+
await eventLogger.info('Thinking', `Context-aware analysis: ${context.subject}`, {
|
|
412
|
+
model: this.model,
|
|
413
|
+
system_prompt: systemPrompt,
|
|
414
|
+
content_preview: cleanedContent,
|
|
415
|
+
rules_count: rulesCount,
|
|
416
|
+
}, emailId);
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.error('[Intelligence] Failed to log thinking event:', err);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let rawResponse = '';
|
|
423
|
+
try {
|
|
424
|
+
const response = await this.client!.chat.completions.create({
|
|
425
|
+
model: this.model,
|
|
426
|
+
messages: [
|
|
427
|
+
{ role: 'system', content: systemPrompt },
|
|
428
|
+
{ role: 'user', content: cleanedContent || '[Empty email body]' },
|
|
429
|
+
],
|
|
430
|
+
temperature: 0.1,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
rawResponse = response.choices[0]?.message?.content || '';
|
|
434
|
+
console.log('[Intelligence] Context-aware response received (length:', rawResponse.length, ')');
|
|
435
|
+
|
|
436
|
+
// Parse JSON from response
|
|
437
|
+
let jsonStr = rawResponse.trim();
|
|
438
|
+
const startIdx = jsonStr.indexOf('{');
|
|
439
|
+
const endIdx = jsonStr.lastIndexOf('}');
|
|
440
|
+
|
|
441
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
442
|
+
throw new Error('Response did not contain a valid JSON object');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
jsonStr = jsonStr.substring(startIdx, endIdx + 1);
|
|
446
|
+
const parsed = JSON.parse(jsonStr);
|
|
447
|
+
const validated = ContextAwareAnalysisSchema.parse(parsed);
|
|
448
|
+
|
|
449
|
+
logger.debug('Context-aware analysis complete', {
|
|
450
|
+
matched_rule: validated.matched_rule.rule_name,
|
|
451
|
+
confidence: validated.matched_rule.confidence,
|
|
452
|
+
actions: validated.actions_to_execute,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
if (eventLogger && emailId) {
|
|
456
|
+
await eventLogger.analysis('Decided', emailId, {
|
|
457
|
+
...validated,
|
|
458
|
+
_raw_response: rawResponse
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return validated;
|
|
463
|
+
} catch (error) {
|
|
464
|
+
console.error('[Intelligence] Context-aware analysis failed:', error);
|
|
465
|
+
if (eventLogger) {
|
|
466
|
+
await eventLogger.error('Error', {
|
|
467
|
+
error: error instanceof Error ? error.message : String(error),
|
|
468
|
+
raw_response: rawResponse || 'No response received from LLM'
|
|
469
|
+
}, emailId);
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
254
475
|
async testConnection(): Promise<{ success: boolean; message: string }> {
|
|
255
476
|
if (!this.isReady()) {
|
|
256
477
|
return { success: false, message: 'Intelligence service not initialized. Check your API Key.' };
|
|
@@ -4,7 +4,7 @@ import { createLogger } from '../utils/logger.js';
|
|
|
4
4
|
import { config } from '../config/index.js';
|
|
5
5
|
import { getGmailService, GmailMessage } from './gmail.js';
|
|
6
6
|
import { getMicrosoftService, OutlookMessage } from './microsoft.js';
|
|
7
|
-
import { getIntelligenceService, EmailAnalysis } from './intelligence.js';
|
|
7
|
+
import { getIntelligenceService, EmailAnalysis, ContextAwareAnalysis, RuleContext } from './intelligence.js';
|
|
8
8
|
import { getStorageService } from './storage.js';
|
|
9
9
|
import { EmailAccount, Email, Rule, ProcessingLog } from './supabase.js';
|
|
10
10
|
import { EventLogger } from './eventLogger.js';
|
|
@@ -551,7 +551,39 @@ export class EmailProcessorService {
|
|
|
551
551
|
mailer: parsed.headers.get('x-mailer')?.toString()
|
|
552
552
|
};
|
|
553
553
|
|
|
554
|
-
// 3.
|
|
554
|
+
// 3. Fetch account for action execution
|
|
555
|
+
const { data: account } = await this.supabase
|
|
556
|
+
.from('email_accounts')
|
|
557
|
+
.select('*')
|
|
558
|
+
.eq('id', email.account_id)
|
|
559
|
+
.single();
|
|
560
|
+
|
|
561
|
+
// 4. Fetch pre-compiled rule context (fast path - no loop/formatting)
|
|
562
|
+
// Falls back to building context if not cached
|
|
563
|
+
let compiledContext: string | null = settings?.compiled_rule_context || null;
|
|
564
|
+
|
|
565
|
+
// Fetch rules for action execution (need attachments, instructions)
|
|
566
|
+
const { data: rules } = await this.supabase
|
|
567
|
+
.from('rules')
|
|
568
|
+
.select('*')
|
|
569
|
+
.eq('user_id', userId)
|
|
570
|
+
.eq('is_enabled', true)
|
|
571
|
+
.order('priority', { ascending: false });
|
|
572
|
+
|
|
573
|
+
// Fallback: build context if not pre-compiled
|
|
574
|
+
if (!compiledContext && rules && rules.length > 0) {
|
|
575
|
+
compiledContext = rules.map((r, i) =>
|
|
576
|
+
`Rule ${i + 1} [ID: ${r.id}]\n` +
|
|
577
|
+
` Name: ${r.name}\n` +
|
|
578
|
+
(r.description ? ` Description: ${r.description}\n` : '') +
|
|
579
|
+
(r.intent ? ` Intent: ${r.intent}\n` : '') +
|
|
580
|
+
` Actions: ${r.actions?.join(', ') || r.action || 'none'}\n` +
|
|
581
|
+
(r.instructions ? ` Draft Instructions: ${r.instructions}\n` : '') +
|
|
582
|
+
'\n'
|
|
583
|
+
).join('');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// 5. Context-Aware Analysis: AI evaluates email against user's rules
|
|
555
587
|
const intelligenceService = getIntelligenceService(
|
|
556
588
|
settings?.llm_model || settings?.llm_base_url || settings?.llm_api_key
|
|
557
589
|
? {
|
|
@@ -562,53 +594,76 @@ export class EmailProcessorService {
|
|
|
562
594
|
: undefined
|
|
563
595
|
);
|
|
564
596
|
|
|
565
|
-
const analysis = await intelligenceService.
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
597
|
+
const analysis = await intelligenceService.analyzeEmailWithRules(
|
|
598
|
+
cleanContent,
|
|
599
|
+
{
|
|
600
|
+
subject: email.subject || '',
|
|
601
|
+
sender: email.sender || '',
|
|
602
|
+
date: email.date || '',
|
|
603
|
+
metadata,
|
|
604
|
+
userPreferences: {
|
|
605
|
+
autoTrashSpam: settings?.auto_trash_spam,
|
|
606
|
+
smartDrafts: settings?.smart_drafts,
|
|
607
|
+
},
|
|
573
608
|
},
|
|
574
|
-
|
|
609
|
+
compiledContext || '', // Pre-compiled context (fast path)
|
|
610
|
+
eventLogger || undefined,
|
|
611
|
+
email.id
|
|
612
|
+
);
|
|
575
613
|
|
|
576
614
|
if (!analysis) {
|
|
577
615
|
throw new Error('AI analysis returned no result');
|
|
578
616
|
}
|
|
579
617
|
|
|
580
|
-
//
|
|
618
|
+
// 6. Update the email record with context-aware results
|
|
581
619
|
await this.supabase
|
|
582
620
|
.from('emails')
|
|
583
621
|
.update({
|
|
584
622
|
category: analysis.category,
|
|
585
|
-
is_useless: analysis.is_useless,
|
|
586
623
|
ai_analysis: analysis as any,
|
|
587
|
-
suggested_actions: analysis.
|
|
588
|
-
suggested_action: analysis.
|
|
624
|
+
suggested_actions: analysis.actions_to_execute || [],
|
|
625
|
+
suggested_action: analysis.actions_to_execute?.[0] || 'none',
|
|
626
|
+
matched_rule_id: analysis.matched_rule.rule_id,
|
|
627
|
+
matched_rule_confidence: analysis.matched_rule.confidence,
|
|
589
628
|
processing_status: 'completed'
|
|
590
629
|
})
|
|
591
630
|
.eq('id', email.id);
|
|
592
631
|
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
.eq('user_id', userId)
|
|
605
|
-
.eq('is_enabled', true);
|
|
632
|
+
// 7. Execute actions if rule matched with sufficient confidence
|
|
633
|
+
if (account && analysis.matched_rule.rule_id && analysis.matched_rule.confidence >= 0.7) {
|
|
634
|
+
const matchedRule = rules?.find(r => r.id === analysis.matched_rule.rule_id);
|
|
635
|
+
|
|
636
|
+
if (eventLogger) {
|
|
637
|
+
await eventLogger.info('Rule Matched',
|
|
638
|
+
`"${analysis.matched_rule.rule_name}" matched with ${(analysis.matched_rule.confidence * 100).toFixed(0)}% confidence`,
|
|
639
|
+
{ reasoning: analysis.matched_rule.reasoning },
|
|
640
|
+
email.id
|
|
641
|
+
);
|
|
642
|
+
}
|
|
606
643
|
|
|
607
|
-
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
644
|
+
// Execute each action from the AI's decision
|
|
645
|
+
for (const action of analysis.actions_to_execute) {
|
|
646
|
+
if (action === 'none') continue;
|
|
647
|
+
|
|
648
|
+
// Use AI-generated draft content if available
|
|
649
|
+
const draftContent = action === 'draft' ? analysis.draft_content : undefined;
|
|
650
|
+
|
|
651
|
+
await this.executeAction(
|
|
652
|
+
account,
|
|
653
|
+
email,
|
|
654
|
+
action as any,
|
|
655
|
+
draftContent,
|
|
656
|
+
eventLogger,
|
|
657
|
+
`Rule: ${matchedRule?.name || analysis.matched_rule.rule_name}`,
|
|
658
|
+
matchedRule?.attachments
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
} else if (eventLogger && rules && rules.length > 0) {
|
|
662
|
+
await eventLogger.info('No Match',
|
|
663
|
+
analysis.matched_rule.reasoning,
|
|
664
|
+
{ confidence: analysis.matched_rule.confidence },
|
|
665
|
+
email.id
|
|
666
|
+
);
|
|
612
667
|
}
|
|
613
668
|
|
|
614
669
|
// Mark log as success
|
|
@@ -115,10 +115,13 @@ export interface Rule {
|
|
|
115
115
|
id: string;
|
|
116
116
|
user_id: string;
|
|
117
117
|
name: string;
|
|
118
|
-
|
|
118
|
+
description?: string; // Semantic context for AI matching
|
|
119
|
+
intent?: string; // The intent behind the rule (e.g., "Politely decline sales pitches")
|
|
120
|
+
priority?: number; // Higher = evaluated first by AI
|
|
121
|
+
condition: Record<string, unknown>; // Legacy - kept for backwards compatibility
|
|
119
122
|
action?: 'delete' | 'archive' | 'draft' | 'star' | 'read'; // Legacy single action
|
|
120
123
|
actions?: ('delete' | 'archive' | 'draft' | 'star' | 'read')[]; // New multi-action array
|
|
121
|
-
instructions?: string;
|
|
124
|
+
instructions?: string; // Draft generation instructions
|
|
122
125
|
attachments?: any[];
|
|
123
126
|
is_enabled: boolean;
|
|
124
127
|
created_at: string;
|