@realtimex/email-automator 2.20.0 → 2.21.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.
- package/api/src/config/index.ts +9 -8
- package/api/src/routes/drafts.ts +242 -0
- package/api/src/routes/index.ts +2 -0
- package/api/src/services/gmail.ts +66 -3
- package/api/src/services/intelligence.ts +133 -17
- package/api/src/services/learning.ts +273 -0
- package/api/src/services/microsoft.ts +64 -1
- package/api/src/services/processor.ts +139 -8
- package/api/src/services/scheduler.ts +44 -0
- package/api/src/services/supabase.ts +73 -3
- package/api/src/utils/nameExtraction.ts +136 -0
- package/api/src/utils/senderValidation.ts +143 -0
- package/dist/api/src/config/index.js +7 -6
- package/dist/api/src/routes/drafts.js +187 -0
- package/dist/api/src/routes/index.js +2 -0
- package/dist/api/src/services/gmail.js +53 -2
- package/dist/api/src/services/intelligence.js +114 -17
- package/dist/api/src/services/learning.js +231 -0
- package/dist/api/src/services/microsoft.js +42 -0
- package/dist/api/src/services/processor.js +115 -9
- package/dist/api/src/services/scheduler.js +40 -0
- package/dist/api/src/services/supabase.js +2 -2
- package/dist/api/src/utils/nameExtraction.js +109 -0
- package/dist/api/src/utils/senderValidation.js +120 -0
- package/dist/assets/{es-jMIxKu3w.js → es-D3VfHd3p.js} +1 -1
- package/dist/assets/{fr-C2DJGnGk.js → fr-BCaT2FDG.js} +1 -1
- package/dist/assets/index-BNIm2wsr.js +130 -0
- package/dist/assets/index-DwCPoGUC.css +1 -0
- package/dist/assets/{ja-BAbHgg3S.js → ja-46h-LNuz.js} +1 -1
- package/dist/assets/{ko-BqELMO5T.js → ko-D7KXrzVC.js} +1 -1
- package/dist/assets/{vi-D_3oX_6P.js → vi-C3rVsi7-.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/cleanup-invalid-drafts.sql +42 -0
- package/supabase/functions/api-v1-drafts/index.ts +34 -13
- package/supabase/functions/learning-daily-job/index.ts +122 -0
- package/supabase/migrations/20260202000001_add_persona_to_settings.sql +34 -0
- package/supabase/migrations/20260202000002_create_user_feedback.sql +50 -0
- package/supabase/migrations/20260203000000_phase4_foundations.sql +53 -0
- package/dist/assets/index-Czj6FLF_.js +0 -105
- package/dist/assets/index-dbXCkqFe.css +0 -1
- package/supabase/functions/_shared/gmail-service.ts +0 -4
- package/supabase/functions/_shared/microsoft-service.ts +0 -4
package/api/src/config/index.ts
CHANGED
|
@@ -48,10 +48,10 @@ function parseArgs(args: string[]): { port: number | null, noUi: boolean, rename
|
|
|
48
48
|
port = p;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
const noUi = args.includes('--no-ui');
|
|
53
53
|
const rename = args.includes('--rename');
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
return { port, noUi, rename };
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -120,12 +120,13 @@ export const config = {
|
|
|
120
120
|
export function validateConfig(): { valid: boolean; errors: string[] } {
|
|
121
121
|
const errors: string[] = [];
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
// For BYOK (Bring Your Own Key) apps, .env is optional
|
|
124
|
+
// if (!config.supabase.url) {
|
|
125
|
+
// errors.push('SUPABASE_URL is required');
|
|
126
|
+
// }
|
|
127
|
+
// if (!config.supabase.anonKey) {
|
|
128
|
+
// errors.push('SUPABASE_ANON_KEY is required');
|
|
129
|
+
// }
|
|
129
130
|
if (config.isProduction && config.security.jwtSecret === 'dev-secret-change-in-production') {
|
|
130
131
|
errors.push('JWT_SECRET must be set in production');
|
|
131
132
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { asyncHandler, NotFoundError } from '../middleware/errorHandler.js';
|
|
3
|
+
import { authMiddleware } from '../middleware/auth.js';
|
|
4
|
+
import { apiRateLimit } from '../middleware/rateLimit.js';
|
|
5
|
+
import { getGmailService } from '../services/gmail.js';
|
|
6
|
+
import { getMicrosoftService } from '../services/microsoft.js';
|
|
7
|
+
import { createLogger } from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
const router = Router();
|
|
10
|
+
const logger = createLogger('DraftsRoutes');
|
|
11
|
+
|
|
12
|
+
// List drafts
|
|
13
|
+
router.get('/',
|
|
14
|
+
apiRateLimit,
|
|
15
|
+
authMiddleware,
|
|
16
|
+
asyncHandler(async (req, res) => {
|
|
17
|
+
const {
|
|
18
|
+
limit = '100',
|
|
19
|
+
offset = '0',
|
|
20
|
+
status = 'pending',
|
|
21
|
+
account_id
|
|
22
|
+
} = req.query;
|
|
23
|
+
|
|
24
|
+
let query = req.supabase!
|
|
25
|
+
.from('emails')
|
|
26
|
+
.select(`
|
|
27
|
+
id, subject, sender, recipient, date,
|
|
28
|
+
draft_status, draft_created_at, draft_content, ai_analysis,
|
|
29
|
+
account_id, external_id, draft_id,
|
|
30
|
+
email_accounts!inner(id, user_id, email_address, provider)
|
|
31
|
+
`, { count: 'exact' })
|
|
32
|
+
.eq('email_accounts.user_id', req.user!.id)
|
|
33
|
+
.eq('draft_status', status)
|
|
34
|
+
.order('draft_created_at', { ascending: false })
|
|
35
|
+
.range(
|
|
36
|
+
parseInt(offset as string, 10),
|
|
37
|
+
parseInt(offset as string, 10) + parseInt(limit as string, 10) - 1
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (account_id) {
|
|
41
|
+
query = query.eq('account_id', account_id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { data, error, count } = await query;
|
|
45
|
+
|
|
46
|
+
if (error) throw error;
|
|
47
|
+
|
|
48
|
+
// Debug: Log draft content structure for first few emails
|
|
49
|
+
if (data && data.length > 0) {
|
|
50
|
+
logger.info(`Found ${data.length} drafts with draft_status='${status}'`);
|
|
51
|
+
data.slice(0, 3).forEach((email, idx) => {
|
|
52
|
+
const aiAnalysis = email.ai_analysis as any;
|
|
53
|
+
logger.debug(`Draft ${idx + 1} content check`, {
|
|
54
|
+
emailId: email.id,
|
|
55
|
+
hasDraftContent: !!email.draft_content,
|
|
56
|
+
hasAiAnalysis: !!email.ai_analysis,
|
|
57
|
+
aiAnalysisKeys: email.ai_analysis ? Object.keys(email.ai_analysis) : [],
|
|
58
|
+
hasDraftResponse: !!aiAnalysis?.draft_response,
|
|
59
|
+
hasDraftContentInAi: !!aiAnalysis?.draft_content,
|
|
60
|
+
draftResponseLength: aiAnalysis?.draft_response?.length || 0,
|
|
61
|
+
draftContentLength: aiAnalysis?.draft_content?.length || 0,
|
|
62
|
+
persistedDraftLength: email.draft_content?.length || 0
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Filter out drafts without content (these can't be sent)
|
|
68
|
+
const validDrafts = (data || []).filter(email => {
|
|
69
|
+
const aiAnalysis = email.ai_analysis as any;
|
|
70
|
+
const hasContent = email.draft_content || aiAnalysis?.draft_response || aiAnalysis?.draft_content;
|
|
71
|
+
|
|
72
|
+
if (!hasContent) {
|
|
73
|
+
logger.debug('Draft without content filtered out', {
|
|
74
|
+
emailId: email.id,
|
|
75
|
+
subject: email.subject,
|
|
76
|
+
hasAiAnalysis: !!email.ai_analysis,
|
|
77
|
+
aiAnalysisKeys: email.ai_analysis ? Object.keys(email.ai_analysis) : []
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return hasContent;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
logger.info(`Returning ${validDrafts.length} valid drafts out of ${data?.length || 0} total`);
|
|
85
|
+
|
|
86
|
+
res.json({
|
|
87
|
+
drafts: validDrafts,
|
|
88
|
+
total: validDrafts.length
|
|
89
|
+
});
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Send draft
|
|
94
|
+
router.post('/:emailId/send',
|
|
95
|
+
apiRateLimit,
|
|
96
|
+
authMiddleware,
|
|
97
|
+
asyncHandler(async (req, res) => {
|
|
98
|
+
const { emailId } = req.params;
|
|
99
|
+
const userId = req.user!.id;
|
|
100
|
+
|
|
101
|
+
// Fetch email with account info and SECURITY CHECK in query
|
|
102
|
+
const { data: email, error } = await req.supabase!
|
|
103
|
+
.from('emails')
|
|
104
|
+
.select('*, email_accounts!inner(*)')
|
|
105
|
+
.eq('id', emailId)
|
|
106
|
+
.eq('email_accounts.user_id', userId) // Security: Ensure user owns the account
|
|
107
|
+
.single();
|
|
108
|
+
|
|
109
|
+
if (error || !email) {
|
|
110
|
+
throw new NotFoundError('Email');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// if (email.email_accounts.user_id !== userId) {
|
|
114
|
+
// throw new NotFoundError('Email');
|
|
115
|
+
// }
|
|
116
|
+
|
|
117
|
+
const account = email.email_accounts;
|
|
118
|
+
// CRITICAL: Priority Definition
|
|
119
|
+
// 1. draft_content (User edits / Persisted draft) overrides everything
|
|
120
|
+
// 2. ai_analysis.draft_response (EmailAnalysisSchema)
|
|
121
|
+
// 3. ai_analysis.draft_content (ContextAwareAnalysisSchema)
|
|
122
|
+
const aiAnalysis = email.ai_analysis as any;
|
|
123
|
+
const draftContent = email.draft_content ?? aiAnalysis?.draft_response ?? aiAnalysis?.draft_content;
|
|
124
|
+
|
|
125
|
+
logger.debug('Sending draft', {
|
|
126
|
+
emailId,
|
|
127
|
+
hasDraftContent: !!email.draft_content,
|
|
128
|
+
hasAiDraftResponse: !!aiAnalysis?.draft_response,
|
|
129
|
+
hasAiDraftContent: !!aiAnalysis?.draft_content,
|
|
130
|
+
contentLength: draftContent?.length || 0,
|
|
131
|
+
analysisKeys: email.ai_analysis ? Object.keys(email.ai_analysis) : []
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!draftContent) {
|
|
135
|
+
logger.warn('Draft content missing', {
|
|
136
|
+
emailId,
|
|
137
|
+
draft_content: email.draft_content,
|
|
138
|
+
ai_draft_response: aiAnalysis?.draft_response,
|
|
139
|
+
ai_draft_content: aiAnalysis?.draft_content,
|
|
140
|
+
analysisKeys: email.ai_analysis ? Object.keys(email.ai_analysis) : []
|
|
141
|
+
});
|
|
142
|
+
return res.status(400).json({
|
|
143
|
+
error: 'No draft content found for this email',
|
|
144
|
+
details: 'No draft found in draft_content, ai_analysis.draft_response, or ai_analysis.draft_content fields'
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let sentMessageId: string | null = null;
|
|
149
|
+
const replyToId = email.external_id;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// Priority: Send existing draft if ID exists (cleaner, preserves original draft object)
|
|
153
|
+
if (email.draft_id) {
|
|
154
|
+
if (account.provider === 'gmail') {
|
|
155
|
+
const gmailService = getGmailService();
|
|
156
|
+
sentMessageId = await gmailService.sendDraft(account, email.draft_id);
|
|
157
|
+
} else if (account.provider === 'outlook') {
|
|
158
|
+
const microsoftService = getMicrosoftService();
|
|
159
|
+
sentMessageId = await microsoftService.sendDraft(account, email.draft_id);
|
|
160
|
+
}
|
|
161
|
+
logger.info('Sent existing draft', { draftId: email.draft_id, sentMessageId });
|
|
162
|
+
}
|
|
163
|
+
// Fallback: Create new reply using content (if draft object was deleted externally or not saved)
|
|
164
|
+
else {
|
|
165
|
+
if (account.provider === 'gmail') {
|
|
166
|
+
const gmailService = getGmailService();
|
|
167
|
+
// Send reply
|
|
168
|
+
sentMessageId = await gmailService.sendReply(
|
|
169
|
+
account,
|
|
170
|
+
replyToId,
|
|
171
|
+
draftContent,
|
|
172
|
+
email.subject || ''
|
|
173
|
+
);
|
|
174
|
+
} else if (account.provider === 'outlook') {
|
|
175
|
+
const microsoftService = getMicrosoftService();
|
|
176
|
+
sentMessageId = await microsoftService.sendReply(
|
|
177
|
+
account,
|
|
178
|
+
replyToId,
|
|
179
|
+
draftContent
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
logger.info('Sent draft via reply', { sentMessageId });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Update email status
|
|
186
|
+
await req.supabase!
|
|
187
|
+
.from('emails')
|
|
188
|
+
.update({
|
|
189
|
+
draft_status: 'sent',
|
|
190
|
+
draft_sent_at: new Date().toISOString(),
|
|
191
|
+
action_taken: 'reply', // Legacy compatibility
|
|
192
|
+
actions_taken: ['reply']
|
|
193
|
+
})
|
|
194
|
+
.eq('id', emailId);
|
|
195
|
+
|
|
196
|
+
res.json({ success: true, messageId: sentMessageId || email.draft_id });
|
|
197
|
+
|
|
198
|
+
} catch (error: any) {
|
|
199
|
+
logger.error('Failed to send draft', error, { emailId });
|
|
200
|
+
res.status(500).json({
|
|
201
|
+
error: 'Failed to send draft',
|
|
202
|
+
details: error.message
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Dismiss draft
|
|
209
|
+
router.post('/:emailId/dismiss',
|
|
210
|
+
apiRateLimit,
|
|
211
|
+
authMiddleware,
|
|
212
|
+
asyncHandler(async (req, res) => {
|
|
213
|
+
const { emailId } = req.params;
|
|
214
|
+
|
|
215
|
+
// Verify ownership
|
|
216
|
+
const { data: email, error } = await req.supabase!
|
|
217
|
+
.from('emails')
|
|
218
|
+
.select('id, email_accounts!inner(user_id)')
|
|
219
|
+
.eq('id', emailId)
|
|
220
|
+
.eq('email_accounts.user_id', req.user!.id)
|
|
221
|
+
.single();
|
|
222
|
+
|
|
223
|
+
if (error || !email) {
|
|
224
|
+
throw new NotFoundError('Email');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Update status to dismissed
|
|
228
|
+
const { error: updateError } = await req.supabase!
|
|
229
|
+
.from('emails')
|
|
230
|
+
.update({
|
|
231
|
+
draft_status: 'dismissed',
|
|
232
|
+
draft_dismissed_at: new Date().toISOString()
|
|
233
|
+
})
|
|
234
|
+
.eq('id', emailId);
|
|
235
|
+
|
|
236
|
+
if (updateError) throw updateError;
|
|
237
|
+
|
|
238
|
+
res.json({ success: true });
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
export default router;
|
package/api/src/routes/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import emailsRoutes from './emails.js';
|
|
|
9
9
|
import migrateRoutes from './migrate.js';
|
|
10
10
|
import sdkRoutes from './sdk.js';
|
|
11
11
|
import rulePacksRoutes from './rulePacks.js';
|
|
12
|
+
import draftsRoutes from './drafts.js';
|
|
12
13
|
|
|
13
14
|
const router = Router();
|
|
14
15
|
|
|
@@ -22,5 +23,6 @@ router.use('/emails', emailsRoutes);
|
|
|
22
23
|
router.use('/migrate', migrateRoutes);
|
|
23
24
|
router.use('/sdk', sdkRoutes);
|
|
24
25
|
router.use('/rule-packs', rulePacksRoutes);
|
|
26
|
+
router.use('/drafts', draftsRoutes);
|
|
25
27
|
|
|
26
28
|
export default router;
|
|
@@ -413,24 +413,87 @@ export class GmailService {
|
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
async sendDraft(account: EmailAccount, draftId: string): Promise<
|
|
416
|
+
async sendDraft(account: EmailAccount, draftId: string): Promise<string> {
|
|
417
417
|
const gmail = await this.getAuthenticatedClient(account);
|
|
418
418
|
|
|
419
419
|
try {
|
|
420
|
-
await gmail.users.drafts.send({
|
|
420
|
+
const response = await gmail.users.drafts.send({
|
|
421
421
|
userId: 'me',
|
|
422
422
|
requestBody: {
|
|
423
423
|
id: draftId,
|
|
424
424
|
},
|
|
425
425
|
});
|
|
426
426
|
|
|
427
|
-
|
|
427
|
+
const sentMessageId = response.data.id || draftId; // Fallback to draftId if no DB ID returned (rare)
|
|
428
|
+
logger.info('Draft sent successfully', { draftId, sentMessageId });
|
|
429
|
+
return sentMessageId;
|
|
428
430
|
} catch (error) {
|
|
429
431
|
logger.error('Gmail API Error sending draft', error);
|
|
430
432
|
throw error;
|
|
431
433
|
}
|
|
432
434
|
}
|
|
433
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Send a reply to a thread (used for sending drafts directly without creating a draft object first)
|
|
438
|
+
*/
|
|
439
|
+
async sendReply(
|
|
440
|
+
account: EmailAccount,
|
|
441
|
+
originalMessageId: string,
|
|
442
|
+
replyContent: string,
|
|
443
|
+
subject: string
|
|
444
|
+
): Promise<string> {
|
|
445
|
+
const gmail = await this.getAuthenticatedClient(account);
|
|
446
|
+
|
|
447
|
+
//Fetch original to get threadId and Message-ID
|
|
448
|
+
const original = await gmail.users.messages.get({ userId: 'me', id: originalMessageId });
|
|
449
|
+
const headers = original.data.payload?.headers || [];
|
|
450
|
+
const getHeader = (name: string) => headers.find(h => h.name?.toLowerCase() === name.toLowerCase())?.value || '';
|
|
451
|
+
|
|
452
|
+
const toAddress = getHeader('From');
|
|
453
|
+
const originalMsgId = getHeader('Message-ID');
|
|
454
|
+
const threadId = original.data.threadId;
|
|
455
|
+
|
|
456
|
+
// Threading headers
|
|
457
|
+
const replyHeaders = [];
|
|
458
|
+
if (originalMsgId) {
|
|
459
|
+
replyHeaders.push(`In-Reply-To: ${originalMsgId}`);
|
|
460
|
+
replyHeaders.push(`References: ${originalMsgId}`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const rawMessage = [
|
|
464
|
+
`To: ${toAddress}`,
|
|
465
|
+
`Subject: ${subject}`,
|
|
466
|
+
...replyHeaders,
|
|
467
|
+
'MIME-Version: 1.0',
|
|
468
|
+
'Content-Type: text/plain; charset="UTF-8"',
|
|
469
|
+
'',
|
|
470
|
+
replyContent,
|
|
471
|
+
].join('\r\n');
|
|
472
|
+
|
|
473
|
+
const encodedMessage = Buffer.from(rawMessage)
|
|
474
|
+
.toString('base64')
|
|
475
|
+
.replace(/\+/g, '-')
|
|
476
|
+
.replace(/\//g, '_')
|
|
477
|
+
.replace(/=+$/, '');
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
const result = await gmail.users.messages.send({
|
|
481
|
+
userId: 'me',
|
|
482
|
+
requestBody: {
|
|
483
|
+
threadId,
|
|
484
|
+
raw: encodedMessage,
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const messageId = result.data.id || 'unknown';
|
|
489
|
+
logger.info('Reply sent successfully', { messageId, threadId });
|
|
490
|
+
return messageId;
|
|
491
|
+
} catch (error) {
|
|
492
|
+
logger.error('Gmail API Error sending reply', error);
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
434
497
|
|
|
435
498
|
async addLabel(account: EmailAccount, messageId: string, labelIds: string[]): Promise<void> {
|
|
436
499
|
const gmail = await this.getAuthenticatedClient(account);
|
|
@@ -84,6 +84,9 @@ export interface EmailContext {
|
|
|
84
84
|
userPreferences?: {
|
|
85
85
|
autoTrashSpam?: boolean;
|
|
86
86
|
smartDrafts?: boolean;
|
|
87
|
+
categoryPatterns?: Record<string, string>;
|
|
88
|
+
vipSenders?: string[];
|
|
89
|
+
preferredLength?: number;
|
|
87
90
|
};
|
|
88
91
|
}
|
|
89
92
|
|
|
@@ -121,8 +124,58 @@ export class IntelligenceService {
|
|
|
121
124
|
if (context.metadata?.autoSubmitted && context.metadata.autoSubmitted !== 'no') metadataSignals.push(`- Auto-Submitted: ${context.metadata.autoSubmitted}`);
|
|
122
125
|
if (context.metadata?.importance) metadataSignals.push(`- Priority: ${context.metadata.importance}`);
|
|
123
126
|
|
|
127
|
+
// Adaptive Learning Signals
|
|
128
|
+
if (context.userPreferences?.vipSenders?.includes(context.sender)) {
|
|
129
|
+
metadataSignals.push('- SENDER IS VIP (High Priority Required)');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const senderDomain = context.sender.split('@')[1];
|
|
133
|
+
if (senderDomain && context.userPreferences?.categoryPatterns?.[senderDomain]) {
|
|
134
|
+
const learnedCategory = context.userPreferences.categoryPatterns[senderDomain];
|
|
135
|
+
metadataSignals.push(`- LEARNED PATTERN: Sender domain '${senderDomain}' is strictly category '${learnedCategory}'`);
|
|
136
|
+
}
|
|
137
|
+
|
|
124
138
|
const systemPrompt = `You are an AI Email Assistant. Analyze the email and return structured JSON.
|
|
125
|
-
|
|
139
|
+
|
|
140
|
+
CATEGORY DEFINITIONS:
|
|
141
|
+
- spam: Unwanted/unsolicited bulk email, phishing attempts
|
|
142
|
+
- newsletter: Recurring subscription content (weekly digests, company updates) - check for List-Unsubscribe
|
|
143
|
+
- news: Breaking news alerts, timely notifications (Google Alerts, news feeds)
|
|
144
|
+
- promotional: Marketing emails, sales offers, advertisements
|
|
145
|
+
- transactional: Receipts, confirmations, order updates, account notifications
|
|
146
|
+
- social: Social media notifications (LinkedIn, Twitter, Facebook)
|
|
147
|
+
- support: Customer service, help desk, support tickets
|
|
148
|
+
- client: Business correspondence from clients/customers (High Importance)
|
|
149
|
+
- internal: Company-internal communications (colleagues, HR, IT)
|
|
150
|
+
- personal: Personal correspondence from friends/family
|
|
151
|
+
- notification: Platform alerts/notifications (Github, Linear, etc) - distinct from social
|
|
152
|
+
- other: Anything that doesn't fit above categories
|
|
153
|
+
|
|
154
|
+
CRITICAL RULES:
|
|
155
|
+
1. Platform notifications (linkedin.com, github.com) are ALWAYS "notification" or "social", never "personal"
|
|
156
|
+
2. Emails from noreply@, no-reply@ are likely "transactional" or "notification"
|
|
157
|
+
3. Weekly/Monthly digests are "newsletter"
|
|
158
|
+
4. If "List-Unsubscribe" header is present, it is likely "newsletter" or "promotional"
|
|
159
|
+
5. Follow "LEARNED PATTERN" signals strictly if present
|
|
160
|
+
6. VIP Senders must be "High" priority unless irrelevant (e.g. OOO auto-reply)
|
|
161
|
+
|
|
162
|
+
FEW-SHOT EXAMPLES:
|
|
163
|
+
|
|
164
|
+
Example 1: LinkedIn Connection
|
|
165
|
+
Subject: Canh Le wants to connect
|
|
166
|
+
From: messages-noreply@linkedin.com
|
|
167
|
+
Signals: [List-Unsubscribe]
|
|
168
|
+
-> { "category": "social", "is_useless": true, "priority": "Low", "suggested_actions": ["archive"] }
|
|
169
|
+
|
|
170
|
+
Example 2: Cold Sales
|
|
171
|
+
Subject: Boost your productivity
|
|
172
|
+
From: sales@unknown-vendor.com
|
|
173
|
+
-> { "category": "spam", "is_useless": true, "priority": "Low", "suggested_actions": ["delete"] }
|
|
174
|
+
|
|
175
|
+
Example 3: Client Question
|
|
176
|
+
Subject: Question about the contract
|
|
177
|
+
From: client@valued-customer.com
|
|
178
|
+
-> { "category": "client", "priority": "High", "is_useless": false, "suggested_actions": ["reply", "flag"] }
|
|
126
179
|
|
|
127
180
|
Context:
|
|
128
181
|
- Subject: ${context.subject}
|
|
@@ -220,6 +273,15 @@ REQUIRED JSON STRUCTURE:
|
|
|
220
273
|
myName?: string;
|
|
221
274
|
myRole?: string;
|
|
222
275
|
myCompany?: string;
|
|
276
|
+
myIndustry?: string;
|
|
277
|
+
workStyle?: 'corporate' | 'startup' | 'creative' | 'academic';
|
|
278
|
+
communicationStyle?: {
|
|
279
|
+
tone?: string;
|
|
280
|
+
length?: string;
|
|
281
|
+
signature?: string;
|
|
282
|
+
commonPhrases?: string[];
|
|
283
|
+
};
|
|
284
|
+
primaryGoal?: string;
|
|
223
285
|
|
|
224
286
|
// Email analysis metadata
|
|
225
287
|
category?: string;
|
|
@@ -254,6 +316,9 @@ REQUIRED JSON STRUCTURE:
|
|
|
254
316
|
if (richContext.myCompany) {
|
|
255
317
|
systemPrompt += ` at ${richContext.myCompany}`;
|
|
256
318
|
}
|
|
319
|
+
if (richContext.myIndustry) {
|
|
320
|
+
systemPrompt += ` (${richContext.myIndustry} industry)`;
|
|
321
|
+
}
|
|
257
322
|
systemPrompt += '.';
|
|
258
323
|
}
|
|
259
324
|
|
|
@@ -277,7 +342,55 @@ REQUIRED JSON STRUCTURE:
|
|
|
277
342
|
systemPrompt += `\n\nIMPORTANT: The incoming email is written in ${richContext.language}. You MUST write your reply in ${richContext.language}. Maintain appropriate formality and cultural conventions for ${richContext.language}.`;
|
|
278
343
|
}
|
|
279
344
|
|
|
280
|
-
|
|
345
|
+
// Add persona-specific instructions
|
|
346
|
+
if (richContext?.workStyle) {
|
|
347
|
+
const styles = {
|
|
348
|
+
corporate: "Maintain a formal, structured, and polite tone.",
|
|
349
|
+
startup: "Be direct, concise, and action-oriented. Avoid fluff.",
|
|
350
|
+
creative: "Be expressive, flexible, and approachable.",
|
|
351
|
+
academic: "Be thorough, precise, and formal."
|
|
352
|
+
};
|
|
353
|
+
systemPrompt += `\nYour work style is: ${richContext.workStyle}. ${styles[richContext.workStyle as keyof typeof styles] || ''}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Communication Preferences
|
|
357
|
+
if (richContext?.communicationStyle) {
|
|
358
|
+
const { tone, length, commonPhrases } = richContext.communicationStyle;
|
|
359
|
+
|
|
360
|
+
if (tone) systemPrompt += `\nPreferred Tone: ${tone}.`;
|
|
361
|
+
|
|
362
|
+
if (length) {
|
|
363
|
+
const lengths = {
|
|
364
|
+
brief: "Keep the reply very short (1-2 sentences).",
|
|
365
|
+
medium: "Keep the reply distinct and focused (2-3 paragraphs max).",
|
|
366
|
+
detailed: "Provide a comprehensive and detailed response."
|
|
367
|
+
};
|
|
368
|
+
systemPrompt += `\nResponse Length: ${lengths[length as keyof typeof lengths] || 'Medium'}.`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (commonPhrases && commonPhrases.length > 0) {
|
|
372
|
+
systemPrompt += `\nincorporate these phrases if natural: ${commonPhrases.join(', ')}.`;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Goal Alignment
|
|
377
|
+
if (richContext?.primaryGoal) {
|
|
378
|
+
const goals = {
|
|
379
|
+
inbox_zero: "Goal: Clear the inbox. Resolve efficiently.",
|
|
380
|
+
respond_faster: "Goal: Quick acknowledgment or resolution.",
|
|
381
|
+
focus: "Goal: Protect user's time. Defer low-priority items.",
|
|
382
|
+
reduce_time: "Goal: Minimal user editing required. Draft ready-to-send."
|
|
383
|
+
};
|
|
384
|
+
systemPrompt += `\n${goals[richContext.primaryGoal as keyof typeof goals] || ''}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
systemPrompt += '\n\nWrite ONLY the email body (no subject line). Match the tone of the incoming email unless overridden by preferences.';
|
|
388
|
+
systemPrompt += '\n\nSTYLE GUIDELINES:';
|
|
389
|
+
systemPrompt += '\n- Start directly (e.g., "Thanks for the update" or "Hi [Name],")';
|
|
390
|
+
systemPrompt += '\n- NEVER use robotic phrases like "I hope this email finds you well" or "I am writing to you today"';
|
|
391
|
+
systemPrompt += '\n- Avoid "Please let me know if you have any questions" unless necessary - just say "Let me know if you need anything else"';
|
|
392
|
+
systemPrompt += '\n- Keep it under 150 words unless detail is explicitly requested';
|
|
393
|
+
systemPrompt += '\n- Use active voice';
|
|
281
394
|
|
|
282
395
|
// Build user message with email context
|
|
283
396
|
let userMessage = '';
|
|
@@ -357,13 +470,27 @@ REQUIRED JSON STRUCTURE:
|
|
|
357
470
|
|
|
358
471
|
const systemPrompt = `You are an AI Automation Agent. Analyze the email and identify ALL rules that apply.
|
|
359
472
|
|
|
473
|
+
CATEGORY DEFINITIONS:
|
|
474
|
+
- spam: Unwanted/unsolicited bulk email, phishing attempts
|
|
475
|
+
- newsletter: Recurring subscription content (weekly digests, company updates) - check for List-Unsubscribe
|
|
476
|
+
- news: Breaking news alerts, timely notifications (Google Alerts, news feeds)
|
|
477
|
+
- promotional: Marketing emails, sales offers, advertisements
|
|
478
|
+
- transactional: Receipts, confirmations, order updates, account notifications
|
|
479
|
+
- social: Social media notifications (LinkedIn, Twitter, Facebook)
|
|
480
|
+
- support: Customer service, help desk, support tickets
|
|
481
|
+
- client: Business correspondence from clients/customers (High Importance)
|
|
482
|
+
- internal: Company-internal communications (colleagues, HR, IT)
|
|
483
|
+
- personal: Personal correspondence from friends/family
|
|
484
|
+
- notification: Platform alerts/notifications (Github, Linear, etc) - distinct from social
|
|
485
|
+
- other: Anything that doesn't fit above categories
|
|
486
|
+
|
|
360
487
|
Rules Context:
|
|
361
488
|
${rulesContext}
|
|
362
489
|
|
|
363
490
|
REQUIRED JSON STRUCTURE:
|
|
364
491
|
{
|
|
365
492
|
"summary": "A brief summary of the email content",
|
|
366
|
-
"category": "spam|newsletter|news|promotional|transactional|social|support|client|internal|personal|other",
|
|
493
|
+
"category": "spam|newsletter|news|promotional|transactional|social|support|client|internal|personal|notification|other",
|
|
367
494
|
"priority": "High|Medium|Low",
|
|
368
495
|
"matched_rules": [
|
|
369
496
|
{
|
|
@@ -377,26 +504,15 @@ REQUIRED JSON STRUCTURE:
|
|
|
377
504
|
"draft_content": "Suggested reply if drafting, otherwise null"
|
|
378
505
|
}
|
|
379
506
|
|
|
380
|
-
CATEGORY DEFINITIONS (choose the most specific):
|
|
381
|
-
- spam: Unwanted/unsolicited bulk email, phishing attempts
|
|
382
|
-
- newsletter: Recurring subscription content (weekly digests, company updates)
|
|
383
|
-
- news: Breaking news alerts, timely notifications, one-off news items (Google Alerts, news feeds)
|
|
384
|
-
- promotional: Marketing emails, sales offers, advertisements
|
|
385
|
-
- transactional: Receipts, confirmations, order updates, account notifications
|
|
386
|
-
- social: Social media notifications (LinkedIn, Twitter, Facebook)
|
|
387
|
-
- support: Customer service, help desk, support tickets
|
|
388
|
-
- client: Business correspondence from clients/customers
|
|
389
|
-
- internal: Company-internal communications (colleagues, HR, IT)
|
|
390
|
-
- personal: Personal correspondence from friends/family
|
|
391
|
-
- other: Anything that doesn't fit above categories
|
|
392
|
-
|
|
393
507
|
CRITICAL INSTRUCTIONS:
|
|
394
508
|
- Identify ALL rules that apply to this email (not just the best one)
|
|
395
509
|
- Return an empty array if no rules match
|
|
396
510
|
- Only include rules with confidence >= 0.7
|
|
397
511
|
- For each matched rule, explain why it applies
|
|
398
512
|
- Actions will be merged by the system - you don't need to resolve conflicts
|
|
399
|
-
- Use "draft" action only if a rule explicitly requests it
|
|
513
|
+
- Use "draft" action only if a rule explicitly requests it
|
|
514
|
+
- Platform notifications (linkedin, github) are ALWAYS "notification" or "social"
|
|
515
|
+
- Emails from noreply@ are likely "transactional" or "notification"`;
|
|
400
516
|
|
|
401
517
|
if (eventLogger) {
|
|
402
518
|
await eventLogger.info('Thinking', `Context-aware analysis: ${context.subject}`, {
|