@realtimex/email-automator 2.6.5 → 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.
@@ -211,7 +211,8 @@ REQUIRED JSON STRUCTURE:
211
211
  });
212
212
 
213
213
  rawResponse = response.choices[0]?.message?.content || '';
214
- console.log('[Intelligence] Raw LLM Response received (length:', rawResponse.length, ')');
214
+ const usage = response.usage;
215
+ console.log('[Intelligence] Raw LLM Response received (length:', rawResponse.length, ')', { usage });
215
216
 
216
217
  // Clean the response: Find first '{' and last '}'
217
218
  let jsonStr = rawResponse.trim();
@@ -235,7 +236,8 @@ REQUIRED JSON STRUCTURE:
235
236
  if (eventLogger && emailId) {
236
237
  await eventLogger.analysis('Decided', emailId, {
237
238
  ...validated,
238
- _raw_response: rawResponse
239
+ _raw_response: rawResponse,
240
+ usage: usage // Include token usage
239
241
  });
240
242
  }
241
243
 
@@ -431,7 +433,8 @@ Return ONLY valid JSON.`;
431
433
  });
432
434
 
433
435
  rawResponse = response.choices[0]?.message?.content || '';
434
- console.log('[Intelligence] Context-aware response received (length:', rawResponse.length, ')');
436
+ const usage = response.usage;
437
+ console.log('[Intelligence] Context-aware response received (length:', rawResponse.length, ')', { usage });
435
438
 
436
439
  // Parse JSON from response
437
440
  let jsonStr = rawResponse.trim();
@@ -455,7 +458,8 @@ Return ONLY valid JSON.`;
455
458
  if (eventLogger && emailId) {
456
459
  await eventLogger.analysis('Decided', emailId, {
457
460
  ...validated,
458
- _raw_response: rawResponse
461
+ _raw_response: rawResponse,
462
+ usage: usage // Include token usage
459
463
  });
460
464
  }
461
465
 
@@ -55,8 +55,8 @@ export class EmailProcessorService {
55
55
  throw new Error('Account not found or access denied');
56
56
  }
57
57
 
58
- logger.info('Retrieved account settings', {
59
- accountId: account.id,
58
+ logger.info('Retrieved account settings', {
59
+ accountId: account.id,
60
60
  sync_start_date: account.sync_start_date,
61
61
  last_sync_checkpoint: account.last_sync_checkpoint
62
62
  });
@@ -104,7 +104,7 @@ export class EmailProcessorService {
104
104
  await this.runRetentionRules(refreshedAccount, rules || [], settings, result, eventLogger);
105
105
 
106
106
  // Trigger background worker (async) to process the queue
107
- this.processQueue(userId, settings).catch(err =>
107
+ this.processQueue(userId, settings).catch(err =>
108
108
  logger.error('Background worker failed', err)
109
109
  );
110
110
 
@@ -160,7 +160,7 @@ export class EmailProcessorService {
160
160
  if (errMsg.includes('Account not found') || errMsg.includes('access denied')) {
161
161
  throw error;
162
162
  }
163
-
163
+
164
164
  // Otherwise, increment error count and return partial results
165
165
  result.errors++;
166
166
  }
@@ -199,7 +199,7 @@ export class EmailProcessorService {
199
199
  const windowSizeMs = 7 * 24 * 60 * 60 * 1000;
200
200
  const nowMs = Date.now();
201
201
  const tomorrowMs = nowMs + (24 * 60 * 60 * 1000);
202
-
202
+
203
203
  let currentStartMs = effectiveStartMs;
204
204
  let messages: GmailMessage[] = [];
205
205
  let hasMore = false;
@@ -231,7 +231,7 @@ export class EmailProcessorService {
231
231
  logger.info('No emails in 7-day window, skipping forward', { start: new Date(currentStartMs).toISOString() });
232
232
  currentStartMs = effectiveEndMs;
233
233
  attempts++;
234
-
234
+
235
235
  if (eventLogger && attempts % 3 === 0) {
236
236
  await eventLogger.info('Sync', `Scanning history... reached ${new Date(currentStartMs).toLocaleDateString()}`);
237
237
  }
@@ -262,17 +262,17 @@ export class EmailProcessorService {
262
262
 
263
263
  // Update checkpoint once at the end of the batch if we made progress
264
264
  if (maxInternalDate > effectiveStartMs) {
265
- logger.info('Updating Gmail checkpoint', {
266
- accountId: account.id,
265
+ logger.info('Updating Gmail checkpoint', {
266
+ accountId: account.id,
267
267
  oldCheckpoint: account.last_sync_checkpoint,
268
- newCheckpoint: maxInternalDate.toString()
268
+ newCheckpoint: maxInternalDate.toString()
269
269
  });
270
-
270
+
271
271
  const { error: updateError } = await this.supabase
272
272
  .from('email_accounts')
273
273
  .update({ last_sync_checkpoint: maxInternalDate.toString() })
274
274
  .eq('id', account.id);
275
-
275
+
276
276
  if (updateError) {
277
277
  logger.error('Failed to update Gmail checkpoint', updateError);
278
278
  }
@@ -346,12 +346,12 @@ export class EmailProcessorService {
346
346
 
347
347
  // Update checkpoint once at the end of the batch if we made progress
348
348
  if (latestCheckpoint && latestCheckpoint !== effectiveStartIso) {
349
- logger.info('Updating Outlook checkpoint', {
350
- accountId: account.id,
349
+ logger.info('Updating Outlook checkpoint', {
350
+ accountId: account.id,
351
351
  oldCheckpoint: account.last_sync_checkpoint,
352
- newCheckpoint: latestCheckpoint
352
+ newCheckpoint: latestCheckpoint
353
353
  });
354
-
354
+
355
355
  const { error: updateError } = await this.supabase
356
356
  .from('email_accounts')
357
357
  .update({ last_sync_checkpoint: latestCheckpoint })
@@ -387,11 +387,11 @@ export class EmailProcessorService {
387
387
  if (existing) {
388
388
  logger.debug('Message already processed', { messageId: message.id });
389
389
  if (eventLogger) await eventLogger.info('Skipped', `Already processed ID: ${message.id}`);
390
-
390
+
391
391
  // Still need to return the date for checkpointing even if skipped
392
- const rawMime = 'raw' in message
393
- ? (account.provider === 'gmail'
394
- ? Buffer.from(message.raw, 'base64').toString('utf-8')
392
+ const rawMime = 'raw' in message
393
+ ? (account.provider === 'gmail'
394
+ ? Buffer.from(message.raw, 'base64').toString('utf-8')
395
395
  : message.raw)
396
396
  : '';
397
397
  if (rawMime) {
@@ -400,11 +400,11 @@ export class EmailProcessorService {
400
400
  }
401
401
  return;
402
402
  }
403
-
403
+
404
404
  // Extract raw content string (Gmail is base64url, Outlook is raw text from $value)
405
- const rawMime = 'raw' in message
406
- ? (account.provider === 'gmail'
407
- ? Buffer.from(message.raw, 'base64').toString('utf-8')
405
+ const rawMime = 'raw' in message
406
+ ? (account.provider === 'gmail'
407
+ ? Buffer.from(message.raw, 'base64').toString('utf-8')
408
408
  : message.raw)
409
409
  : '';
410
410
 
@@ -462,7 +462,7 @@ export class EmailProcessorService {
462
462
  if (eventLogger) await eventLogger.info('Ingested', `Successfully ingested email: ${subject}`, { filePath }, savedEmail.id);
463
463
 
464
464
  result.processed++;
465
-
465
+
466
466
  return { date };
467
467
  }
468
468
 
@@ -522,7 +522,7 @@ export class EmailProcessorService {
522
522
  .select('processing_status')
523
523
  .eq('id', email.id)
524
524
  .single();
525
-
525
+
526
526
  if (current?.processing_status !== 'pending') {
527
527
  if (log) await this.supabase.from('processing_logs').delete().eq('id', log.id);
528
528
  return;
@@ -539,7 +539,7 @@ export class EmailProcessorService {
539
539
  if (!email.file_path) throw new Error('No file path found for email');
540
540
  const rawMime = await this.storageService.readEmail(email.file_path);
541
541
  const parsed = await simpleParser(rawMime);
542
-
542
+
543
543
  // Extract clean content (prioritize text)
544
544
  const cleanContent = parsed.text || parsed.textAsHtml || '';
545
545
 
@@ -561,7 +561,7 @@ export class EmailProcessorService {
561
561
  // 4. Fetch pre-compiled rule context (fast path - no loop/formatting)
562
562
  // Falls back to building context if not cached
563
563
  let compiledContext: string | null = settings?.compiled_rule_context || null;
564
-
564
+
565
565
  // Fetch rules for action execution (need attachments, instructions)
566
566
  const { data: rules } = await this.supabase
567
567
  .from('rules')
@@ -572,15 +572,44 @@ export class EmailProcessorService {
572
572
 
573
573
  // Fallback: build context if not pre-compiled
574
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('');
575
+ compiledContext = rules.map((r, i) => {
576
+ // Build human-readable condition text
577
+ let conditionText = '';
578
+ if (r.condition) {
579
+ const cond = r.condition as any;
580
+ if (cond.field) {
581
+ conditionText = `When ${cond.field}`;
582
+ if (cond.operator === 'equals') {
583
+ conditionText += ` equals "${cond.value}"`;
584
+ } else if (cond.operator === 'contains') {
585
+ conditionText += ` contains "${cond.value}"`;
586
+ } else if (cond.operator === 'domain_equals') {
587
+ conditionText += ` domain equals "${cond.value}"`;
588
+ } else {
589
+ conditionText += ` ${cond.operator} "${cond.value}"`;
590
+ }
591
+ }
592
+ if (cond.is_useless === true) {
593
+ conditionText += (conditionText ? ' AND ' : 'When ') + 'email is useless/low-value';
594
+ }
595
+ if (cond.ai_priority) {
596
+ conditionText += (conditionText ? ' AND ' : 'When ') + `AI priority is "${cond.ai_priority}"`;
597
+ }
598
+ // Extract older_than_days from condition JSONB
599
+ if (cond.older_than_days) {
600
+ conditionText += (conditionText ? ' AND ' : 'When ') + `email is older than ${cond.older_than_days} days`;
601
+ }
602
+ }
603
+
604
+ return `Rule ${i + 1} [ID: ${r.id}]\n` +
605
+ ` Name: ${r.name}\n` +
606
+ (r.description ? ` Description: ${r.description}\n` : '') +
607
+ (r.intent ? ` Intent: ${r.intent}\n` : '') +
608
+ (conditionText ? ` Condition: ${conditionText}\n` : '') +
609
+ ` Actions: ${r.actions?.join(', ') || r.action || 'none'}\n` +
610
+ (r.instructions ? ` Draft Instructions: ${r.instructions}\n` : '') +
611
+ '\n';
612
+ }).join('');
584
613
  }
585
614
 
586
615
  // 5. Context-Aware Analysis: AI evaluates email against user's rules
@@ -632,9 +661,9 @@ export class EmailProcessorService {
632
661
  // 7. Execute actions if rule matched with sufficient confidence
633
662
  if (account && analysis.matched_rule.rule_id && analysis.matched_rule.confidence >= 0.7) {
634
663
  const matchedRule = rules?.find(r => r.id === analysis.matched_rule.rule_id);
635
-
664
+
636
665
  if (eventLogger) {
637
- await eventLogger.info('Rule Matched',
666
+ await eventLogger.info('Rule Matched',
638
667
  `"${analysis.matched_rule.rule_name}" matched with ${(analysis.matched_rule.confidence * 100).toFixed(0)}% confidence`,
639
668
  { reasoning: analysis.matched_rule.reasoning },
640
669
  email.id
@@ -644,22 +673,22 @@ export class EmailProcessorService {
644
673
  // Execute each action from the AI's decision
645
674
  for (const action of analysis.actions_to_execute) {
646
675
  if (action === 'none') continue;
647
-
676
+
648
677
  // Use AI-generated draft content if available
649
678
  const draftContent = action === 'draft' ? analysis.draft_content : undefined;
650
-
679
+
651
680
  await this.executeAction(
652
- account,
653
- email,
654
- action as any,
655
- draftContent,
656
- eventLogger,
681
+ account,
682
+ email,
683
+ action as any,
684
+ draftContent,
685
+ eventLogger,
657
686
  `Rule: ${matchedRule?.name || analysis.matched_rule.rule_name}`,
658
687
  matchedRule?.attachments
659
688
  );
660
689
  }
661
690
  } else if (eventLogger && rules && rules.length > 0) {
662
- await eventLogger.info('No Match',
691
+ await eventLogger.info('No Match',
663
692
  analysis.matched_rule.reasoning,
664
693
  { confidence: analysis.matched_rule.confidence },
665
694
  email.id
@@ -670,10 +699,10 @@ export class EmailProcessorService {
670
699
  if (log) {
671
700
  await this.supabase
672
701
  .from('processing_logs')
673
- .update({
674
- status: 'success',
702
+ .update({
703
+ status: 'success',
675
704
  completed_at: new Date().toISOString(),
676
- emails_processed: 1
705
+ emails_processed: 1
677
706
  })
678
707
  .eq('id', log.id);
679
708
  }
@@ -681,13 +710,13 @@ export class EmailProcessorService {
681
710
  } catch (error) {
682
711
  logger.error('Failed to process pending email', error, { emailId: email.id });
683
712
  if (eventLogger) await eventLogger.error('Processing Failed', error, email.id);
684
-
713
+
685
714
  // Mark log as failed
686
715
  if (log) {
687
716
  await this.supabase
688
717
  .from('processing_logs')
689
- .update({
690
- status: 'failed',
718
+ .update({
719
+ status: 'failed',
691
720
  completed_at: new Date().toISOString(),
692
721
  error_message: error instanceof Error ? error.message : String(error)
693
722
  })
@@ -696,7 +725,7 @@ export class EmailProcessorService {
696
725
 
697
726
  await this.supabase
698
727
  .from('emails')
699
- .update({
728
+ .update({
700
729
  processing_status: 'failed',
701
730
  processing_error: error instanceof Error ? error.message : String(error),
702
731
  retry_count: (email.retry_count || 0) + 1
@@ -766,10 +795,10 @@ export class EmailProcessorService {
766
795
 
767
796
  private matchesCondition(email: Partial<Email>, analysis: EmailAnalysis, condition: Record<string, unknown>): boolean {
768
797
  if (!analysis) return false;
769
-
798
+
770
799
  for (const [key, value] of Object.entries(condition)) {
771
800
  const val = value as string;
772
-
801
+
773
802
  switch (key) {
774
803
  case 'sender_email':
775
804
  if (email.sender?.toLowerCase() !== val.toLowerCase()) return false;
@@ -810,7 +839,7 @@ export class EmailProcessorService {
810
839
  // Handle array membership check (e.g. if condition expects "reply" to be in actions)
811
840
  const requiredActions = Array.isArray(value) ? value : [value];
812
841
  const actualActions = analysis.suggested_actions || [];
813
- const hasAllActions = requiredActions.every(req =>
842
+ const hasAllActions = requiredActions.every(req =>
814
843
  actualActions.includes(req as any)
815
844
  );
816
845
  if (!hasAllActions) return false;
@@ -936,7 +965,7 @@ export class EmailProcessorService {
936
965
  .eq('id', email.id);
937
966
 
938
967
  logger.debug('Action executed', { emailId: email.id, action });
939
-
968
+
940
969
  if (eventLogger) {
941
970
  await eventLogger.action('Acted', email.id, action, reason);
942
971
  }
@@ -166,7 +166,8 @@ REQUIRED JSON STRUCTURE:
166
166
  temperature: 0.1,
167
167
  });
168
168
  rawResponse = response.choices[0]?.message?.content || '';
169
- console.log('[Intelligence] Raw LLM Response received (length:', rawResponse.length, ')');
169
+ const usage = response.usage;
170
+ console.log('[Intelligence] Raw LLM Response received (length:', rawResponse.length, ')', { usage });
170
171
  // Clean the response: Find first '{' and last '}'
171
172
  let jsonStr = rawResponse.trim();
172
173
  const startIdx = jsonStr.indexOf('{');
@@ -184,7 +185,8 @@ REQUIRED JSON STRUCTURE:
184
185
  if (eventLogger && emailId) {
185
186
  await eventLogger.analysis('Decided', emailId, {
186
187
  ...validated,
187
- _raw_response: rawResponse
188
+ _raw_response: rawResponse,
189
+ usage: usage // Include token usage
188
190
  });
189
191
  }
190
192
  return validated;
@@ -362,7 +364,8 @@ Return ONLY valid JSON.`;
362
364
  temperature: 0.1,
363
365
  });
364
366
  rawResponse = response.choices[0]?.message?.content || '';
365
- console.log('[Intelligence] Context-aware response received (length:', rawResponse.length, ')');
367
+ const usage = response.usage;
368
+ console.log('[Intelligence] Context-aware response received (length:', rawResponse.length, ')', { usage });
366
369
  // Parse JSON from response
367
370
  let jsonStr = rawResponse.trim();
368
371
  const startIdx = jsonStr.indexOf('{');
@@ -381,7 +384,8 @@ Return ONLY valid JSON.`;
381
384
  if (eventLogger && emailId) {
382
385
  await eventLogger.analysis('Decided', emailId, {
383
386
  ...validated,
384
- _raw_response: rawResponse
387
+ _raw_response: rawResponse,
388
+ usage: usage // Include token usage
385
389
  });
386
390
  }
387
391
  return validated;
@@ -472,13 +472,46 @@ export class EmailProcessorService {
472
472
  .order('priority', { ascending: false });
473
473
  // Fallback: build context if not pre-compiled
474
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('');
475
+ compiledContext = rules.map((r, i) => {
476
+ // Build human-readable condition text
477
+ let conditionText = '';
478
+ if (r.condition) {
479
+ const cond = r.condition;
480
+ if (cond.field) {
481
+ conditionText = `When ${cond.field}`;
482
+ if (cond.operator === 'equals') {
483
+ conditionText += ` equals "${cond.value}"`;
484
+ }
485
+ else if (cond.operator === 'contains') {
486
+ conditionText += ` contains "${cond.value}"`;
487
+ }
488
+ else if (cond.operator === 'domain_equals') {
489
+ conditionText += ` domain equals "${cond.value}"`;
490
+ }
491
+ else {
492
+ conditionText += ` ${cond.operator} "${cond.value}"`;
493
+ }
494
+ }
495
+ if (cond.is_useless === true) {
496
+ conditionText += (conditionText ? ' AND ' : 'When ') + 'email is useless/low-value';
497
+ }
498
+ if (cond.ai_priority) {
499
+ conditionText += (conditionText ? ' AND ' : 'When ') + `AI priority is "${cond.ai_priority}"`;
500
+ }
501
+ // Extract older_than_days from condition JSONB
502
+ if (cond.older_than_days) {
503
+ conditionText += (conditionText ? ' AND ' : 'When ') + `email is older than ${cond.older_than_days} days`;
504
+ }
505
+ }
506
+ return `Rule ${i + 1} [ID: ${r.id}]\n` +
507
+ ` Name: ${r.name}\n` +
508
+ (r.description ? ` Description: ${r.description}\n` : '') +
509
+ (r.intent ? ` Intent: ${r.intent}\n` : '') +
510
+ (conditionText ? ` Condition: ${conditionText}\n` : '') +
511
+ ` Actions: ${r.actions?.join(', ') || r.action || 'none'}\n` +
512
+ (r.instructions ? ` Draft Instructions: ${r.instructions}\n` : '') +
513
+ '\n';
514
+ }).join('');
482
515
  }
483
516
  // 5. Context-Aware Analysis: AI evaluates email against user's rules
484
517
  const intelligenceService = getIntelligenceService(settings?.llm_model || settings?.llm_base_url || settings?.llm_api_key