@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.
@@ -50,7 +50,7 @@ export class IntelligenceService {
50
50
  isReady() {
51
51
  return this.isConfigured && !!SDKService.getSDK();
52
52
  }
53
- async analyzeEmail(content, context, eventLogger, emailId) {
53
+ async analyzeEmail(content, context, eventLogger, emailId, llmSettings) {
54
54
  const sdk = SDKService.getSDK();
55
55
  if (!sdk) {
56
56
  logger.warn('Intelligence service not ready, skipping analysis');
@@ -59,7 +59,10 @@ export class IntelligenceService {
59
59
  }
60
60
  return null;
61
61
  }
62
- const { provider, model, isDefaultFallback } = await SDKService.resolveChatProvider({});
62
+ const { provider, model, isDefaultFallback } = await SDKService.resolveChatProvider({
63
+ llm_provider: llmSettings?.llm_provider,
64
+ llm_model: llmSettings?.llm_model
65
+ });
63
66
  const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
64
67
  const metadataSignals = [];
65
68
  if (context.metadata?.listUnsubscribe)
@@ -91,10 +94,10 @@ REQUIRED JSON STRUCTURE:
91
94
  `;
92
95
  if (eventLogger) {
93
96
  await eventLogger.info('Thinking', `Analyzing email: ${context.subject}`, {
94
- model,
95
- provider,
97
+ provider: `${provider}/${model}`,
96
98
  is_fallback: isDefaultFallback,
97
- content_preview: cleanedContent
99
+ signals: metadataSignals,
100
+ content_preview: cleanedContent.substring(0, 100) + '...'
98
101
  }, emailId);
99
102
  }
100
103
  try {
@@ -102,8 +105,27 @@ REQUIRED JSON STRUCTURE:
102
105
  { role: 'system', content: systemPrompt },
103
106
  { role: 'user', content: cleanedContent || '[Empty body]' }
104
107
  ], { provider, model });
108
+ // Check if SDK call failed
109
+ if (!response.success || response.error) {
110
+ const errorMsg = response.error || 'Unknown SDK error';
111
+ logger.error('SDK chat failed for email analysis', {
112
+ provider,
113
+ model,
114
+ error: errorMsg,
115
+ code: response.code
116
+ });
117
+ if (eventLogger)
118
+ await eventLogger.error('SDK Error', `${errorMsg} (${provider}/${model})`, emailId);
119
+ return null;
120
+ }
105
121
  const rawResponse = response.response?.content || '';
106
- const validated = this.parseRobustJSON(rawResponse, EmailAnalysisSchema);
122
+ if (!rawResponse) {
123
+ logger.warn('SDK returned empty response for analysis', { provider, model });
124
+ if (eventLogger)
125
+ await eventLogger.error('Empty Response', `LLM (${provider}/${model}) returned no content`, emailId);
126
+ return null;
127
+ }
128
+ const validated = this.parseRobustJSON(rawResponse, EmailAnalysisSchema, eventLogger, emailId);
107
129
  const result = validated ? {
108
130
  ...validated,
109
131
  _metadata: {
@@ -119,6 +141,12 @@ REQUIRED JSON STRUCTURE:
119
141
  _raw_response: rawResponse
120
142
  });
121
143
  }
144
+ else if (eventLogger && !result) {
145
+ await eventLogger.error('Malformed Response', {
146
+ message: 'AI returned data that did not match the required schema',
147
+ raw_response: rawResponse.substring(0, 500)
148
+ }, emailId);
149
+ }
122
150
  return result;
123
151
  }
124
152
  catch (error) {
@@ -128,11 +156,14 @@ REQUIRED JSON STRUCTURE:
128
156
  return null;
129
157
  }
130
158
  }
131
- async generateDraftReply(originalEmail, instructions) {
159
+ async generateDraftReply(originalEmail, instructions, llmSettings) {
132
160
  const sdk = SDKService.getSDK();
133
161
  if (!sdk)
134
162
  return null;
135
- const { provider, model } = await SDKService.resolveChatProvider({});
163
+ const { provider, model } = await SDKService.resolveChatProvider({
164
+ llm_provider: llmSettings?.llm_provider,
165
+ llm_model: llmSettings?.llm_model
166
+ });
136
167
  try {
137
168
  const response = await sdk.llm.chat([
138
169
  {
@@ -144,6 +175,16 @@ REQUIRED JSON STRUCTURE:
144
175
  content: `From: ${originalEmail.sender}\nSubject: ${originalEmail.subject}\n\n${originalEmail.body}`,
145
176
  },
146
177
  ], { provider, model });
178
+ // Check if SDK call failed
179
+ if (!response.success || response.error) {
180
+ logger.error('SDK chat failed for draft generation', {
181
+ provider,
182
+ model,
183
+ error: response.error,
184
+ code: response.code
185
+ });
186
+ return null;
187
+ }
147
188
  return response.response?.content || null;
148
189
  }
149
190
  catch (error) {
@@ -151,11 +192,14 @@ REQUIRED JSON STRUCTURE:
151
192
  return null;
152
193
  }
153
194
  }
154
- async analyzeEmailWithRules(content, context, compiledRulesContext, eventLogger, emailId) {
195
+ async analyzeEmailWithRules(content, context, compiledRulesContext, eventLogger, emailId, llmSettings) {
155
196
  const sdk = SDKService.getSDK();
156
197
  if (!sdk)
157
198
  return null;
158
- const { provider, model, isDefaultFallback } = await SDKService.resolveChatProvider({});
199
+ const { provider, model, isDefaultFallback } = await SDKService.resolveChatProvider({
200
+ llm_provider: llmSettings?.llm_provider,
201
+ llm_model: llmSettings?.llm_model
202
+ });
159
203
  const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
160
204
  let rulesContext;
161
205
  if (typeof compiledRulesContext === 'string') {
@@ -164,21 +208,68 @@ REQUIRED JSON STRUCTURE:
164
208
  else {
165
209
  rulesContext = compiledRulesContext.map(r => `- ${r.name}: ${r.intent}`).join('\n');
166
210
  }
167
- 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.`;
211
+ const systemPrompt = `You are an AI Automation Agent. Analyze the email and match it against the user's rules.
212
+
213
+ Rules Context:
214
+ ${rulesContext}
215
+
216
+ REQUIRED JSON STRUCTURE:
217
+ {
218
+ "summary": "A brief summary of the email content",
219
+ "category": "spam|newsletter|promotional|transactional|social|support|client|internal|personal|other",
220
+ "priority": "High|Medium|Low",
221
+ "matched_rule": {
222
+ "rule_id": "string or null",
223
+ "rule_name": "string or null",
224
+ "confidence": 0.0 to 1.0,
225
+ "reasoning": "Brief explanation"
226
+ },
227
+ "actions_to_execute": ["none"|"delete"|"archive"|"draft"|"read"|"star"],
228
+ "draft_content": "Suggested reply if drafting, otherwise null"
229
+ }
230
+
231
+ IMPORTANT:
232
+ - Use "draft" action only if a rule explicitly requests it or if it's very clear a reply is needed.
233
+ - Categorize accurately.
234
+ - Confidence 0.7+ is required for automatic execution.`;
168
235
  if (eventLogger) {
169
236
  await eventLogger.info('Thinking', `Context-aware analysis: ${context.subject}`, {
170
- model,
171
- provider,
172
- is_fallback: isDefaultFallback
237
+ provider: `${provider}/${model}`,
238
+ is_fallback: isDefaultFallback,
239
+ rules_count: Array.isArray(compiledRulesContext) ? compiledRulesContext.length : 'compiled'
173
240
  }, emailId);
174
241
  }
175
242
  try {
243
+ logger.debug('Calling SDK chat for rule analysis', { provider, model, promptLength: systemPrompt.length });
176
244
  const response = await sdk.llm.chat([
177
245
  { role: 'system', content: systemPrompt },
178
246
  { role: 'user', content: cleanedContent || '[Empty body]' }
179
247
  ], { provider, model });
248
+ // Check if SDK call failed
249
+ if (!response.success || response.error) {
250
+ const errorMsg = response.error || 'Unknown SDK error';
251
+ logger.error('SDK chat failed for rule analysis', {
252
+ provider,
253
+ model,
254
+ error: errorMsg,
255
+ code: response.code
256
+ });
257
+ if (eventLogger)
258
+ await eventLogger.error('SDK Error', `${errorMsg} (${provider}/${model})`, emailId);
259
+ return null;
260
+ }
180
261
  const rawResponse = response.response?.content || '';
181
- const validated = this.parseRobustJSON(rawResponse, ContextAwareAnalysisSchema);
262
+ if (!rawResponse) {
263
+ logger.warn('SDK returned empty response for rule analysis', {
264
+ provider,
265
+ model,
266
+ success: response.success
267
+ });
268
+ if (eventLogger)
269
+ await eventLogger.error('Empty Response', `LLM (${provider}/${model}) returned no content`, emailId);
270
+ return null;
271
+ }
272
+ const validated = this.parseRobustJSON(rawResponse, ContextAwareAnalysisSchema, eventLogger, emailId);
182
273
  const result = validated ? {
183
274
  ...validated,
184
275
  _metadata: {
@@ -194,12 +285,24 @@ REQUIRED JSON STRUCTURE:
194
285
  _raw_response: rawResponse
195
286
  });
196
287
  }
288
+ else if (eventLogger && !result) {
289
+ await eventLogger.error('Malformed Response', {
290
+ message: 'AI returned rule analysis that did not match the required schema',
291
+ raw_response: rawResponse.substring(0, 500)
292
+ }, emailId);
293
+ }
197
294
  return result;
198
295
  }
199
296
  catch (error) {
200
- logger.error('Rule analysis failed', error);
297
+ logger.error('Rule analysis failed', {
298
+ error: error.message,
299
+ stack: error.stack,
300
+ provider,
301
+ model,
302
+ errorType: error.constructor.name
303
+ });
201
304
  if (eventLogger)
202
- await eventLogger.error('Error', error.message, emailId);
305
+ await eventLogger.error('Error', `${error.message} (${provider}/${model})`, emailId);
203
306
  return null;
204
307
  }
205
308
  }
@@ -219,15 +322,44 @@ REQUIRED JSON STRUCTURE:
219
322
  return { success: false, message: error.message };
220
323
  }
221
324
  }
222
- parseRobustJSON(input, schema) {
325
+ parseRobustJSON(input, schema, eventLogger, emailId) {
223
326
  try {
224
- const jsonMatch = input.match(/\{[\s\S]*\}/);
225
- const jsonStr = jsonMatch ? jsonMatch[0] : input;
226
- const cleaned = jsonStr.replace(/<\|[\s\S]*?\|>/g, '').replace(/```json/g, '').replace(/```/g, '').trim();
327
+ // 1. Remove common LLM artifacts and markdown blocks
328
+ let cleaned = input.trim();
329
+ // Handle markdown blocks
330
+ if (cleaned.includes('```json')) {
331
+ cleaned = cleaned.split('```json')[1].split('```')[0].trim();
332
+ }
333
+ else if (cleaned.includes('```')) {
334
+ cleaned = cleaned.split('```')[1].split('```')[0].trim();
335
+ }
336
+ // 2. Extract the first { ... } block if visible
337
+ const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
338
+ if (jsonMatch) {
339
+ cleaned = jsonMatch[0];
340
+ }
341
+ // 3. Strip aggressive local LLM tokens
342
+ cleaned = cleaned.replace(/<\|[\s\S]*?\|>/g, '').trim();
343
+ // 4. Parse and Normalize
227
344
  const parsed = JSON.parse(cleaned);
345
+ // Normalize actions_to_execute: convert string to array if needed
346
+ if (parsed && typeof parsed === 'object' && 'actions_to_execute' in parsed) {
347
+ if (typeof parsed.actions_to_execute === 'string') {
348
+ parsed.actions_to_execute = [parsed.actions_to_execute];
349
+ logger.debug('Normalized actions_to_execute from string to array', { original: parsed.actions_to_execute[0] });
350
+ }
351
+ }
352
+ // 5. Validate with Zod
228
353
  return schema.parse(parsed);
229
354
  }
230
355
  catch (e) {
356
+ logger.error('JSON Robust Parsing failed', { error: e.message, input: input.substring(0, 200) });
357
+ if (eventLogger && emailId) {
358
+ eventLogger.error('JSON Parse Error', {
359
+ error: e.message,
360
+ raw_input_preview: input.substring(0, 500)
361
+ }, emailId).catch(() => { });
362
+ }
231
363
  return null;
232
364
  }
233
365
  }
@@ -548,7 +548,10 @@ export class EmailProcessorService {
548
548
  smartDrafts: settings?.smart_drafts,
549
549
  },
550
550
  }, compiledContext || '', // Pre-compiled context (fast path)
551
- eventLogger || undefined, email.id);
551
+ eventLogger || undefined, email.id, {
552
+ llm_provider: settings?.llm_provider,
553
+ llm_model: settings?.llm_model
554
+ });
552
555
  if (!analysis) {
553
556
  throw new Error('AI analysis returned no result');
554
557
  }
@@ -650,7 +653,10 @@ export class EmailProcessorService {
650
653
  subject: email.subject || '',
651
654
  sender: email.sender || '',
652
655
  body: email.body_snippet || ''
653
- }, rule.instructions);
656
+ }, rule.instructions, {
657
+ llm_provider: settings?.llm_provider,
658
+ llm_model: settings?.llm_model
659
+ });
654
660
  if (customizedDraft) {
655
661
  draftContent = customizedDraft;
656
662
  }