@realtimex/email-automator 2.6.4 → 2.7.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/middleware/validation.ts +7 -0
- package/api/src/services/intelligence.ts +232 -7
- package/api/src/services/processor.ts +153 -69
- 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/services/intelligence.js +193 -2
- package/dist/api/src/services/processor.js +85 -24
- package/dist/api/src/utils/contentCleaner.js +74 -58
- package/dist/assets/index-aTk6SbAd.js +97 -0
- package/dist/assets/index-npWWfPF9.css +1 -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/supabase/migrations/20260119000002_fix_compiled_context_conditions.sql +137 -0
- package/dist/assets/index-BSHZ3lFn.js +0 -97
- package/dist/assets/index-CRQKk5IW.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(),
|
|
@@ -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,
|
|
@@ -173,13 +211,14 @@ REQUIRED JSON STRUCTURE:
|
|
|
173
211
|
});
|
|
174
212
|
|
|
175
213
|
rawResponse = response.choices[0]?.message?.content || '';
|
|
176
|
-
|
|
177
|
-
|
|
214
|
+
const usage = response.usage;
|
|
215
|
+
console.log('[Intelligence] Raw LLM Response received (length:', rawResponse.length, ')', { usage });
|
|
216
|
+
|
|
178
217
|
// Clean the response: Find first '{' and last '}'
|
|
179
218
|
let jsonStr = rawResponse.trim();
|
|
180
219
|
const startIdx = jsonStr.indexOf('{');
|
|
181
220
|
const endIdx = jsonStr.lastIndexOf('}');
|
|
182
|
-
|
|
221
|
+
|
|
183
222
|
if (startIdx === -1 || endIdx === -1) {
|
|
184
223
|
throw new Error('Response did not contain a valid JSON object (missing curly braces)');
|
|
185
224
|
}
|
|
@@ -197,7 +236,8 @@ REQUIRED JSON STRUCTURE:
|
|
|
197
236
|
if (eventLogger && emailId) {
|
|
198
237
|
await eventLogger.analysis('Decided', emailId, {
|
|
199
238
|
...validated,
|
|
200
|
-
_raw_response: rawResponse
|
|
239
|
+
_raw_response: rawResponse,
|
|
240
|
+
usage: usage // Include token usage
|
|
201
241
|
});
|
|
202
242
|
}
|
|
203
243
|
|
|
@@ -251,6 +291,191 @@ Please write a reply.`,
|
|
|
251
291
|
}
|
|
252
292
|
}
|
|
253
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Context-Aware Analysis: AI evaluates email against user's rules semantically
|
|
296
|
+
* This is the core of the new automation engine
|
|
297
|
+
*
|
|
298
|
+
* @param compiledRulesContext - Pre-compiled rules context string (from user_settings.compiled_rule_context)
|
|
299
|
+
* OR RuleContext[] for backwards compatibility
|
|
300
|
+
*/
|
|
301
|
+
async analyzeEmailWithRules(
|
|
302
|
+
content: string,
|
|
303
|
+
context: EmailContext,
|
|
304
|
+
compiledRulesContext: string | RuleContext[],
|
|
305
|
+
eventLogger?: EventLogger,
|
|
306
|
+
emailId?: string
|
|
307
|
+
): Promise<ContextAwareAnalysis | null> {
|
|
308
|
+
console.log('[Intelligence] analyzeEmailWithRules called for:', context.subject);
|
|
309
|
+
|
|
310
|
+
if (!this.isReady()) {
|
|
311
|
+
console.log('[Intelligence] Not ready, skipping');
|
|
312
|
+
logger.warn('Intelligence service not ready, skipping analysis');
|
|
313
|
+
if (eventLogger) {
|
|
314
|
+
await eventLogger.info('Skipped', 'AI Analysis skipped: Model not configured.', undefined, emailId);
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Prepare content
|
|
320
|
+
const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
|
|
321
|
+
|
|
322
|
+
// Use pre-compiled context if string, otherwise build from RuleContext[] (backwards compat)
|
|
323
|
+
let rulesContext: string;
|
|
324
|
+
let rulesCount: number;
|
|
325
|
+
|
|
326
|
+
if (typeof compiledRulesContext === 'string') {
|
|
327
|
+
// Fast path: use pre-compiled context
|
|
328
|
+
rulesContext = compiledRulesContext || '\n[No rules defined - analyze email but take no actions]\n';
|
|
329
|
+
rulesCount = (rulesContext.match(/Rule \d+/g) || []).length;
|
|
330
|
+
} else {
|
|
331
|
+
// Backwards compatibility: build from RuleContext[]
|
|
332
|
+
const rules = compiledRulesContext;
|
|
333
|
+
rulesCount = rules.length;
|
|
334
|
+
rulesContext = rules.length > 0
|
|
335
|
+
? rules.map((r, i) => `
|
|
336
|
+
### Rule ${i + 1}: "${r.name}" (ID: ${r.id})
|
|
337
|
+
- Description: ${r.description || 'No description provided'}
|
|
338
|
+
- Intent: ${r.intent || 'General automation'}
|
|
339
|
+
- Actions: ${r.actions.join(', ')}
|
|
340
|
+
${r.draft_instructions ? `- Draft Instructions: "${r.draft_instructions}"` : ''}
|
|
341
|
+
`).join('\n')
|
|
342
|
+
: '\n[No rules defined - analyze email but take no actions]\n';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const systemPrompt = `You are an AI Email Automation Agent.
|
|
346
|
+
|
|
347
|
+
## Your Operating Rules
|
|
348
|
+
The user has defined the following automation rules. Your job is to:
|
|
349
|
+
1. Analyze the incoming email
|
|
350
|
+
2. Determine if ANY rule semantically matches this email's context
|
|
351
|
+
3. Match based on INTENT, not just keywords
|
|
352
|
+
|
|
353
|
+
${rulesContext}
|
|
354
|
+
|
|
355
|
+
## Category Definitions (choose the most accurate)
|
|
356
|
+
- **client**: Business inquiries, RFPs, quote requests, project discussions, potential customers reaching out
|
|
357
|
+
- **support**: Help requests, bug reports, technical questions from existing users
|
|
358
|
+
- **internal**: Messages from colleagues, team communications
|
|
359
|
+
- **transactional**: Receipts, confirmations, shipping updates, account notifications
|
|
360
|
+
- **newsletter**: Subscribed content, digests, updates from services you signed up for
|
|
361
|
+
- **promotional**: UNSOLICITED marketing, cold sales pitches, ads - NOT legitimate business inquiries
|
|
362
|
+
- **spam**: Scams, phishing, junk mail
|
|
363
|
+
- **social**: Social media notifications, friend requests
|
|
364
|
+
- **personal**: Friends, family, personal matters
|
|
365
|
+
- **other**: Anything that doesn't fit above
|
|
366
|
+
|
|
367
|
+
## Matching Guidelines
|
|
368
|
+
- A "decline sales" rule should match ANY sales pitch, not just ones with "sales" in the subject
|
|
369
|
+
- Match the rule that best fits the USER'S INTENT
|
|
370
|
+
- Only match if you are confident (>= 0.7 confidence)
|
|
371
|
+
- If no rule clearly matches, return null for rule_id
|
|
372
|
+
- If a matched rule includes "draft" action, generate an appropriate draft using the rule's intent
|
|
373
|
+
|
|
374
|
+
## CRITICAL: Distinguish Between Inbound vs Outbound
|
|
375
|
+
**INBOUND (Client Inquiries - NOT promotional):**
|
|
376
|
+
- User is RECEIVING a request for quote/proposal/service
|
|
377
|
+
- Examples: "Please send me a quote", "RFP: [project]", "Can you provide pricing", "I need a quote asap"
|
|
378
|
+
- Category: client, support, or transactional (NEVER promotional)
|
|
379
|
+
|
|
380
|
+
**OUTBOUND (Sales/Marketing - IS promotional):**
|
|
381
|
+
- User is RECEIVING a sales pitch or marketing message
|
|
382
|
+
- Examples: "Get a FREE quote today!", "Limited offer", "Don't miss out", "Special discount"
|
|
383
|
+
- Category: promotional, spam, or newsletter
|
|
384
|
+
|
|
385
|
+
**Key Distinction:** If someone is ASKING the user for something (quote, proposal, service), it's a CLIENT INQUIRY, not promotional content.
|
|
386
|
+
|
|
387
|
+
## Email Context
|
|
388
|
+
- Current Date: ${new Date().toISOString()}
|
|
389
|
+
- Subject: ${context.subject}
|
|
390
|
+
- From: ${context.sender}
|
|
391
|
+
- Date: ${context.date}
|
|
392
|
+
|
|
393
|
+
## Required JSON Response
|
|
394
|
+
{
|
|
395
|
+
"summary": "Brief summary of the email",
|
|
396
|
+
"category": "spam|newsletter|promotional|transactional|social|support|client|internal|personal|other",
|
|
397
|
+
"priority": "High|Medium|Low",
|
|
398
|
+
"matched_rule": {
|
|
399
|
+
"rule_id": "UUID or null",
|
|
400
|
+
"rule_name": "Rule name or null",
|
|
401
|
+
"confidence": 0.0-1.0,
|
|
402
|
+
"reasoning": "Why this rule was or wasn't matched"
|
|
403
|
+
},
|
|
404
|
+
"actions_to_execute": ["none"] or ["archive", "read", etc.],
|
|
405
|
+
"draft_content": "Optional: draft reply if action includes 'draft'"
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
Return ONLY valid JSON.`;
|
|
409
|
+
|
|
410
|
+
// Log thinking phase
|
|
411
|
+
if (eventLogger) {
|
|
412
|
+
try {
|
|
413
|
+
await eventLogger.info('Thinking', `Context-aware analysis: ${context.subject}`, {
|
|
414
|
+
model: this.model,
|
|
415
|
+
system_prompt: systemPrompt,
|
|
416
|
+
content_preview: cleanedContent,
|
|
417
|
+
rules_count: rulesCount,
|
|
418
|
+
}, emailId);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
console.error('[Intelligence] Failed to log thinking event:', err);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
let rawResponse = '';
|
|
425
|
+
try {
|
|
426
|
+
const response = await this.client!.chat.completions.create({
|
|
427
|
+
model: this.model,
|
|
428
|
+
messages: [
|
|
429
|
+
{ role: 'system', content: systemPrompt },
|
|
430
|
+
{ role: 'user', content: cleanedContent || '[Empty email body]' },
|
|
431
|
+
],
|
|
432
|
+
temperature: 0.1,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
rawResponse = response.choices[0]?.message?.content || '';
|
|
436
|
+
const usage = response.usage;
|
|
437
|
+
console.log('[Intelligence] Context-aware response received (length:', rawResponse.length, ')', { usage });
|
|
438
|
+
|
|
439
|
+
// Parse JSON from response
|
|
440
|
+
let jsonStr = rawResponse.trim();
|
|
441
|
+
const startIdx = jsonStr.indexOf('{');
|
|
442
|
+
const endIdx = jsonStr.lastIndexOf('}');
|
|
443
|
+
|
|
444
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
445
|
+
throw new Error('Response did not contain a valid JSON object');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
jsonStr = jsonStr.substring(startIdx, endIdx + 1);
|
|
449
|
+
const parsed = JSON.parse(jsonStr);
|
|
450
|
+
const validated = ContextAwareAnalysisSchema.parse(parsed);
|
|
451
|
+
|
|
452
|
+
logger.debug('Context-aware analysis complete', {
|
|
453
|
+
matched_rule: validated.matched_rule.rule_name,
|
|
454
|
+
confidence: validated.matched_rule.confidence,
|
|
455
|
+
actions: validated.actions_to_execute,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
if (eventLogger && emailId) {
|
|
459
|
+
await eventLogger.analysis('Decided', emailId, {
|
|
460
|
+
...validated,
|
|
461
|
+
_raw_response: rawResponse,
|
|
462
|
+
usage: usage // Include token usage
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return validated;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
console.error('[Intelligence] Context-aware analysis failed:', error);
|
|
469
|
+
if (eventLogger) {
|
|
470
|
+
await eventLogger.error('Error', {
|
|
471
|
+
error: error instanceof Error ? error.message : String(error),
|
|
472
|
+
raw_response: rawResponse || 'No response received from LLM'
|
|
473
|
+
}, emailId);
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
254
479
|
async testConnection(): Promise<{ success: boolean; message: string }> {
|
|
255
480
|
if (!this.isReady()) {
|
|
256
481
|
return { success: false, message: 'Intelligence service not initialized. Check your API Key.' };
|