@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.
@@ -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
- console.log('[Intelligence] Raw LLM Response received (length:', rawResponse.length, ')');
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.' };