@realtimex/email-automator 2.23.10 → 2.24.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/middleware/validation.ts +21 -4
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/learning.ts +99 -0
- package/api/src/services/defaultRules/emailOrg.ts +1 -1
- package/api/src/services/defaultRules/types.ts +5 -0
- package/api/src/services/intelligence.ts +126 -4
- package/api/src/services/learning.ts +16 -4
- package/api/src/services/processor.ts +384 -78
- package/api/src/services/supabase.ts +13 -1
- package/api/src/utils/emailHeaders.ts +292 -0
- package/dist/api/src/middleware/validation.js +21 -4
- package/dist/api/src/routes/index.js +2 -0
- package/dist/api/src/routes/learning.js +86 -0
- package/dist/api/src/services/defaultRules/emailOrg.js +1 -1
- package/dist/api/src/services/intelligence.js +140 -4
- package/dist/api/src/services/learning.js +11 -4
- package/dist/api/src/services/processor.js +488 -202
- package/dist/api/src/utils/emailHeaders.js +238 -0
- package/dist/assets/index-FRMyxVih.js +217 -0
- package/dist/assets/index-otwjpYTB.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/supabase/migrations/20260206000012_add_email_headers.sql +29 -0
- package/supabase/migrations/20260206000013_add_negative_conditions.sql +15 -0
- package/supabase/migrations/20260206000014_add_rule_confidence.sql +15 -0
- package/supabase/migrations/20260206000015_add_negative_conditions_to_templates.sql +73 -0
- package/supabase/migrations/20260206000016_update_init_function_negative_conditions.sql +104 -0
- package/supabase/migrations/20260206000017_backfill_negative_conditions_existing_users.sql +109 -0
- package/vite.config.ts +1 -1
- package/dist/assets/index-BLg38ak1.js +0 -217
- package/dist/assets/index-COvYx29q.css +0 -1
|
@@ -88,14 +88,23 @@ export const schemas = {
|
|
|
88
88
|
|
|
89
89
|
// Rule schemas - supports both single action (legacy) and actions array
|
|
90
90
|
// Now includes description and intent for context-aware AI matching
|
|
91
|
+
// Actions support: 'delete', 'archive', 'draft', 'star', 'read', or 'label:*' (e.g., 'label:Financial')
|
|
91
92
|
createRule: z.object({
|
|
92
93
|
name: z.string().min(1).max(100),
|
|
93
94
|
description: z.string().max(500).optional(),
|
|
94
95
|
intent: z.string().max(200).optional(),
|
|
95
96
|
priority: z.number().int().min(0).max(100).optional(),
|
|
96
97
|
condition: z.record(z.unknown()),
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
negative_condition: z.record(z.unknown()).optional(),
|
|
99
|
+
min_confidence: z.number().min(0).max(1).optional().default(0.7),
|
|
100
|
+
action: z.union([
|
|
101
|
+
z.enum(['delete', 'archive', 'draft', 'star', 'read']),
|
|
102
|
+
z.string().regex(/^label:.+/, 'Label actions must start with "label:"')
|
|
103
|
+
]).optional(),
|
|
104
|
+
actions: z.array(z.union([
|
|
105
|
+
z.enum(['delete', 'archive', 'draft', 'star', 'read']),
|
|
106
|
+
z.string().regex(/^label:.+/, 'Label actions must start with "label:"')
|
|
107
|
+
])).optional(),
|
|
99
108
|
instructions: z.string().optional(),
|
|
100
109
|
is_enabled: z.boolean().default(true),
|
|
101
110
|
}).refine(data => data.action || (data.actions && data.actions.length > 0), {
|
|
@@ -108,8 +117,16 @@ export const schemas = {
|
|
|
108
117
|
intent: z.string().max(200).optional(),
|
|
109
118
|
priority: z.number().int().min(0).max(100).optional(),
|
|
110
119
|
condition: z.record(z.unknown()).optional(),
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
negative_condition: z.record(z.unknown()).optional(),
|
|
121
|
+
min_confidence: z.number().min(0).max(1).optional(),
|
|
122
|
+
action: z.union([
|
|
123
|
+
z.enum(['delete', 'archive', 'draft', 'star', 'read']),
|
|
124
|
+
z.string().regex(/^label:.+/, 'Label actions must start with "label:"')
|
|
125
|
+
]).optional(),
|
|
126
|
+
actions: z.array(z.union([
|
|
127
|
+
z.enum(['delete', 'archive', 'draft', 'star', 'read']),
|
|
128
|
+
z.string().regex(/^label:.+/, 'Label actions must start with "label:"')
|
|
129
|
+
])).optional(),
|
|
113
130
|
instructions: z.string().optional(),
|
|
114
131
|
is_enabled: z.boolean().optional(),
|
|
115
132
|
}),
|
package/api/src/routes/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import ttsRoutes from './tts.js';
|
|
|
12
12
|
import agentRoutes from './agent.js';
|
|
13
13
|
import draftsRoutes from './drafts.js';
|
|
14
14
|
import attachmentsRoutes from './attachments.js';
|
|
15
|
+
import learningRoutes from './learning.js';
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
const router = Router();
|
|
@@ -30,6 +31,7 @@ router.use('/tts', ttsRoutes);
|
|
|
30
31
|
router.use('/agent', agentRoutes);
|
|
31
32
|
router.use('/drafts', draftsRoutes);
|
|
32
33
|
router.use('/drafts', attachmentsRoutes);
|
|
34
|
+
router.use('/learning', learningRoutes);
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
export default router;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import { authMiddleware } from '../middleware/auth.js';
|
|
3
|
+
import { asyncHandler } from '../middleware/errorHandler.js';
|
|
4
|
+
import { createLogger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
const logger = createLogger('LearningRoutes');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/learning/feedback
|
|
11
|
+
* Submit user feedback and immediately update metrics
|
|
12
|
+
*/
|
|
13
|
+
router.post('/feedback', authMiddleware, asyncHandler(async (req: Request, res: Response) => {
|
|
14
|
+
const supabase = req.supabase;
|
|
15
|
+
const user = req.user;
|
|
16
|
+
|
|
17
|
+
if (!supabase) {
|
|
18
|
+
return res.status(503).json({ error: 'Supabase client not configured' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!user) {
|
|
22
|
+
return res.status(401).json({ error: 'Not authenticated' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { email_id, feedback_type, original_state, corrected_state, reasoning } = req.body;
|
|
26
|
+
|
|
27
|
+
if (!email_id || !feedback_type) {
|
|
28
|
+
return res.status(400).json({ error: 'Missing required fields' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get account_id from email
|
|
32
|
+
const { data: email, error: emailError } = await supabase
|
|
33
|
+
.from('emails')
|
|
34
|
+
.select('account_id')
|
|
35
|
+
.eq('id', email_id)
|
|
36
|
+
.single();
|
|
37
|
+
|
|
38
|
+
if (emailError) {
|
|
39
|
+
logger.warn('Failed to get email account_id', { email_id, error: emailError });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 1. Insert feedback record
|
|
43
|
+
const { error: feedbackError } = await supabase
|
|
44
|
+
.from('user_feedback')
|
|
45
|
+
.insert({
|
|
46
|
+
user_id: user.id,
|
|
47
|
+
email_id,
|
|
48
|
+
account_id: email?.account_id || null, // Populate account_id
|
|
49
|
+
feedback_type,
|
|
50
|
+
original_data: original_state,
|
|
51
|
+
corrected_data: corrected_state,
|
|
52
|
+
reasoning: reasoning || null
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (feedbackError) {
|
|
56
|
+
logger.error('Failed to insert feedback', feedbackError);
|
|
57
|
+
return res.status(500).json({ error: 'Failed to submit feedback' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. Immediately update metrics if positive feedback
|
|
61
|
+
if (feedback_type === 'analysis' && corrected_state?.is_correct === true) {
|
|
62
|
+
const { data: metrics, error: fetchError } = await supabase
|
|
63
|
+
.from('learning_metrics')
|
|
64
|
+
.select('*')
|
|
65
|
+
.eq('user_id', user.id)
|
|
66
|
+
.maybeSingle();
|
|
67
|
+
|
|
68
|
+
if (fetchError && fetchError.code !== 'PGRST116') {
|
|
69
|
+
logger.error('Failed to fetch metrics', fetchError);
|
|
70
|
+
} else {
|
|
71
|
+
if (metrics) {
|
|
72
|
+
// Update existing
|
|
73
|
+
await supabase
|
|
74
|
+
.from('learning_metrics')
|
|
75
|
+
.update({
|
|
76
|
+
total_classifications: (metrics.total_classifications || 0) + 1,
|
|
77
|
+
correct_classifications: (metrics.correct_classifications || 0) + 1,
|
|
78
|
+
updated_at: new Date().toISOString()
|
|
79
|
+
})
|
|
80
|
+
.eq('user_id', user.id);
|
|
81
|
+
} else {
|
|
82
|
+
// Create new
|
|
83
|
+
await supabase
|
|
84
|
+
.from('learning_metrics')
|
|
85
|
+
.insert({
|
|
86
|
+
user_id: user.id,
|
|
87
|
+
total_classifications: 1,
|
|
88
|
+
correct_classifications: 1
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
logger.info('Updated metrics for positive feedback', { user_id: user.id });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
res.json({ success: true });
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
export default router;
|
|
@@ -59,7 +59,7 @@ export const EMAIL_ORG_RULES: DefaultRule[] = [
|
|
|
59
59
|
actions: ['label:Sales/Cold Outreach', 'draft'],
|
|
60
60
|
instructions: 'Politely acknowledge the outreach. If not interested, thank them and decline. If potentially interested, ask for more details.',
|
|
61
61
|
priority: 20,
|
|
62
|
-
is_enabled_by_default: true
|
|
62
|
+
is_enabled_by_default: true // ← RE-ENABLED: recipient_type now implemented!
|
|
63
63
|
},
|
|
64
64
|
|
|
65
65
|
// Rule 3: CC Organizer
|
|
@@ -59,6 +59,11 @@ export interface EnhancedRuleCondition {
|
|
|
59
59
|
recipient_count_gt?: number; // Number of recipients threshold
|
|
60
60
|
is_first_contact?: boolean; // No prior thread with sender
|
|
61
61
|
|
|
62
|
+
// Email header metadata (parsed during sync)
|
|
63
|
+
is_automated?: boolean; // Detected from List-Unsubscribe, Precedence:bulk, X-Mailer
|
|
64
|
+
has_unsubscribe?: boolean; // Contains List-Unsubscribe header
|
|
65
|
+
is_reply?: boolean; // Part of a reply thread (In-Reply-To/References headers)
|
|
66
|
+
|
|
62
67
|
// Content matching
|
|
63
68
|
contains_keywords?: string[]; // Any of these words (case-insensitive)
|
|
64
69
|
matches_pattern?: string; // Regex pattern
|
|
@@ -69,6 +69,8 @@ export interface RuleContext {
|
|
|
69
69
|
intent?: string;
|
|
70
70
|
actions: string[];
|
|
71
71
|
draft_instructions?: string;
|
|
72
|
+
condition?: any; // Positive condition
|
|
73
|
+
negative_condition?: any; // Negative condition (exclusion logic)
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
export interface EmailContext {
|
|
@@ -76,10 +78,18 @@ export interface EmailContext {
|
|
|
76
78
|
sender: string;
|
|
77
79
|
date: string;
|
|
78
80
|
metadata?: {
|
|
81
|
+
// Original metadata fields (deprecated but kept for compatibility)
|
|
79
82
|
importance?: string;
|
|
80
83
|
listUnsubscribe?: string;
|
|
81
84
|
autoSubmitted?: string;
|
|
82
85
|
mailer?: string;
|
|
86
|
+
// Enhanced header metadata for better LLM analysis
|
|
87
|
+
recipient_type?: 'to' | 'cc' | 'bcc';
|
|
88
|
+
is_automated?: boolean;
|
|
89
|
+
has_unsubscribe?: boolean;
|
|
90
|
+
is_reply?: boolean;
|
|
91
|
+
sender_priority?: 'high' | 'normal' | 'low';
|
|
92
|
+
thread_id?: string;
|
|
83
93
|
};
|
|
84
94
|
userPreferences?: {
|
|
85
95
|
autoTrashSpam?: boolean;
|
|
@@ -158,7 +168,37 @@ export class IntelligenceService {
|
|
|
158
168
|
const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
|
|
159
169
|
|
|
160
170
|
const metadataSignals = [];
|
|
161
|
-
|
|
171
|
+
|
|
172
|
+
// Email age signal (objective time context only)
|
|
173
|
+
if (context.date) {
|
|
174
|
+
const emailDate = new Date(context.date);
|
|
175
|
+
const now = new Date();
|
|
176
|
+
const ageMs = now.getTime() - emailDate.getTime();
|
|
177
|
+
const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
178
|
+
const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
|
|
179
|
+
|
|
180
|
+
if (ageDays === 0 && ageHours < 1) {
|
|
181
|
+
metadataSignals.push('- Email age: Less than 1 hour old');
|
|
182
|
+
} else if (ageDays === 0) {
|
|
183
|
+
metadataSignals.push(`- Email age: ${ageHours} hours old`);
|
|
184
|
+
} else if (ageDays === 1) {
|
|
185
|
+
metadataSignals.push('- Email age: 1 day old');
|
|
186
|
+
} else {
|
|
187
|
+
metadataSignals.push(`- Email age: ${ageDays} days old`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Header-based signals (enhanced)
|
|
192
|
+
if (context.metadata?.recipient_type === 'cc') metadataSignals.push('- Recipient: CC (not directly addressed)');
|
|
193
|
+
if (context.metadata?.recipient_type === 'bcc') metadataSignals.push('- Recipient: BCC (bulk/mass email)');
|
|
194
|
+
if (context.metadata?.is_automated) metadataSignals.push('- Automated/Bulk email detected (List-Unsubscribe or Precedence:bulk)');
|
|
195
|
+
if (context.metadata?.has_unsubscribe) metadataSignals.push('- Contains Unsubscribe header (likely newsletter/marketing)');
|
|
196
|
+
if (context.metadata?.is_reply) metadataSignals.push('- Part of reply thread (ongoing conversation)');
|
|
197
|
+
if (context.metadata?.sender_priority === 'high') metadataSignals.push('- Sender Priority: HIGH (marked as urgent)');
|
|
198
|
+
if (context.metadata?.sender_priority === 'low') metadataSignals.push('- Sender Priority: LOW');
|
|
199
|
+
if (context.metadata?.mailer) metadataSignals.push(`- Sent via: ${context.metadata.mailer}`);
|
|
200
|
+
// Legacy metadata (deprecated but kept for compatibility)
|
|
201
|
+
if (context.metadata?.listUnsubscribe && !context.metadata?.has_unsubscribe) metadataSignals.push('- Contains Unsubscribe header');
|
|
162
202
|
if (context.metadata?.autoSubmitted && context.metadata.autoSubmitted !== 'no') metadataSignals.push(`- Auto-Submitted: ${context.metadata.autoSubmitted}`);
|
|
163
203
|
if (context.metadata?.importance) metadataSignals.push(`- Priority: ${context.metadata.importance}`);
|
|
164
204
|
|
|
@@ -191,11 +231,12 @@ CATEGORY DEFINITIONS:
|
|
|
191
231
|
|
|
192
232
|
CRITICAL RULES:
|
|
193
233
|
1. Platform notifications (linkedin.com, github.com) are ALWAYS "notification" or "social", never "personal"
|
|
194
|
-
2.
|
|
234
|
+
2. Automated sender addresses (noreply@, no-reply@, donotreply@, alerts@, notifications@, updates@, newsletter@, etc.) are ALWAYS "transactional", "notification", or "newsletter" - NEVER "personal"
|
|
195
235
|
3. Weekly/Monthly digests are "newsletter"
|
|
196
236
|
4. If "List-Unsubscribe" header is present, it is likely "newsletter" or "promotional"
|
|
197
237
|
5. Follow "LEARNED PATTERN" signals strictly if present
|
|
198
238
|
6. VIP Senders must be "High" priority unless irrelevant (e.g. OOO auto-reply)
|
|
239
|
+
7. Google Alerts (googlealerts-noreply@google.com) are ALWAYS "news" category, NEVER "personal" or "promotional"
|
|
199
240
|
|
|
200
241
|
FEW-SHOT EXAMPLES:
|
|
201
242
|
|
|
@@ -520,11 +561,86 @@ REQUIRED JSON STRUCTURE:
|
|
|
520
561
|
});
|
|
521
562
|
const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
|
|
522
563
|
|
|
564
|
+
// Build metadata signals (same as analyzeEmail)
|
|
565
|
+
const metadataSignals = [];
|
|
566
|
+
|
|
567
|
+
// Email age signal (objective time context only)
|
|
568
|
+
if (context.date) {
|
|
569
|
+
const emailDate = new Date(context.date);
|
|
570
|
+
const now = new Date();
|
|
571
|
+
const ageMs = now.getTime() - emailDate.getTime();
|
|
572
|
+
const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
573
|
+
const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
|
|
574
|
+
|
|
575
|
+
if (ageDays === 0 && ageHours < 1) {
|
|
576
|
+
metadataSignals.push('- Email age: Less than 1 hour old');
|
|
577
|
+
} else if (ageDays === 0) {
|
|
578
|
+
metadataSignals.push(`- Email age: ${ageHours} hours old`);
|
|
579
|
+
} else if (ageDays === 1) {
|
|
580
|
+
metadataSignals.push('- Email age: 1 day old');
|
|
581
|
+
} else {
|
|
582
|
+
metadataSignals.push(`- Email age: ${ageDays} days old`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Header-based signals (enhanced)
|
|
587
|
+
if (context.metadata?.recipient_type === 'cc') metadataSignals.push('- Recipient: CC (not directly addressed)');
|
|
588
|
+
if (context.metadata?.recipient_type === 'bcc') metadataSignals.push('- Recipient: BCC (bulk/mass email)');
|
|
589
|
+
if (context.metadata?.is_automated) metadataSignals.push('- Automated/Bulk email detected (likely newsletter/marketing)');
|
|
590
|
+
if (context.metadata?.has_unsubscribe) metadataSignals.push('- Contains Unsubscribe header (newsletter indicator)');
|
|
591
|
+
if (context.metadata?.is_reply) metadataSignals.push('- Part of reply thread (ongoing conversation)');
|
|
592
|
+
if (context.metadata?.sender_priority === 'high') metadataSignals.push('- Sender Priority: HIGH (marked as urgent)');
|
|
593
|
+
if (context.metadata?.sender_priority === 'low') metadataSignals.push('- Sender Priority: LOW');
|
|
594
|
+
if (context.metadata?.mailer) metadataSignals.push(`- Sent via: ${context.metadata.mailer}`);
|
|
595
|
+
// Legacy metadata (deprecated but kept for compatibility)
|
|
596
|
+
if (context.metadata?.listUnsubscribe && !context.metadata?.has_unsubscribe) metadataSignals.push('- Contains Unsubscribe header');
|
|
597
|
+
if (context.metadata?.autoSubmitted && context.metadata.autoSubmitted !== 'no') metadataSignals.push(`- Auto-Submitted: ${context.metadata.autoSubmitted}`);
|
|
598
|
+
if (context.metadata?.importance) metadataSignals.push(`- Priority: ${context.metadata.importance}`);
|
|
599
|
+
|
|
600
|
+
// Adaptive Learning Signals
|
|
601
|
+
if (context.userPreferences?.vipSenders?.includes(context.sender)) {
|
|
602
|
+
metadataSignals.push('- SENDER IS VIP (High Priority Required)');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const senderDomain = context.sender.split('@')[1];
|
|
606
|
+
if (senderDomain && context.userPreferences?.categoryPatterns?.[senderDomain]) {
|
|
607
|
+
const learnedCategory = context.userPreferences.categoryPatterns[senderDomain];
|
|
608
|
+
metadataSignals.push(`- LEARNED PATTERN: Sender domain '${senderDomain}' is strictly category '${learnedCategory}'`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Format rules with positive and negative conditions
|
|
612
|
+
const formatCondition = (cond: any): string => {
|
|
613
|
+
if (!cond) return '';
|
|
614
|
+
if (cond.and) return `(${cond.and.map(formatCondition).join(' AND ')})`;
|
|
615
|
+
if (cond.or) return `(${cond.or.map(formatCondition).join(' OR ')})`;
|
|
616
|
+
if (cond.not) return `NOT ${formatCondition(cond.not)}`;
|
|
617
|
+
|
|
618
|
+
const parts = [];
|
|
619
|
+
for (const [key, value] of Object.entries(cond)) {
|
|
620
|
+
if (key === 'category') parts.push(`category=${value}`);
|
|
621
|
+
else if (key === 'is_automated') parts.push(`is_automated=${value}`);
|
|
622
|
+
else if (key === 'has_unsubscribe') parts.push(`has_unsubscribe=${value}`);
|
|
623
|
+
else if (key === 'recipient_type') parts.push(`recipient_type=${value}`);
|
|
624
|
+
else if (key === 'sender_domain') parts.push(`sender_domain=${value}`);
|
|
625
|
+
else parts.push(`${key}=${value}`);
|
|
626
|
+
}
|
|
627
|
+
return parts.join(', ');
|
|
628
|
+
};
|
|
629
|
+
|
|
523
630
|
let rulesContext: string;
|
|
524
631
|
if (typeof compiledRulesContext === 'string') {
|
|
525
632
|
rulesContext = compiledRulesContext;
|
|
526
633
|
} else {
|
|
527
|
-
rulesContext = compiledRulesContext.map(r =>
|
|
634
|
+
rulesContext = compiledRulesContext.map(r => {
|
|
635
|
+
let ruleText = `- ${r.name}: ${r.intent || r.description || 'No description'}`;
|
|
636
|
+
if (r.condition) {
|
|
637
|
+
ruleText += `\n Match when: ${formatCondition(r.condition)}`;
|
|
638
|
+
}
|
|
639
|
+
if (r.negative_condition) {
|
|
640
|
+
ruleText += `\n EXCLUDE when: ${formatCondition(r.negative_condition)}`;
|
|
641
|
+
}
|
|
642
|
+
return ruleText;
|
|
643
|
+
}).join('\n');
|
|
528
644
|
}
|
|
529
645
|
|
|
530
646
|
const systemPrompt = `You are an AI Automation Agent. Analyze the email and identify ALL rules that apply.
|
|
@@ -543,6 +659,11 @@ CATEGORY DEFINITIONS:
|
|
|
543
659
|
- notification: Platform alerts/notifications (Github, Linear, etc) - distinct from social
|
|
544
660
|
- other: Anything that doesn't fit above categories
|
|
545
661
|
|
|
662
|
+
Email Context:
|
|
663
|
+
- Subject: ${context.subject}
|
|
664
|
+
- From: ${context.sender}
|
|
665
|
+
${metadataSignals.join('\n')}
|
|
666
|
+
|
|
546
667
|
Rules Context:
|
|
547
668
|
${rulesContext}
|
|
548
669
|
|
|
@@ -571,7 +692,8 @@ CRITICAL INSTRUCTIONS:
|
|
|
571
692
|
- Actions will be merged by the system - you don't need to resolve conflicts
|
|
572
693
|
- Use "draft" action only if a rule explicitly requests it
|
|
573
694
|
- Platform notifications (linkedin, github) are ALWAYS "notification" or "social"
|
|
574
|
-
-
|
|
695
|
+
- Automated sender addresses (noreply@, no-reply@, donotreply@, alerts@, notifications@, updates@, newsletter@, etc.) are ALWAYS "transactional", "notification", or "newsletter" - NEVER "personal"
|
|
696
|
+
- Google Alerts (googlealerts-noreply@google.com) are ALWAYS "news" category`;
|
|
575
697
|
|
|
576
698
|
if (eventLogger) {
|
|
577
699
|
await eventLogger.info('Thinking', `Context-aware analysis: ${context.subject}`, {
|
|
@@ -226,8 +226,13 @@ export class LearningService {
|
|
|
226
226
|
|
|
227
227
|
private async updateMetrics(userId: string, items: UserFeedback[]): Promise<void> {
|
|
228
228
|
try {
|
|
229
|
-
//
|
|
230
|
-
const
|
|
229
|
+
// Separate positive feedback (correct) from corrections (incorrect)
|
|
230
|
+
const positiveFeedback = items.filter(i =>
|
|
231
|
+
i.feedback_type === 'analysis' && i.corrected_data?.is_correct === true
|
|
232
|
+
);
|
|
233
|
+
const corrections = items.filter(i =>
|
|
234
|
+
i.feedback_type === 'analysis' && i.corrected_data?.is_correct !== true
|
|
235
|
+
);
|
|
231
236
|
const draftEdits = items.filter(i => i.feedback_type === 'draft_edit').length;
|
|
232
237
|
|
|
233
238
|
// Fetch existing or create
|
|
@@ -247,8 +252,7 @@ export class LearningService {
|
|
|
247
252
|
.from('learning_metrics')
|
|
248
253
|
.update({
|
|
249
254
|
total_classifications: (existing.total_classifications || 0) + items.length,
|
|
250
|
-
|
|
251
|
-
// For now, this is just activity tracking
|
|
255
|
+
correct_classifications: (existing.correct_classifications || 0) + positiveFeedback.length,
|
|
252
256
|
drafts_edited: (existing.drafts_edited || 0) + draftEdits,
|
|
253
257
|
updated_at: new Date().toISOString()
|
|
254
258
|
})
|
|
@@ -261,11 +265,19 @@ export class LearningService {
|
|
|
261
265
|
.insert({
|
|
262
266
|
user_id: userId,
|
|
263
267
|
total_classifications: items.length,
|
|
268
|
+
correct_classifications: positiveFeedback.length,
|
|
264
269
|
drafts_edited: draftEdits
|
|
265
270
|
});
|
|
266
271
|
|
|
267
272
|
if (insertError) throw insertError;
|
|
268
273
|
}
|
|
274
|
+
|
|
275
|
+
logger.info('Updated learning metrics', {
|
|
276
|
+
userId,
|
|
277
|
+
positive: positiveFeedback.length,
|
|
278
|
+
corrections: corrections.length,
|
|
279
|
+
total: items.length
|
|
280
|
+
});
|
|
269
281
|
} catch (error) {
|
|
270
282
|
logger.error('Failed to update learning metrics', error as Error);
|
|
271
283
|
}
|