@elizaos/plugin-inbox 2.0.3-beta.6 → 2.0.3-beta.7
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/dist/actions/inbox.d.ts +69 -0
- package/dist/actions/inbox.d.ts.map +1 -0
- package/dist/actions/inbox.js +345 -0
- package/dist/actions/inbox.js.map +1 -0
- package/dist/components/inbox/InboxSpatialView.d.ts +54 -0
- package/dist/components/inbox/InboxSpatialView.d.ts.map +1 -0
- package/dist/components/inbox/InboxSpatialView.js +171 -0
- package/dist/components/inbox/InboxSpatialView.js.map +1 -0
- package/dist/components/inbox/InboxView.d.ts +64 -0
- package/dist/components/inbox/InboxView.d.ts.map +1 -0
- package/dist/components/inbox/InboxView.js +169 -0
- package/dist/components/inbox/InboxView.js.map +1 -0
- package/dist/components/inbox/inbox-view-bundle.d.ts +2 -0
- package/dist/components/inbox/inbox-view-bundle.d.ts.map +1 -0
- package/dist/components/inbox/inbox-view-bundle.js +5 -0
- package/dist/components/inbox/inbox-view-bundle.js.map +1 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +1729 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +79 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/sql.d.ts +32 -0
- package/dist/db/sql.d.ts.map +1 -0
- package/dist/db/sql.js +130 -0
- package/dist/db/sql.js.map +1 -0
- package/dist/inbox/channel-deep-links.d.ts +7 -0
- package/dist/inbox/channel-deep-links.d.ts.map +1 -0
- package/dist/inbox/channel-deep-links.js +97 -0
- package/dist/inbox/channel-deep-links.js.map +1 -0
- package/dist/inbox/config.d.ts +7 -0
- package/dist/inbox/config.d.ts.map +1 -0
- package/dist/inbox/config.js +61 -0
- package/dist/inbox/config.js.map +1 -0
- package/dist/inbox/email-curation.d.ts +174 -0
- package/dist/inbox/email-curation.d.ts.map +1 -0
- package/dist/inbox/email-curation.js +1056 -0
- package/dist/inbox/email-curation.js.map +1 -0
- package/dist/inbox/email-unsubscribe-types.d.ts +71 -0
- package/dist/inbox/email-unsubscribe-types.d.ts.map +1 -0
- package/dist/inbox/email-unsubscribe-types.js +1 -0
- package/dist/inbox/email-unsubscribe-types.js.map +1 -0
- package/dist/inbox/gmail-normalize.d.ts +99 -0
- package/dist/inbox/gmail-normalize.d.ts.map +1 -0
- package/dist/inbox/gmail-normalize.js +937 -0
- package/dist/inbox/gmail-normalize.js.map +1 -0
- package/dist/inbox/google-gmail-seam.d.ts +52 -0
- package/dist/inbox/google-gmail-seam.d.ts.map +1 -0
- package/dist/inbox/google-gmail-seam.js +263 -0
- package/dist/inbox/google-gmail-seam.js.map +1 -0
- package/dist/inbox/message-fetcher.d.ts +47 -0
- package/dist/inbox/message-fetcher.d.ts.map +1 -0
- package/dist/inbox/message-fetcher.js +461 -0
- package/dist/inbox/message-fetcher.js.map +1 -0
- package/dist/inbox/migration.d.ts +46 -0
- package/dist/inbox/migration.d.ts.map +1 -0
- package/dist/inbox/migration.js +114 -0
- package/dist/inbox/migration.js.map +1 -0
- package/dist/inbox/reflection.d.ts +40 -0
- package/dist/inbox/reflection.d.ts.map +1 -0
- package/dist/inbox/reflection.js +142 -0
- package/dist/inbox/reflection.js.map +1 -0
- package/dist/inbox/repository.d.ts +58 -0
- package/dist/inbox/repository.d.ts.map +1 -0
- package/dist/inbox/repository.js +376 -0
- package/dist/inbox/repository.js.map +1 -0
- package/dist/inbox/service.d.ts +149 -0
- package/dist/inbox/service.d.ts.map +1 -0
- package/dist/inbox/service.js +247 -0
- package/dist/inbox/service.js.map +1 -0
- package/dist/inbox/triage-classifier.d.ts +28 -0
- package/dist/inbox/triage-classifier.d.ts.map +1 -0
- package/dist/inbox/triage-classifier.js +306 -0
- package/dist/inbox/triage-classifier.js.map +1 -0
- package/dist/inbox/types.d.ts +124 -0
- package/dist/inbox/types.d.ts.map +1 -0
- package/dist/inbox/types.js +1 -0
- package/dist/inbox/types.js.map +1 -0
- package/dist/inbox/unsubscribe-repository.d.ts +14 -0
- package/dist/inbox/unsubscribe-repository.d.ts.map +1 -0
- package/dist/inbox/unsubscribe-repository.js +112 -0
- package/dist/inbox/unsubscribe-repository.js.map +1 -0
- package/dist/inbox/unsubscribe-service.d.ts +41 -0
- package/dist/inbox/unsubscribe-service.d.ts.map +1 -0
- package/dist/inbox/unsubscribe-service.js +351 -0
- package/dist/inbox/unsubscribe-service.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +38 -0
- package/dist/plugin.js.map +1 -0
- package/dist/providers/cross-channel-context.d.ts +21 -0
- package/dist/providers/cross-channel-context.d.ts.map +1 -0
- package/dist/providers/cross-channel-context.js +96 -0
- package/dist/providers/cross-channel-context.js.map +1 -0
- package/dist/providers/inbox-triage.d.ts +12 -0
- package/dist/providers/inbox-triage.d.ts.map +1 -0
- package/dist/providers/inbox-triage.js +98 -0
- package/dist/providers/inbox-triage.js.map +1 -0
- package/dist/register-terminal-view.d.ts +15 -0
- package/dist/register-terminal-view.d.ts.map +1 -0
- package/dist/register-terminal-view.js +21 -0
- package/dist/register-terminal-view.js.map +1 -0
- package/dist/register.d.ts +9 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +5 -0
- package/dist/register.js.map +1 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +25 -0
- package/dist/types.js.map +1 -0
- package/dist/views/bundle.js +315 -0
- package/dist/views/bundle.js.map +1 -0
- package/package.json +9 -9
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { resolveKnowledgeGraphService } from "@elizaos/agent/services/knowledge-graph/service";
|
|
2
|
+
import { logger, ServiceType } from "@elizaos/core";
|
|
3
|
+
import { loadInboxTriageConfig } from "./config.js";
|
|
4
|
+
import {
|
|
5
|
+
curateEmailCandidates
|
|
6
|
+
} from "./email-curation.js";
|
|
7
|
+
import { InboxRepository } from "./repository.js";
|
|
8
|
+
import { classifyMessages } from "./triage-classifier.js";
|
|
9
|
+
function normalizeSenderEmail(candidate) {
|
|
10
|
+
const raw = candidate.fromEmail ?? candidate.from;
|
|
11
|
+
if (!raw) return null;
|
|
12
|
+
const angle = raw.match(/<([^>]+)>/);
|
|
13
|
+
const value = (angle?.[1] ?? raw).trim().toLowerCase();
|
|
14
|
+
const email = value.match(/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i);
|
|
15
|
+
return email?.[0]?.toLowerCase() ?? null;
|
|
16
|
+
}
|
|
17
|
+
function entityToCurationIdentity(candidate) {
|
|
18
|
+
const { entity } = candidate;
|
|
19
|
+
const isVip = entity.tags.some((tag) => tag.toLowerCase() === "vip");
|
|
20
|
+
return {
|
|
21
|
+
kind: isVip ? "vip" : "known_person",
|
|
22
|
+
label: entity.preferredName,
|
|
23
|
+
matchedBy: ["knowledge_graph.identity.email"],
|
|
24
|
+
blockDelete: true,
|
|
25
|
+
personId: entity.entityId
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function toCurationCandidate(message) {
|
|
29
|
+
return {
|
|
30
|
+
id: message.id,
|
|
31
|
+
...message.threadId ? { threadId: message.threadId } : {},
|
|
32
|
+
subject: message.channelName,
|
|
33
|
+
snippet: message.snippet,
|
|
34
|
+
from: message.senderName,
|
|
35
|
+
...message.senderEmail ? { fromEmail: message.senderEmail } : {},
|
|
36
|
+
bodyText: message.text,
|
|
37
|
+
receivedAt: new Date(message.timestamp).toISOString()
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
class InboxService {
|
|
41
|
+
constructor(runtime) {
|
|
42
|
+
this.runtime = runtime;
|
|
43
|
+
this.repository = new InboxRepository(runtime);
|
|
44
|
+
}
|
|
45
|
+
runtime;
|
|
46
|
+
repository;
|
|
47
|
+
getRepository() {
|
|
48
|
+
return this.repository;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Classify a batch of inbound messages and (unless `classifyOnly`) persist
|
|
52
|
+
* one triage entry per message. Returns the per-message decision in input
|
|
53
|
+
* order. Messages already triaged by `source_message_id` are skipped so a
|
|
54
|
+
* re-run does not double-store.
|
|
55
|
+
*/
|
|
56
|
+
async triage(messages, opts = {}) {
|
|
57
|
+
if (messages.length === 0) return { triaged: [] };
|
|
58
|
+
const config = opts.config ?? loadInboxTriageConfig();
|
|
59
|
+
const examples = opts.classifyOnly ? [] : await this.repository.getExamples(opts.exampleLimit ?? 10);
|
|
60
|
+
const results = await classifyMessages(this.runtime, messages, {
|
|
61
|
+
config,
|
|
62
|
+
examples,
|
|
63
|
+
...opts.ownerContext ? { ownerContext: opts.ownerContext } : {}
|
|
64
|
+
});
|
|
65
|
+
const triaged = [];
|
|
66
|
+
for (let i = 0; i < messages.length; i += 1) {
|
|
67
|
+
const message = messages[i];
|
|
68
|
+
const result = results[i];
|
|
69
|
+
if (!message || !result) continue;
|
|
70
|
+
const triagedMessage = {
|
|
71
|
+
message,
|
|
72
|
+
classification: result.classification,
|
|
73
|
+
urgency: result.urgency,
|
|
74
|
+
confidence: result.confidence,
|
|
75
|
+
reasoning: result.reasoning,
|
|
76
|
+
...result.suggestedResponse ? { suggestedResponse: result.suggestedResponse } : {}
|
|
77
|
+
};
|
|
78
|
+
if (!opts.classifyOnly) {
|
|
79
|
+
const existing = message.id ? await this.repository.getBySourceMessageId(message.id) : null;
|
|
80
|
+
if (existing) {
|
|
81
|
+
triagedMessage.entry = existing;
|
|
82
|
+
} else {
|
|
83
|
+
const stored = await this.repository.storeTriage({
|
|
84
|
+
source: message.source,
|
|
85
|
+
...message.roomId ? { sourceRoomId: message.roomId } : {},
|
|
86
|
+
...message.entityId ? { sourceEntityId: message.entityId } : {},
|
|
87
|
+
...message.id ? { sourceMessageId: message.id } : {},
|
|
88
|
+
channelName: message.channelName,
|
|
89
|
+
channelType: message.channelType,
|
|
90
|
+
...message.deepLink ? { deepLink: message.deepLink } : {},
|
|
91
|
+
classification: result.classification,
|
|
92
|
+
urgency: result.urgency,
|
|
93
|
+
confidence: result.confidence,
|
|
94
|
+
snippet: message.snippet,
|
|
95
|
+
...message.senderName ? { senderName: message.senderName } : {},
|
|
96
|
+
...message.threadMessages && message.threadMessages.length > 0 ? { threadContext: message.threadMessages } : {},
|
|
97
|
+
...result.reasoning ? { triageReasoning: result.reasoning } : {},
|
|
98
|
+
...result.suggestedResponse ? { suggestedResponse: result.suggestedResponse } : {}
|
|
99
|
+
});
|
|
100
|
+
triagedMessage.entry = stored;
|
|
101
|
+
this.notifyAttention(stored);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
triaged.push(triagedMessage);
|
|
105
|
+
}
|
|
106
|
+
return { triaged };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Surface an act-now triage entry (`urgent` / `needs_reply`) on the home
|
|
110
|
+
* notification rail. Best-effort: a runtime with no NotificationService (a
|
|
111
|
+
* headless/test runtime) is a no-op. `info`/`notify`/`ignore` stay in the
|
|
112
|
+
* inbox view without pushing a notification.
|
|
113
|
+
*/
|
|
114
|
+
notifyAttention(entry) {
|
|
115
|
+
const isUrgent = entry.classification === "urgent";
|
|
116
|
+
if (!isUrgent && entry.classification !== "needs_reply") return;
|
|
117
|
+
const who = entry.senderName ? ` from ${entry.senderName}` : "";
|
|
118
|
+
this.runtime.getService(ServiceType.NOTIFICATION)?.notify({
|
|
119
|
+
title: isUrgent ? `Urgent message${who}` : `Message${who} needs a reply`,
|
|
120
|
+
body: entry.snippet,
|
|
121
|
+
category: "message",
|
|
122
|
+
priority: isUrgent ? "urgent" : "high",
|
|
123
|
+
source: "inbox",
|
|
124
|
+
groupKey: `inbox:${entry.id}`,
|
|
125
|
+
deepLink: entry.deepLink ?? "/inbox",
|
|
126
|
+
data: {
|
|
127
|
+
triageEntryId: entry.id,
|
|
128
|
+
classification: entry.classification,
|
|
129
|
+
channelName: entry.channelName
|
|
130
|
+
}
|
|
131
|
+
}).catch((error) => {
|
|
132
|
+
logger.debug({ src: "inbox", error }, "Triage notify failed");
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Run the pure email-curation engine over a batch of inbound messages and
|
|
137
|
+
* return the per-candidate decision (save / archive / delete / review with
|
|
138
|
+
* evidence, citations, and a bulk-review block).
|
|
139
|
+
*
|
|
140
|
+
* This is the richer decision path that complements {@link triage}: triage
|
|
141
|
+
* answers "how urgent is this and should I reply", curation answers "what
|
|
142
|
+
* should happen to this message in the owner's mailbox". It does not persist
|
|
143
|
+
* anything — callers decide what to do with the suggested action.
|
|
144
|
+
*
|
|
145
|
+
* The identity hook is backed by the runtime knowledge-graph service so the
|
|
146
|
+
* sender's entity (VIP / known person / service) feeds the engine's
|
|
147
|
+
* delete-blockers. Both hooks are injectable as a test seam.
|
|
148
|
+
*/
|
|
149
|
+
async curate(messages, opts = {}) {
|
|
150
|
+
const candidates = messages.map((message) => toCurationCandidate(message));
|
|
151
|
+
const identityHook = opts.identityHook ?? await this.buildKnowledgeGraphIdentityHook(candidates);
|
|
152
|
+
return curateEmailCandidates({
|
|
153
|
+
candidates,
|
|
154
|
+
identityHook,
|
|
155
|
+
...opts.policyHook ? { policyHook: opts.policyHook } : {},
|
|
156
|
+
...opts.policy ? { policy: opts.policy } : {},
|
|
157
|
+
...opts.now ? { now: opts.now } : {}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Triage a batch, then attach a curation decision to each message in the
|
|
162
|
+
* same input order. Triage behavior is unchanged (the existing
|
|
163
|
+
* classification + persistence still runs); curation is additive.
|
|
164
|
+
*/
|
|
165
|
+
async triageWithCuration(messages, opts = {}) {
|
|
166
|
+
const { identityHook, policyHook, policy, now, ...triageOpts } = opts;
|
|
167
|
+
const triageResult = await this.triage(messages, triageOpts);
|
|
168
|
+
const curation = await this.curate(messages, {
|
|
169
|
+
...identityHook ? { identityHook } : {},
|
|
170
|
+
...policyHook ? { policyHook } : {},
|
|
171
|
+
...policy ? { policy } : {},
|
|
172
|
+
...now ? { now } : {}
|
|
173
|
+
});
|
|
174
|
+
const decisionByCandidateId = new Map(
|
|
175
|
+
curation.decisions.flatMap(
|
|
176
|
+
(decision) => decision.canonicalMessageIds.map((id) => [id, decision])
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
const triaged = triageResult.triaged.flatMap(
|
|
180
|
+
(triagedMessage) => {
|
|
181
|
+
const decision = decisionByCandidateId.get(triagedMessage.message.id);
|
|
182
|
+
if (!decision) return [];
|
|
183
|
+
return [{ ...triagedMessage, curation: decision }];
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
return { triaged, curation };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Build an {@link EmailCurationIdentityHook} backed by the runtime
|
|
190
|
+
* knowledge-graph service. Pre-resolves every candidate's sender against the
|
|
191
|
+
* entity graph (the engine hook itself must be synchronous, so the async
|
|
192
|
+
* graph reads happen here) and maps the resolved entity onto the engine's
|
|
193
|
+
* identity kinds. Candidates the graph cannot resolve fall through to the
|
|
194
|
+
* engine's built-in sender heuristics. When the graph service is absent the
|
|
195
|
+
* hook resolves nothing.
|
|
196
|
+
*/
|
|
197
|
+
async buildKnowledgeGraphIdentityHook(candidates) {
|
|
198
|
+
const kg = resolveKnowledgeGraphService(this.runtime);
|
|
199
|
+
if (!kg) return () => null;
|
|
200
|
+
const store = kg.getEntityStore();
|
|
201
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
202
|
+
for (const candidate of candidates) {
|
|
203
|
+
const senderEmail = normalizeSenderEmail(candidate);
|
|
204
|
+
if (!senderEmail) continue;
|
|
205
|
+
const matches = await store.resolve({
|
|
206
|
+
identity: { platform: "email", handle: senderEmail }
|
|
207
|
+
});
|
|
208
|
+
const best = matches[0];
|
|
209
|
+
if (!best) continue;
|
|
210
|
+
resolved.set(candidate.id, entityToCurationIdentity(best));
|
|
211
|
+
}
|
|
212
|
+
return (candidate) => resolved.get(candidate.id) ?? null;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Read persisted triage entries, optionally filtered by classification.
|
|
216
|
+
* Backs the INBOX action's `search`/`list` reads over the triage queue.
|
|
217
|
+
*/
|
|
218
|
+
async search(opts = {}) {
|
|
219
|
+
if (opts.classification) {
|
|
220
|
+
return this.repository.getByClassification(opts.classification, {
|
|
221
|
+
...opts.limit !== void 0 ? { limit: opts.limit } : {},
|
|
222
|
+
...opts.unresolvedOnly !== void 0 ? { unresolvedOnly: opts.unresolvedOnly } : {}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return this.repository.getUnresolved(
|
|
226
|
+
opts.limit !== void 0 ? { limit: opts.limit } : void 0
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
/** Unresolved triage queue (urgency-ordered). */
|
|
230
|
+
async list(limit) {
|
|
231
|
+
return this.repository.getUnresolved(
|
|
232
|
+
limit !== void 0 ? { limit } : void 0
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
/** Non-ignored triage entries created since `sinceIso`, urgency-ordered. */
|
|
236
|
+
async digest(sinceIso) {
|
|
237
|
+
return this.repository.getRecentForDigest(sinceIso);
|
|
238
|
+
}
|
|
239
|
+
/** Mark a triage entry resolved (optionally recording the sent draft). */
|
|
240
|
+
async resolve(id, opts) {
|
|
241
|
+
await this.repository.markResolved(id, opts);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export {
|
|
245
|
+
InboxService
|
|
246
|
+
};
|
|
247
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/inbox/service.ts"],"sourcesContent":["/**\n * InboxService — the inbox triage back-end.\n *\n * Standalone successor to the inbox-domain logic that lived in PA's\n * `service-mixin-inbox` + `inbox/` modules. It holds its own runtime and\n * {@link InboxRepository} (raw SQL over the `app_inbox.life_inbox_triage_*`\n * tables PA still registers), classifies inbound messages with the LLM, and\n * answers the triage-queue reads the INBOX action and inboxTriage provider need.\n * It carries no dependency on `@elizaos/plugin-personal-assistant`.\n *\n * NOT here (delegated / left in PA, by design):\n * - `getInbox` / `markInboxEntryRead` — the cached cross-channel inbox that\n * backs `GET /api/lifeops/inbox`. It is coupled to PA's `LifeOpsRepository`\n * inbox cache, LLM priority scoring, Gmail/X connector sources, and the\n * app-state store, so it remains a PA service method (the route shape stays\n * byte-identical). InboxService takes the inbound feed as input instead of\n * pulling connectors itself.\n */\n\nimport { resolveKnowledgeGraphService } from \"@elizaos/agent/services/knowledge-graph/service\";\nimport type { IAgentRuntime, NotificationService } from \"@elizaos/core\";\nimport { logger, ServiceType } from \"@elizaos/core\";\nimport type { EntityResolveCandidate } from \"@elizaos/shared\";\nimport { loadInboxTriageConfig } from \"./config.js\";\nimport {\n type CurationDecision,\n curateEmailCandidates,\n type EmailCurationCandidate,\n type EmailCurationIdentityHook,\n type EmailCurationOutput,\n type EmailCurationPolicy,\n type EmailCurationPolicyHook,\n type EmailCurationResolvedIdentity,\n} from \"./email-curation.js\";\nimport { InboxRepository } from \"./repository.js\";\nimport { classifyMessages } from \"./triage-classifier.js\";\nimport type {\n InboundMessage,\n InboxTriageConfig,\n TriageClassification,\n TriageEntry,\n} from \"./types.js\";\n\n/** Lower-cased, angle-bracket-stripped sender email, or null. */\nfunction normalizeSenderEmail(\n candidate: EmailCurationCandidate,\n): string | null {\n const raw = candidate.fromEmail ?? candidate.from;\n if (!raw) return null;\n const angle = raw.match(/<([^>]+)>/);\n const value = (angle?.[1] ?? raw).trim().toLowerCase();\n const email = value.match(/[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}/i);\n return email?.[0]?.toLowerCase() ?? null;\n}\n\n/**\n * Map a resolved knowledge-graph entity onto the engine's identity contract.\n * A `vip`-tagged entity is a VIP; any other graph person/org is a known\n * sender. Both block bulk delete (the graph deliberately tracked them).\n */\nfunction entityToCurationIdentity(\n candidate: EntityResolveCandidate,\n): EmailCurationResolvedIdentity {\n const { entity } = candidate;\n const isVip = entity.tags.some((tag) => tag.toLowerCase() === \"vip\");\n return {\n kind: isVip ? \"vip\" : \"known_person\",\n label: entity.preferredName,\n matchedBy: [\"knowledge_graph.identity.email\"],\n blockDelete: true,\n personId: entity.entityId,\n };\n}\n\n/** Project a normalized inbound message onto the engine's candidate shape. */\nfunction toCurationCandidate(message: InboundMessage): EmailCurationCandidate {\n return {\n id: message.id,\n ...(message.threadId ? { threadId: message.threadId } : {}),\n subject: message.channelName,\n snippet: message.snippet,\n from: message.senderName,\n ...(message.senderEmail ? { fromEmail: message.senderEmail } : {}),\n bodyText: message.text,\n receivedAt: new Date(message.timestamp).toISOString(),\n };\n}\n\nexport interface TriageOptions {\n /** Override the loaded triage config (priority senders/channels, rules). */\n config?: InboxTriageConfig;\n /** Owner context string injected into the classifier prompt. */\n ownerContext?: string;\n /** How many past owner-corrected examples to few-shot the classifier with. */\n exampleLimit?: number;\n /** Skip persistence and only return the classification (default false). */\n classifyOnly?: boolean;\n}\n\nexport interface TriagedMessage {\n message: InboundMessage;\n classification: TriageClassification;\n urgency: \"low\" | \"medium\" | \"high\";\n confidence: number;\n reasoning: string;\n suggestedResponse?: string;\n /** The persisted triage entry, unless `classifyOnly` was set. */\n entry?: TriageEntry;\n}\n\nexport interface TriageRunResult {\n triaged: TriagedMessage[];\n}\n\nexport interface SearchOptions {\n classification?: TriageClassification;\n limit?: number;\n unresolvedOnly?: boolean;\n}\n\nexport interface CurateOptions {\n /**\n * Identity resolver. Defaults to a hook backed by the runtime\n * {@link resolveKnowledgeGraphService | knowledge-graph service}, which\n * resolves the sender's entity from the runtime graph. Injectable as a test\n * seam and to let callers override the identity source.\n */\n identityHook?: EmailCurationIdentityHook;\n /**\n * Policy hook applied after the engine's provisional decision. Defaults to a\n * no-op (the engine's built-in `DEFAULT_POLICY` thresholds and delete\n * blockers still apply). No PA-owned policy store is reachable from the\n * inbox plugin without importing PA, so there is no default policy source\n * beyond the engine's own defaults.\n */\n policyHook?: EmailCurationPolicyHook;\n /** Static policy overrides merged onto the engine defaults. */\n policy?: EmailCurationPolicy;\n /** Override the curation timestamp (defaults to engine `now`). */\n now?: string;\n}\n\n/** The curation decision attached to a triaged message. */\nexport interface CuratedMessage extends TriagedMessage {\n curation: CurationDecision;\n}\n\nexport interface TriageWithCurationResult {\n triaged: CuratedMessage[];\n curation: EmailCurationOutput;\n}\n\n/**\n * The triage / search / list back-end for the inbox domain. One instance per\n * call is fine — the repository is a thin raw-SQL wrapper over the runtime DB.\n */\nexport class InboxService {\n private readonly repository: InboxRepository;\n\n constructor(private readonly runtime: IAgentRuntime) {\n this.repository = new InboxRepository(runtime);\n }\n\n getRepository(): InboxRepository {\n return this.repository;\n }\n\n /**\n * Classify a batch of inbound messages and (unless `classifyOnly`) persist\n * one triage entry per message. Returns the per-message decision in input\n * order. Messages already triaged by `source_message_id` are skipped so a\n * re-run does not double-store.\n */\n async triage(\n messages: InboundMessage[],\n opts: TriageOptions = {},\n ): Promise<TriageRunResult> {\n if (messages.length === 0) return { triaged: [] };\n\n const config = opts.config ?? loadInboxTriageConfig();\n const examples = opts.classifyOnly\n ? []\n : await this.repository.getExamples(opts.exampleLimit ?? 10);\n\n const results = await classifyMessages(this.runtime, messages, {\n config,\n examples,\n ...(opts.ownerContext ? { ownerContext: opts.ownerContext } : {}),\n });\n\n const triaged: TriagedMessage[] = [];\n for (let i = 0; i < messages.length; i += 1) {\n const message = messages[i];\n const result = results[i];\n if (!message || !result) continue;\n\n const triagedMessage: TriagedMessage = {\n message,\n classification: result.classification,\n urgency: result.urgency,\n confidence: result.confidence,\n reasoning: result.reasoning,\n ...(result.suggestedResponse\n ? { suggestedResponse: result.suggestedResponse }\n : {}),\n };\n\n if (!opts.classifyOnly) {\n const existing = message.id\n ? await this.repository.getBySourceMessageId(message.id)\n : null;\n if (existing) {\n triagedMessage.entry = existing;\n } else {\n const stored = await this.repository.storeTriage({\n source: message.source,\n ...(message.roomId ? { sourceRoomId: message.roomId } : {}),\n ...(message.entityId ? { sourceEntityId: message.entityId } : {}),\n ...(message.id ? { sourceMessageId: message.id } : {}),\n channelName: message.channelName,\n channelType: message.channelType,\n ...(message.deepLink ? { deepLink: message.deepLink } : {}),\n classification: result.classification,\n urgency: result.urgency,\n confidence: result.confidence,\n snippet: message.snippet,\n ...(message.senderName ? { senderName: message.senderName } : {}),\n ...(message.threadMessages && message.threadMessages.length > 0\n ? { threadContext: message.threadMessages }\n : {}),\n ...(result.reasoning ? { triageReasoning: result.reasoning } : {}),\n ...(result.suggestedResponse\n ? { suggestedResponse: result.suggestedResponse }\n : {}),\n });\n triagedMessage.entry = stored;\n // A newly-triaged message the user needs to act on is a home-screen\n // attention moment. Fire once per new entry (groupKey collapses) only\n // for the act-now classifications, so the inbox doesn't spam the\n // notification rail with every \"info\"/\"notify\" item (those stay\n // visible in the inbox view).\n this.notifyAttention(stored);\n }\n }\n\n triaged.push(triagedMessage);\n }\n\n return { triaged };\n }\n\n /**\n * Surface an act-now triage entry (`urgent` / `needs_reply`) on the home\n * notification rail. Best-effort: a runtime with no NotificationService (a\n * headless/test runtime) is a no-op. `info`/`notify`/`ignore` stay in the\n * inbox view without pushing a notification.\n */\n private notifyAttention(entry: TriageEntry): void {\n const isUrgent = entry.classification === \"urgent\";\n if (!isUrgent && entry.classification !== \"needs_reply\") return;\n\n const who = entry.senderName ? ` from ${entry.senderName}` : \"\";\n this.runtime\n .getService<NotificationService>(ServiceType.NOTIFICATION)\n ?.notify({\n title: isUrgent\n ? `Urgent message${who}`\n : `Message${who} needs a reply`,\n body: entry.snippet,\n category: \"message\",\n priority: isUrgent ? \"urgent\" : \"high\",\n source: \"inbox\",\n groupKey: `inbox:${entry.id}`,\n deepLink: entry.deepLink ?? \"/inbox\",\n data: {\n triageEntryId: entry.id,\n classification: entry.classification,\n channelName: entry.channelName,\n },\n })\n .catch((error: unknown) => {\n logger.debug({ src: \"inbox\", error }, \"Triage notify failed\");\n });\n }\n\n /**\n * Run the pure email-curation engine over a batch of inbound messages and\n * return the per-candidate decision (save / archive / delete / review with\n * evidence, citations, and a bulk-review block).\n *\n * This is the richer decision path that complements {@link triage}: triage\n * answers \"how urgent is this and should I reply\", curation answers \"what\n * should happen to this message in the owner's mailbox\". It does not persist\n * anything — callers decide what to do with the suggested action.\n *\n * The identity hook is backed by the runtime knowledge-graph service so the\n * sender's entity (VIP / known person / service) feeds the engine's\n * delete-blockers. Both hooks are injectable as a test seam.\n */\n async curate(\n messages: InboundMessage[],\n opts: CurateOptions = {},\n ): Promise<EmailCurationOutput> {\n const candidates = messages.map((message) => toCurationCandidate(message));\n // The engine's identity hook is synchronous, but the knowledge-graph read\n // is async, so we pre-resolve every candidate's identity here and hand the\n // engine a synchronous lookup over that map. An explicitly injected hook\n // wins (test seam / caller override).\n const identityHook =\n opts.identityHook ??\n (await this.buildKnowledgeGraphIdentityHook(candidates));\n return curateEmailCandidates({\n candidates,\n identityHook,\n ...(opts.policyHook ? { policyHook: opts.policyHook } : {}),\n ...(opts.policy ? { policy: opts.policy } : {}),\n ...(opts.now ? { now: opts.now } : {}),\n });\n }\n\n /**\n * Triage a batch, then attach a curation decision to each message in the\n * same input order. Triage behavior is unchanged (the existing\n * classification + persistence still runs); curation is additive.\n */\n async triageWithCuration(\n messages: InboundMessage[],\n opts: TriageOptions & CurateOptions = {},\n ): Promise<TriageWithCurationResult> {\n const { identityHook, policyHook, policy, now, ...triageOpts } = opts;\n const triageResult = await this.triage(messages, triageOpts);\n const curation = await this.curate(messages, {\n ...(identityHook ? { identityHook } : {}),\n ...(policyHook ? { policyHook } : {}),\n ...(policy ? { policy } : {}),\n ...(now ? { now } : {}),\n });\n\n const decisionByCandidateId = new Map(\n curation.decisions.flatMap((decision) =>\n decision.canonicalMessageIds.map((id) => [id, decision] as const),\n ),\n );\n\n const triaged: CuratedMessage[] = triageResult.triaged.flatMap(\n (triagedMessage) => {\n const decision = decisionByCandidateId.get(triagedMessage.message.id);\n if (!decision) return [];\n return [{ ...triagedMessage, curation: decision }];\n },\n );\n\n return { triaged, curation };\n }\n\n /**\n * Build an {@link EmailCurationIdentityHook} backed by the runtime\n * knowledge-graph service. Pre-resolves every candidate's sender against the\n * entity graph (the engine hook itself must be synchronous, so the async\n * graph reads happen here) and maps the resolved entity onto the engine's\n * identity kinds. Candidates the graph cannot resolve fall through to the\n * engine's built-in sender heuristics. When the graph service is absent the\n * hook resolves nothing.\n */\n private async buildKnowledgeGraphIdentityHook(\n candidates: readonly EmailCurationCandidate[],\n ): Promise<EmailCurationIdentityHook> {\n const kg = resolveKnowledgeGraphService(this.runtime);\n if (!kg) return () => null;\n const store = kg.getEntityStore();\n\n const resolved = new Map<string, EmailCurationResolvedIdentity>();\n for (const candidate of candidates) {\n const senderEmail = normalizeSenderEmail(candidate);\n if (!senderEmail) continue;\n const matches = await store.resolve({\n identity: { platform: \"email\", handle: senderEmail },\n });\n const best = matches[0];\n if (!best) continue;\n resolved.set(candidate.id, entityToCurationIdentity(best));\n }\n\n return (candidate) => resolved.get(candidate.id) ?? null;\n }\n\n /**\n * Read persisted triage entries, optionally filtered by classification.\n * Backs the INBOX action's `search`/`list` reads over the triage queue.\n */\n async search(opts: SearchOptions = {}): Promise<TriageEntry[]> {\n if (opts.classification) {\n return this.repository.getByClassification(opts.classification, {\n ...(opts.limit !== undefined ? { limit: opts.limit } : {}),\n ...(opts.unresolvedOnly !== undefined\n ? { unresolvedOnly: opts.unresolvedOnly }\n : {}),\n });\n }\n return this.repository.getUnresolved(\n opts.limit !== undefined ? { limit: opts.limit } : undefined,\n );\n }\n\n /** Unresolved triage queue (urgency-ordered). */\n async list(limit?: number): Promise<TriageEntry[]> {\n return this.repository.getUnresolved(\n limit !== undefined ? { limit } : undefined,\n );\n }\n\n /** Non-ignored triage entries created since `sinceIso`, urgency-ordered. */\n async digest(sinceIso: string): Promise<TriageEntry[]> {\n return this.repository.getRecentForDigest(sinceIso);\n }\n\n /** Mark a triage entry resolved (optionally recording the sent draft). */\n async resolve(\n id: string,\n opts?: { draftResponse?: string; autoReplied?: boolean },\n ): Promise<void> {\n await this.repository.markResolved(id, opts);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IAgentRuntime } from "@elizaos/core";
|
|
2
|
+
import type { InboundMessage, InboxTriageConfig, TriageExample, TriageResult } from "./types.js";
|
|
3
|
+
export declare class InboxTriageClassificationError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Classify a batch of messages using the LLM. Returns one TriageResult per
|
|
8
|
+
* input message, in the same order.
|
|
9
|
+
*/
|
|
10
|
+
export declare function classifyMessages(runtime: IAgentRuntime, messages: InboundMessage[], opts: {
|
|
11
|
+
config?: InboxTriageConfig;
|
|
12
|
+
examples?: TriageExample[];
|
|
13
|
+
ownerContext?: string;
|
|
14
|
+
}): Promise<TriageResult[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Static classification instructions for the inbox-triage task. Exposed as the
|
|
17
|
+
* optimizable baseline so {@link resolveOptimizedPromptForRuntime} can swap in a
|
|
18
|
+
* GEPA-optimized `inbox_triage` artifact when one is registered (#8795). The
|
|
19
|
+
* dynamic owner context / examples / messages are scaffolded around it below.
|
|
20
|
+
*/
|
|
21
|
+
export declare const INBOX_TRIAGE_INSTRUCTIONS: string;
|
|
22
|
+
export declare function buildTriagePrompt(messages: InboundMessage[], opts: {
|
|
23
|
+
config?: InboxTriageConfig;
|
|
24
|
+
examples?: TriageExample[];
|
|
25
|
+
ownerContext?: string;
|
|
26
|
+
runtime?: IAgentRuntime;
|
|
27
|
+
}): string;
|
|
28
|
+
//# sourceMappingURL=triage-classifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"triage-classifier.d.ts","sourceRoot":"","sources":["../../src/inbox/triage-classifier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAQnD,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EAEjB,aAAa,EACb,YAAY,EACb,MAAM,YAAY,CAAC;AAMpB,qBAAa,8BAA+B,SAAQ,KAAK;gBAC3C,OAAO,EAAE,MAAM;CAI5B;AAaD;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,cAAc,EAAE,EAC1B,IAAI,EAAE;IACJ,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GACA,OAAO,CAAC,YAAY,EAAE,CAAC,CAczB;AAoCD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,QAc1B,CAAC;AAEb,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,cAAc,EAAE,EAC1B,IAAI,EAAE;IACJ,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB,GACA,MAAM,CA+FR"}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logger,
|
|
3
|
+
ModelType,
|
|
4
|
+
parseJsonModelRecord,
|
|
5
|
+
resolveOptimizedPromptForRuntime,
|
|
6
|
+
runWithTrajectoryContext
|
|
7
|
+
} from "@elizaos/core";
|
|
8
|
+
class InboxTriageClassificationError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "InboxTriageClassificationError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function formatPromptScalar(value, maxLength = 600) {
|
|
18
|
+
if (value === null || value === void 0) {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
return String(value).replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
22
|
+
}
|
|
23
|
+
async function classifyMessages(runtime, messages, opts) {
|
|
24
|
+
if (messages.length === 0) return [];
|
|
25
|
+
const results = [];
|
|
26
|
+
const batchSize = 10;
|
|
27
|
+
for (let i = 0; i < messages.length; i += batchSize) {
|
|
28
|
+
const batch = messages.slice(i, i + batchSize);
|
|
29
|
+
const batchResults = await classifyBatch(runtime, batch, opts);
|
|
30
|
+
results.push(...batchResults);
|
|
31
|
+
}
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
async function classifyBatch(runtime, messages, opts) {
|
|
35
|
+
const prompt = buildTriagePrompt(messages, { ...opts, runtime });
|
|
36
|
+
let rawResponse = "";
|
|
37
|
+
try {
|
|
38
|
+
const result = await runWithTrajectoryContext(
|
|
39
|
+
{ purpose: "inbox_triage" },
|
|
40
|
+
() => runtime.useModel(ModelType.TEXT_SMALL, { prompt })
|
|
41
|
+
);
|
|
42
|
+
rawResponse = typeof result === "string" ? result : "";
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logger.warn(
|
|
45
|
+
{
|
|
46
|
+
src: "inbox-classifier",
|
|
47
|
+
error: error instanceof Error ? error.message : String(error)
|
|
48
|
+
},
|
|
49
|
+
"[InboxTriageClassifier] LLM classification failed"
|
|
50
|
+
);
|
|
51
|
+
throw new InboxTriageClassificationError(
|
|
52
|
+
"Inbox classification model call failed."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return parseTriageResults(rawResponse, messages.length);
|
|
56
|
+
}
|
|
57
|
+
const INBOX_TRIAGE_INSTRUCTIONS = [
|
|
58
|
+
"Classify each message into one of these categories:",
|
|
59
|
+
"",
|
|
60
|
+
"- ignore: spam, irrelevant, automated notifications, bot messages, or general chat that needs no attention",
|
|
61
|
+
"- info: informational updates the owner might want to see but doesn't need to act on",
|
|
62
|
+
"- notify: important information the owner should see, but no response is needed",
|
|
63
|
+
"- needs_reply: someone is asking a question or expects a response from the owner",
|
|
64
|
+
"- urgent: time-sensitive, critical, or from a priority contact \u2014 needs immediate attention",
|
|
65
|
+
"",
|
|
66
|
+
"For each message, also provide:",
|
|
67
|
+
"- urgency: low / medium / high",
|
|
68
|
+
"- confidence: 0.0 to 1.0 (how sure you are about this classification)",
|
|
69
|
+
"- reasoning: brief explanation",
|
|
70
|
+
"- suggestedResponse: (optional) a brief draft response if classification is needs_reply or urgent"
|
|
71
|
+
].join("\n");
|
|
72
|
+
function buildTriagePrompt(messages, opts) {
|
|
73
|
+
const sections = [];
|
|
74
|
+
const instructions = opts.runtime ? resolveOptimizedPromptForRuntime(
|
|
75
|
+
opts.runtime,
|
|
76
|
+
"inbox_triage",
|
|
77
|
+
INBOX_TRIAGE_INSTRUCTIONS
|
|
78
|
+
) : INBOX_TRIAGE_INSTRUCTIONS;
|
|
79
|
+
sections.push(instructions);
|
|
80
|
+
if (opts.ownerContext) {
|
|
81
|
+
sections.push("", "Owner context:", opts.ownerContext);
|
|
82
|
+
}
|
|
83
|
+
const config = opts.config;
|
|
84
|
+
if (config?.prioritySenders?.length) {
|
|
85
|
+
sections.push("", "Priority senders (treat as higher urgency):");
|
|
86
|
+
for (const [index, sender] of config.prioritySenders.entries()) {
|
|
87
|
+
sections.push(`prioritySenders[${index}]: ${formatPromptScalar(sender)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (config?.priorityChannels?.length) {
|
|
91
|
+
sections.push("", "Priority channels:");
|
|
92
|
+
for (const [index, channel] of config.priorityChannels.entries()) {
|
|
93
|
+
sections.push(
|
|
94
|
+
`priorityChannels[${index}]: ${formatPromptScalar(channel)}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (opts.examples && opts.examples.length > 0) {
|
|
99
|
+
sections.push("", "Examples from past triage decisions:");
|
|
100
|
+
for (const [index, ex] of opts.examples.slice(0, 5).entries()) {
|
|
101
|
+
sections.push(
|
|
102
|
+
`examples[${index}]:`,
|
|
103
|
+
` source: ${formatPromptScalar(ex.source, 120)}`,
|
|
104
|
+
` snippet: ${formatPromptScalar(ex.snippet, 160)}`,
|
|
105
|
+
` classification: ${ex.classification}`,
|
|
106
|
+
` ownerClassification: ${ex.ownerClassification ?? ""}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
sections.push("", "Messages to classify:", "");
|
|
111
|
+
for (const [index, msg] of messages.entries()) {
|
|
112
|
+
const gmailHints = [];
|
|
113
|
+
if (msg.gmailIsImportant) gmailHints.push("Gmail-marked-important");
|
|
114
|
+
if (msg.gmailLikelyReplyNeeded)
|
|
115
|
+
gmailHints.push("Gmail-likely-reply-needed");
|
|
116
|
+
sections.push(
|
|
117
|
+
`messages[${index}]:`,
|
|
118
|
+
` source: ${formatPromptScalar(msg.source, 120)}`,
|
|
119
|
+
` channelName: ${formatPromptScalar(msg.channelName, 160)}`,
|
|
120
|
+
` channelType: ${msg.channelType}`,
|
|
121
|
+
` senderName: ${formatPromptScalar(msg.senderName, 160)}`
|
|
122
|
+
);
|
|
123
|
+
for (const [hintIndex, hint] of gmailHints.entries()) {
|
|
124
|
+
sections.push(` hints[${hintIndex}]: ${hint}`);
|
|
125
|
+
}
|
|
126
|
+
sections.push(` text: ${formatPromptScalar(msg.text, 500)}`);
|
|
127
|
+
if (msg.threadMessages && msg.threadMessages.length > 0) {
|
|
128
|
+
for (const [threadIndex, threadMessage] of msg.threadMessages.slice(-5).entries()) {
|
|
129
|
+
sections.push(
|
|
130
|
+
` threadMessages[${threadIndex}]: ${formatPromptScalar(threadMessage, 240)}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
sections.push("");
|
|
135
|
+
}
|
|
136
|
+
sections.push(
|
|
137
|
+
"Return JSON only as a single object with one results array entry per message in the same order.",
|
|
138
|
+
"Use this exact shape:",
|
|
139
|
+
'{"results":[{"classification":"ignore|info|notify|needs_reply|urgent","urgency":"low|medium|high","confidence":0.0,"reasoning":"brief explanation","suggestedResponse":null}]}',
|
|
140
|
+
// The `a|b|c` placeholders above list the allowed values — they are NOT
|
|
141
|
+
// literal output. Smaller/local models otherwise echo the pipe string
|
|
142
|
+
// ("urgent|ignore"), which fails strict validation. Be explicit.
|
|
143
|
+
'For "classification" output exactly one of: ignore, info, notify, needs_reply, urgent.',
|
|
144
|
+
'For "urgency" output exactly one of: low, medium, high.',
|
|
145
|
+
'Choose a single value per field \u2014 never output the "|" character or more than one option.',
|
|
146
|
+
"suggestedResponse may be a brief draft response when useful, otherwise null.",
|
|
147
|
+
"",
|
|
148
|
+
"No prose, markdown, code fences, or <think>."
|
|
149
|
+
);
|
|
150
|
+
return sections.join("\n");
|
|
151
|
+
}
|
|
152
|
+
const TRIAGE_CODE_FENCE_PATTERN = /^\s*```(?:json|json5)?\s*\r?\n?([\s\S]*?)\r?\n?```\s*$/i;
|
|
153
|
+
function parseTriageJsonArray(raw) {
|
|
154
|
+
let candidate = raw.trim();
|
|
155
|
+
if (candidate.length === 0) {
|
|
156
|
+
throw new InboxTriageClassificationError(
|
|
157
|
+
"Inbox classification returned an empty response."
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const thinkEnd = candidate.indexOf("</think>");
|
|
161
|
+
if (candidate.startsWith("<think>") && thinkEnd !== -1) {
|
|
162
|
+
candidate = candidate.slice(thinkEnd + "</think>".length).trim();
|
|
163
|
+
}
|
|
164
|
+
const fenced = candidate.match(TRIAGE_CODE_FENCE_PATTERN);
|
|
165
|
+
if (fenced) {
|
|
166
|
+
candidate = (fenced[1] ?? "").trim();
|
|
167
|
+
}
|
|
168
|
+
if (!candidate.startsWith("[")) {
|
|
169
|
+
throw new InboxTriageClassificationError(
|
|
170
|
+
"Inbox classification did not return a JSON array."
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
let parsed;
|
|
174
|
+
try {
|
|
175
|
+
parsed = JSON.parse(candidate);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
logger.warn(
|
|
178
|
+
{ src: "inbox-classifier", error: String(error) },
|
|
179
|
+
"[InboxTriageClassifier] failed to parse LLM classification JSON"
|
|
180
|
+
);
|
|
181
|
+
throw new InboxTriageClassificationError(
|
|
182
|
+
"Inbox classification JSON parsing failed."
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (!Array.isArray(parsed)) {
|
|
186
|
+
throw new InboxTriageClassificationError(
|
|
187
|
+
"Inbox classification did not return a JSON array."
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return parsed;
|
|
191
|
+
}
|
|
192
|
+
function asStructuredArray(value) {
|
|
193
|
+
return Array.isArray(value) ? value : null;
|
|
194
|
+
}
|
|
195
|
+
function parseTriageJsonObjectArray(raw) {
|
|
196
|
+
const parsed = parseJsonModelRecord(raw);
|
|
197
|
+
if (!parsed) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const directArray = asStructuredArray(parsed.results) ?? asStructuredArray(parsed.messages) ?? asStructuredArray(parsed.items) ?? asStructuredArray(parsed.classifications);
|
|
201
|
+
if (directArray) {
|
|
202
|
+
return directArray;
|
|
203
|
+
}
|
|
204
|
+
const classifications = asStructuredArray(parsed.classification);
|
|
205
|
+
if (!classifications) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const urgencies = asStructuredArray(parsed.urgency) ?? [];
|
|
209
|
+
const confidences = asStructuredArray(parsed.confidence) ?? [];
|
|
210
|
+
const reasonings = asStructuredArray(parsed.reasoning) ?? [];
|
|
211
|
+
const suggestedResponses = asStructuredArray(parsed.suggestedResponse) ?? asStructuredArray(parsed.suggested_response) ?? [];
|
|
212
|
+
return classifications.map((classification, index) => ({
|
|
213
|
+
classification,
|
|
214
|
+
urgency: urgencies[index],
|
|
215
|
+
confidence: confidences[index],
|
|
216
|
+
reasoning: reasonings[index],
|
|
217
|
+
suggestedResponse: suggestedResponses[index]
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
function parseTriageStructuredArray(raw) {
|
|
221
|
+
const jsonParsed = parseTriageJsonObjectArray(raw);
|
|
222
|
+
if (jsonParsed) {
|
|
223
|
+
return jsonParsed;
|
|
224
|
+
}
|
|
225
|
+
return parseTriageJsonArray(raw);
|
|
226
|
+
}
|
|
227
|
+
function normalizeOptionalString(value) {
|
|
228
|
+
if (typeof value !== "string") {
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
const trimmed = value.trim();
|
|
232
|
+
if (!trimmed || trimmed.toLowerCase() === "null") {
|
|
233
|
+
return void 0;
|
|
234
|
+
}
|
|
235
|
+
return trimmed;
|
|
236
|
+
}
|
|
237
|
+
function parseTriageResults(raw, expectedCount) {
|
|
238
|
+
const parsed = parseTriageStructuredArray(raw);
|
|
239
|
+
const results = [];
|
|
240
|
+
for (let i = 0; i < expectedCount; i++) {
|
|
241
|
+
const item = parsed[i];
|
|
242
|
+
if (!isRecord(item)) {
|
|
243
|
+
throw new InboxTriageClassificationError(
|
|
244
|
+
"Inbox classification omitted one or more messages."
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const classification = validClassification(item.classification);
|
|
248
|
+
const urgency = validUrgency(item.urgency);
|
|
249
|
+
const confidence = validConfidence(item.confidence);
|
|
250
|
+
if (!classification || !urgency || confidence === null) {
|
|
251
|
+
throw new InboxTriageClassificationError(
|
|
252
|
+
"Inbox classification returned invalid structured fields."
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
results.push({
|
|
256
|
+
classification,
|
|
257
|
+
urgency,
|
|
258
|
+
confidence,
|
|
259
|
+
reasoning: normalizeOptionalString(item.reasoning) ?? "",
|
|
260
|
+
suggestedResponse: normalizeOptionalString(
|
|
261
|
+
item.suggestedResponse ?? item.suggested_response
|
|
262
|
+
)
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return results;
|
|
266
|
+
}
|
|
267
|
+
const VALID_CLASSIFICATIONS = /* @__PURE__ */ new Set([
|
|
268
|
+
"ignore",
|
|
269
|
+
"info",
|
|
270
|
+
"notify",
|
|
271
|
+
"needs_reply",
|
|
272
|
+
"urgent"
|
|
273
|
+
]);
|
|
274
|
+
const VALID_URGENCIES = /* @__PURE__ */ new Set(["low", "medium", "high"]);
|
|
275
|
+
function validClassification(v) {
|
|
276
|
+
if (typeof v === "string") {
|
|
277
|
+
const normalized = v.trim().toLowerCase();
|
|
278
|
+
if (VALID_CLASSIFICATIONS.has(normalized)) {
|
|
279
|
+
return normalized;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
function validUrgency(v) {
|
|
285
|
+
if (typeof v === "string") {
|
|
286
|
+
const normalized = v.trim().toLowerCase();
|
|
287
|
+
if (VALID_URGENCIES.has(normalized)) {
|
|
288
|
+
return normalized;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
function validConfidence(v) {
|
|
294
|
+
const numeric = typeof v === "string" && v.trim().length > 0 ? Number(v.trim()) : v;
|
|
295
|
+
if (typeof numeric === "number" && Number.isFinite(numeric) && numeric >= 0 && numeric <= 1) {
|
|
296
|
+
return numeric;
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
export {
|
|
301
|
+
INBOX_TRIAGE_INSTRUCTIONS,
|
|
302
|
+
InboxTriageClassificationError,
|
|
303
|
+
buildTriagePrompt,
|
|
304
|
+
classifyMessages
|
|
305
|
+
};
|
|
306
|
+
//# sourceMappingURL=triage-classifier.js.map
|