@realtimex/email-automator 2.11.2 → 2.11.3

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.
@@ -161,9 +161,10 @@ export class SDKService {
161
161
 
162
162
  this.defaultChatProvider = {
163
163
  provider: preferredProvider.provider,
164
- model: preferredModel.id
164
+ model: preferredModel.id,
165
+ isDefaultFallback: false
165
166
  };
166
- logger.info(`Using preferred default chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`);
167
+ logger.info(`Using preferred chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`);
167
168
  return this.defaultChatProvider;
168
169
  }
169
170
 
@@ -172,7 +173,8 @@ export class SDKService {
172
173
  if (p.models && p.models.length > 0) {
173
174
  this.defaultChatProvider = {
174
175
  provider: p.provider,
175
- model: p.models[0].id
176
+ model: p.models[0].id,
177
+ isDefaultFallback: true
176
178
  };
177
179
  logger.info(`Defaulting to first available chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`);
178
180
  return this.defaultChatProvider;
@@ -221,9 +223,10 @@ export class SDKService {
221
223
  if (p.models && p.models.length > 0) {
222
224
  this.defaultEmbedProvider = {
223
225
  provider: p.provider,
224
- model: p.models[0].id
226
+ model: p.models[0].id,
227
+ isDefaultFallback: true
225
228
  };
226
- logger.info(`Default embed provider: ${this.defaultEmbedProvider.provider}/${this.defaultEmbedProvider.model}`);
229
+ logger.info(`Selected embed provider: ${this.defaultEmbedProvider.provider}/${this.defaultEmbedProvider.model}`);
227
230
  return this.defaultEmbedProvider;
228
231
  }
229
232
  }
@@ -245,7 +248,11 @@ export class SDKService {
245
248
  static async resolveChatProvider(settings: { llm_provider?: string; llm_model?: string }): Promise<ProviderResult> {
246
249
  // If both provider and model are set in settings, use them
247
250
  if (settings.llm_provider && settings.llm_model) {
248
- return { provider: settings.llm_provider, model: settings.llm_model };
251
+ return {
252
+ provider: settings.llm_provider,
253
+ model: settings.llm_model,
254
+ isDefaultFallback: false
255
+ };
249
256
  }
250
257
 
251
258
  // Try to get from SDK discovery first
@@ -258,7 +265,11 @@ export class SDKService {
258
265
  static async resolveEmbedProvider(settings: { embedding_provider?: string; embedding_model?: string }): Promise<ProviderResult> {
259
266
  // If both provider and model are set in settings, use them
260
267
  if (settings.embedding_provider && settings.embedding_model) {
261
- return { provider: settings.embedding_provider, model: settings.embedding_model };
268
+ return {
269
+ provider: settings.embedding_provider,
270
+ model: settings.embedding_model,
271
+ isDefaultFallback: false
272
+ };
262
273
  }
263
274
 
264
275
  // Try to get from SDK discovery first
@@ -91,7 +91,7 @@ export class IntelligenceService {
91
91
  return this.isConfigured && !!SDKService.getSDK();
92
92
  }
93
93
 
94
- async analyzeEmail(content: string, context: EmailContext, eventLogger?: EventLogger, emailId?: string): Promise<(EmailAnalysis & { _metadata?: any }) | null> {
94
+ async analyzeEmail(content: string, context: EmailContext, eventLogger?: EventLogger, emailId?: string, llmSettings?: { llm_provider?: string; llm_model?: string }): Promise<(EmailAnalysis & { _metadata?: any }) | null> {
95
95
  const sdk = SDKService.getSDK();
96
96
  if (!sdk) {
97
97
  logger.warn('Intelligence service not ready, skipping analysis');
@@ -101,7 +101,10 @@ export class IntelligenceService {
101
101
  return null;
102
102
  }
103
103
 
104
- const { provider, model, isDefaultFallback } = await SDKService.resolveChatProvider({});
104
+ const { provider, model, isDefaultFallback } = await SDKService.resolveChatProvider({
105
+ llm_provider: llmSettings?.llm_provider,
106
+ llm_model: llmSettings?.llm_model
107
+ });
105
108
 
106
109
  const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
107
110
 
@@ -134,10 +137,10 @@ REQUIRED JSON STRUCTURE:
134
137
 
135
138
  if (eventLogger) {
136
139
  await eventLogger.info('Thinking', `Analyzing email: ${context.subject}`, {
137
- model,
138
- provider,
140
+ provider: `${provider}/${model}`,
139
141
  is_fallback: isDefaultFallback,
140
- content_preview: cleanedContent
142
+ signals: metadataSignals,
143
+ content_preview: cleanedContent.substring(0, 100) + '...'
141
144
  }, emailId);
142
145
  }
143
146
 
@@ -147,8 +150,27 @@ REQUIRED JSON STRUCTURE:
147
150
  { role: 'user', content: cleanedContent || '[Empty body]' }
148
151
  ], { provider, model });
149
152
 
153
+ // Check if SDK call failed
154
+ if (!response.success || response.error) {
155
+ const errorMsg = response.error || 'Unknown SDK error';
156
+ logger.error('SDK chat failed for email analysis', {
157
+ provider,
158
+ model,
159
+ error: errorMsg,
160
+ code: response.code
161
+ });
162
+ if (eventLogger) await eventLogger.error('SDK Error', `${errorMsg} (${provider}/${model})`, emailId);
163
+ return null;
164
+ }
165
+
150
166
  const rawResponse = response.response?.content || '';
151
- const validated = this.parseRobustJSON<EmailAnalysis>(rawResponse, EmailAnalysisSchema);
167
+ if (!rawResponse) {
168
+ logger.warn('SDK returned empty response for analysis', { provider, model });
169
+ if (eventLogger) await eventLogger.error('Empty Response', `LLM (${provider}/${model}) returned no content`, emailId);
170
+ return null;
171
+ }
172
+
173
+ const validated = this.parseRobustJSON<EmailAnalysis>(rawResponse, EmailAnalysisSchema, eventLogger, emailId);
152
174
 
153
175
  const result = validated ? {
154
176
  ...validated,
@@ -165,6 +187,11 @@ REQUIRED JSON STRUCTURE:
165
187
  ...result,
166
188
  _raw_response: rawResponse
167
189
  });
190
+ } else if (eventLogger && !result) {
191
+ await eventLogger.error('Malformed Response', {
192
+ message: 'AI returned data that did not match the required schema',
193
+ raw_response: rawResponse.substring(0, 500)
194
+ }, emailId);
168
195
  }
169
196
 
170
197
  return result;
@@ -177,12 +204,16 @@ REQUIRED JSON STRUCTURE:
177
204
 
178
205
  async generateDraftReply(
179
206
  originalEmail: { subject: string; sender: string; body: string },
180
- instructions?: string
207
+ instructions?: string,
208
+ llmSettings?: { llm_provider?: string; llm_model?: string }
181
209
  ): Promise<string | null> {
182
210
  const sdk = SDKService.getSDK();
183
211
  if (!sdk) return null;
184
212
 
185
- const { provider, model } = await SDKService.resolveChatProvider({});
213
+ const { provider, model } = await SDKService.resolveChatProvider({
214
+ llm_provider: llmSettings?.llm_provider,
215
+ llm_model: llmSettings?.llm_model
216
+ });
186
217
 
187
218
  try {
188
219
  const response = await sdk.llm.chat([
@@ -196,6 +227,17 @@ REQUIRED JSON STRUCTURE:
196
227
  },
197
228
  ], { provider, model });
198
229
 
230
+ // Check if SDK call failed
231
+ if (!response.success || response.error) {
232
+ logger.error('SDK chat failed for draft generation', {
233
+ provider,
234
+ model,
235
+ error: response.error,
236
+ code: response.code
237
+ });
238
+ return null;
239
+ }
240
+
199
241
  return response.response?.content || null;
200
242
  } catch (error) {
201
243
  logger.error('Draft generation failed', error);
@@ -208,12 +250,16 @@ REQUIRED JSON STRUCTURE:
208
250
  context: EmailContext,
209
251
  compiledRulesContext: string | RuleContext[],
210
252
  eventLogger?: EventLogger,
211
- emailId?: string
253
+ emailId?: string,
254
+ llmSettings?: { llm_provider?: string; llm_model?: string }
212
255
  ): Promise<(ContextAwareAnalysis & { _metadata?: any }) | null> {
213
256
  const sdk = SDKService.getSDK();
214
257
  if (!sdk) return null;
215
258
 
216
- const { provider, model, isDefaultFallback } = await SDKService.resolveChatProvider({});
259
+ const { provider, model, isDefaultFallback } = await SDKService.resolveChatProvider({
260
+ llm_provider: llmSettings?.llm_provider,
261
+ llm_model: llmSettings?.llm_model
262
+ });
217
263
  const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
218
264
 
219
265
  let rulesContext: string;
@@ -223,24 +269,71 @@ REQUIRED JSON STRUCTURE:
223
269
  rulesContext = compiledRulesContext.map(r => `- ${r.name}: ${r.intent}`).join('\n');
224
270
  }
225
271
 
226
- const systemPrompt = `You are an AI Automation Agent. Match email against these rules:\n${rulesContext}\n\nReturn JSON with matched_rule, actions_to_execute, and draft_content.`;
272
+ const systemPrompt = `You are an AI Automation Agent. Analyze the email and match it against the user's rules.
273
+
274
+ Rules Context:
275
+ ${rulesContext}
276
+
277
+ REQUIRED JSON STRUCTURE:
278
+ {
279
+ "summary": "A brief summary of the email content",
280
+ "category": "spam|newsletter|promotional|transactional|social|support|client|internal|personal|other",
281
+ "priority": "High|Medium|Low",
282
+ "matched_rule": {
283
+ "rule_id": "string or null",
284
+ "rule_name": "string or null",
285
+ "confidence": 0.0 to 1.0,
286
+ "reasoning": "Brief explanation"
287
+ },
288
+ "actions_to_execute": ["none"|"delete"|"archive"|"draft"|"read"|"star"],
289
+ "draft_content": "Suggested reply if drafting, otherwise null"
290
+ }
291
+
292
+ IMPORTANT:
293
+ - Use "draft" action only if a rule explicitly requests it or if it's very clear a reply is needed.
294
+ - Categorize accurately.
295
+ - Confidence 0.7+ is required for automatic execution.`;
227
296
 
228
297
  if (eventLogger) {
229
298
  await eventLogger.info('Thinking', `Context-aware analysis: ${context.subject}`, {
230
- model,
231
- provider,
232
- is_fallback: isDefaultFallback
299
+ provider: `${provider}/${model}`,
300
+ is_fallback: isDefaultFallback,
301
+ rules_count: Array.isArray(compiledRulesContext) ? compiledRulesContext.length : 'compiled'
233
302
  }, emailId);
234
303
  }
235
304
 
236
305
  try {
306
+ logger.debug('Calling SDK chat for rule analysis', { provider, model, promptLength: systemPrompt.length });
237
307
  const response = await sdk.llm.chat([
238
308
  { role: 'system', content: systemPrompt },
239
309
  { role: 'user', content: cleanedContent || '[Empty body]' }
240
310
  ], { provider, model });
241
311
 
312
+ // Check if SDK call failed
313
+ if (!response.success || response.error) {
314
+ const errorMsg = response.error || 'Unknown SDK error';
315
+ logger.error('SDK chat failed for rule analysis', {
316
+ provider,
317
+ model,
318
+ error: errorMsg,
319
+ code: response.code
320
+ });
321
+ if (eventLogger) await eventLogger.error('SDK Error', `${errorMsg} (${provider}/${model})`, emailId);
322
+ return null;
323
+ }
324
+
242
325
  const rawResponse = response.response?.content || '';
243
- const validated = this.parseRobustJSON<ContextAwareAnalysis>(rawResponse, ContextAwareAnalysisSchema);
326
+ if (!rawResponse) {
327
+ logger.warn('SDK returned empty response for rule analysis', {
328
+ provider,
329
+ model,
330
+ success: response.success
331
+ });
332
+ if (eventLogger) await eventLogger.error('Empty Response', `LLM (${provider}/${model}) returned no content`, emailId);
333
+ return null;
334
+ }
335
+
336
+ const validated = this.parseRobustJSON<ContextAwareAnalysis>(rawResponse, ContextAwareAnalysisSchema, eventLogger, emailId);
244
337
 
245
338
  const result = validated ? {
246
339
  ...validated,
@@ -257,12 +350,23 @@ REQUIRED JSON STRUCTURE:
257
350
  ...result,
258
351
  _raw_response: rawResponse
259
352
  });
353
+ } else if (eventLogger && !result) {
354
+ await eventLogger.error('Malformed Response', {
355
+ message: 'AI returned rule analysis that did not match the required schema',
356
+ raw_response: rawResponse.substring(0, 500)
357
+ }, emailId);
260
358
  }
261
359
 
262
360
  return result;
263
361
  } catch (error: any) {
264
- logger.error('Rule analysis failed', error);
265
- if (eventLogger) await eventLogger.error('Error', error.message, emailId);
362
+ logger.error('Rule analysis failed', {
363
+ error: error.message,
364
+ stack: error.stack,
365
+ provider,
366
+ model,
367
+ errorType: error.constructor.name
368
+ });
369
+ if (eventLogger) await eventLogger.error('Error', `${error.message} (${provider}/${model})`, emailId);
266
370
  return null;
267
371
  }
268
372
  }
@@ -283,14 +387,48 @@ REQUIRED JSON STRUCTURE:
283
387
  }
284
388
  }
285
389
 
286
- private parseRobustJSON<T>(input: string, schema: z.ZodSchema<T>): T | null {
390
+ private parseRobustJSON<T>(input: string, schema: z.ZodSchema<T>, eventLogger?: EventLogger, emailId?: string): T | null {
287
391
  try {
288
- const jsonMatch = input.match(/\{[\s\S]*\}/);
289
- const jsonStr = jsonMatch ? jsonMatch[0] : input;
290
- const cleaned = jsonStr.replace(/<\|[\s\S]*?\|>/g, '').replace(/```json/g, '').replace(/```/g, '').trim();
392
+ // 1. Remove common LLM artifacts and markdown blocks
393
+ let cleaned = input.trim();
394
+
395
+ // Handle markdown blocks
396
+ if (cleaned.includes('```json')) {
397
+ cleaned = cleaned.split('```json')[1].split('```')[0].trim();
398
+ } else if (cleaned.includes('```')) {
399
+ cleaned = cleaned.split('```')[1].split('```')[0].trim();
400
+ }
401
+
402
+ // 2. Extract the first { ... } block if visible
403
+ const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
404
+ if (jsonMatch) {
405
+ cleaned = jsonMatch[0];
406
+ }
407
+
408
+ // 3. Strip aggressive local LLM tokens
409
+ cleaned = cleaned.replace(/<\|[\s\S]*?\|>/g, '').trim();
410
+
411
+ // 4. Parse and Normalize
291
412
  const parsed = JSON.parse(cleaned);
413
+
414
+ // Normalize actions_to_execute: convert string to array if needed
415
+ if (parsed && typeof parsed === 'object' && 'actions_to_execute' in parsed) {
416
+ if (typeof parsed.actions_to_execute === 'string') {
417
+ parsed.actions_to_execute = [parsed.actions_to_execute];
418
+ logger.debug('Normalized actions_to_execute from string to array', { original: parsed.actions_to_execute[0] });
419
+ }
420
+ }
421
+
422
+ // 5. Validate with Zod
292
423
  return schema.parse(parsed);
293
- } catch (e) {
424
+ } catch (e: any) {
425
+ logger.error('JSON Robust Parsing failed', { error: e.message, input: input.substring(0, 200) });
426
+ if (eventLogger && emailId) {
427
+ eventLogger.error('JSON Parse Error', {
428
+ error: e.message,
429
+ raw_input_preview: input.substring(0, 500)
430
+ }, emailId).catch(() => { });
431
+ }
294
432
  return null;
295
433
  }
296
434
  }
@@ -651,7 +651,11 @@ export class EmailProcessorService {
651
651
  },
652
652
  compiledContext || '', // Pre-compiled context (fast path)
653
653
  eventLogger || undefined,
654
- email.id
654
+ email.id,
655
+ {
656
+ llm_provider: settings?.llm_provider,
657
+ llm_model: settings?.llm_model
658
+ }
655
659
  );
656
660
 
657
661
  if (!analysis) {
@@ -789,7 +793,10 @@ export class EmailProcessorService {
789
793
  subject: email.subject || '',
790
794
  sender: email.sender || '',
791
795
  body: email.body_snippet || ''
792
- }, rule.instructions);
796
+ }, rule.instructions, {
797
+ llm_provider: settings?.llm_provider,
798
+ llm_model: settings?.llm_model
799
+ });
793
800
 
794
801
  if (customizedDraft) {
795
802
  draftContent = customizedDraft;
@@ -133,9 +133,10 @@ export class SDKService {
133
133
  const preferredModel = preferredProvider.models.find(m => m.id === this.DEFAULT_LLM_MODEL) || preferredProvider.models[0];
134
134
  this.defaultChatProvider = {
135
135
  provider: preferredProvider.provider,
136
- model: preferredModel.id
136
+ model: preferredModel.id,
137
+ isDefaultFallback: false
137
138
  };
138
- logger.info(`Using preferred default chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`);
139
+ logger.info(`Using preferred chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`);
139
140
  return this.defaultChatProvider;
140
141
  }
141
142
  // 2. Fallback to the first provider with available models
@@ -143,7 +144,8 @@ export class SDKService {
143
144
  if (p.models && p.models.length > 0) {
144
145
  this.defaultChatProvider = {
145
146
  provider: p.provider,
146
- model: p.models[0].id
147
+ model: p.models[0].id,
148
+ isDefaultFallback: true
147
149
  };
148
150
  logger.info(`Defaulting to first available chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`);
149
151
  return this.defaultChatProvider;
@@ -183,9 +185,10 @@ export class SDKService {
183
185
  if (p.models && p.models.length > 0) {
184
186
  this.defaultEmbedProvider = {
185
187
  provider: p.provider,
186
- model: p.models[0].id
188
+ model: p.models[0].id,
189
+ isDefaultFallback: true
187
190
  };
188
- logger.info(`Default embed provider: ${this.defaultEmbedProvider.provider}/${this.defaultEmbedProvider.model}`);
191
+ logger.info(`Selected embed provider: ${this.defaultEmbedProvider.provider}/${this.defaultEmbedProvider.model}`);
189
192
  return this.defaultEmbedProvider;
190
193
  }
191
194
  }
@@ -206,7 +209,11 @@ export class SDKService {
206
209
  static async resolveChatProvider(settings) {
207
210
  // If both provider and model are set in settings, use them
208
211
  if (settings.llm_provider && settings.llm_model) {
209
- return { provider: settings.llm_provider, model: settings.llm_model };
212
+ return {
213
+ provider: settings.llm_provider,
214
+ model: settings.llm_model,
215
+ isDefaultFallback: false
216
+ };
210
217
  }
211
218
  // Try to get from SDK discovery first
212
219
  return await this.getDefaultChatProvider();
@@ -217,7 +224,11 @@ export class SDKService {
217
224
  static async resolveEmbedProvider(settings) {
218
225
  // If both provider and model are set in settings, use them
219
226
  if (settings.embedding_provider && settings.embedding_model) {
220
- return { provider: settings.embedding_provider, model: settings.embedding_model };
227
+ return {
228
+ provider: settings.embedding_provider,
229
+ model: settings.embedding_model,
230
+ isDefaultFallback: false
231
+ };
221
232
  }
222
233
  // Try to get from SDK discovery first
223
234
  return await this.getDefaultEmbedProvider();