@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.
- package/api/src/services/SDKService.ts +18 -7
- package/api/src/services/intelligence.ts +160 -22
- package/api/src/services/processor.ts +9 -2
- package/dist/api/src/services/SDKService.js +18 -7
- package/dist/api/src/services/intelligence.js +153 -21
- package/dist/api/src/services/processor.js +8 -2
- package/dist/assets/{index-BuWrl4UD.js → index-BgMWzGP9.js} +3 -3
- package/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
}
|