iris-chatbot 5.2.0 → 5.3.1
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/package.json +1 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/src/app/api/chat/route.ts +889 -92
- package/template/src/app/api/contacts/search/route.ts +71 -0
- package/template/src/app/api/tool-approval/route.ts +30 -2
- package/template/src/app/globals.css +223 -1
- package/template/src/components/ChatView.tsx +247 -27
- package/template/src/components/Composer.tsx +11 -8
- package/template/src/components/MessageCard.tsx +549 -29
- package/template/src/components/SettingsModal.tsx +7 -0
- package/template/src/lib/data.ts +5 -0
- package/template/src/lib/tooling/approvals.ts +24 -9
- package/template/src/lib/tooling/tools/communication.ts +178 -31
- package/template/src/lib/types.ts +12 -2
|
@@ -44,6 +44,8 @@ const LOCAL_TOOL_SYSTEM_INSTRUCTIONS = [
|
|
|
44
44
|
"Calendar: You have direct access to the user's Apple Calendar via calendar_list_events and related tools. You CAN see their events—call the tools; do not say you cannot access or see their calendar. Never ask the user to paste their calendar, export it, or send a screenshot; you already have access. When they ask what's on the calendar, what's happening this/next weekend/month, or say 'check' or 'you can see', call calendar_list_events immediately with from/to for the range (e.g. next month → from: 'today', to: 'end of next month'; specific day → YYYY-MM-DD for both). Always pass calendar: 'all'. from/to support 'today', 'tomorrow', 'next week', 'end of next week', 'next month', 'end of next month'. Report the exact queried range from the tool result. For move/delete/reschedule: call calendar_list_events first to get uids. For rescheduling to a new time on the same calendar use calendar_update_event (preserves recurrence); for delete use calendar_delete_event; use calendar_move_event only when update is not sufficient (e.g. user says move and you need delete+recreate). When the user says to add or put something on their calendar (e.g. 'Add 1:1 with Alex for Thursday at 9am to my calendar', 'Put team standup on Friday at 10', 'Schedule meeting with John next Monday at 2pm'), use calendar_create_event with title = only the event name (e.g. '1:1 with Alex', 'team standup', 'meeting with John') and start = the date/time phrase (e.g. 'Thursday at 9am', 'Friday at 10', 'next Monday at 2pm'). Do not put the date/time in the title.",
|
|
45
45
|
"Files and Folders: You HAVE direct access to the user's files via file_list and file_find. Never say you cannot see, access, or list their files—call the tools. When the user asks 'what files do I have,' 'files from the last 7 days,' 'recent files,' or similar: call file_list immediately with path set to a folder (e.g. ~/Downloads, ~/Desktop, ~/Documents—or call once per root if you want all) and modifiedInLastDays: 7 (or the number of days they said). You have full file system access via file_list, file_mkdir, file_move, file_copy, file_delete_to_trash, file_batch_move, and file_find. When the user gives a folder or file name (e.g. 'us debt clock'), use file_find with searchPath '~' and the name as given. When moving MULTIPLE files, use file_batch_move with { operations: [ ... ] } or { destination, sources }. For organizing: file_find if location unknown, then file_list (use modifiedInLastDays for 'recent'), then file_mkdir/file_batch_move as needed. Never refuse file tasks or ask the user to use Finder manually—you have the tools.",
|
|
46
46
|
"For iMessage/text requests, use messages_send; if contact matching is ambiguous, ask one concise clarification with candidate names.",
|
|
47
|
+
"For requests to write, draft, compose, or send an email/message to someone, call mail_send or messages_send so the user can review/edit in the pre-send draft template when enabled.",
|
|
48
|
+
"When drafting email or message content, write complete, natural prose tailored to the request. Avoid placeholder text unless the user explicitly asks for a template with placeholders.",
|
|
47
49
|
"For media requests, assume Apple Music unless the user specifies otherwise.",
|
|
48
50
|
"For note creation, write structured markdown with clear headings, bolded key labels, and tables when they improve clarity.",
|
|
49
51
|
"If the user requests multiple actions in one prompt, execute every requested action sequentially before your final response.",
|
|
@@ -117,16 +119,50 @@ function buildDetectedActionHint(messages: ChatMessageInput[]): string | null {
|
|
|
117
119
|
return `Detected user actions:\n${lines.join("\n")}\nExecute each action in order and confirm each result.`;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
type DraftEditPromptContext = {
|
|
123
|
+
toolName: "mail_send" | "messages_send";
|
|
124
|
+
to: string[];
|
|
125
|
+
cc: string[];
|
|
126
|
+
subject: string;
|
|
127
|
+
body: string;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function buildDraftEditPromptContextMessage(context: DraftEditPromptContext): string {
|
|
131
|
+
const lines = [
|
|
132
|
+
"You are editing an existing pre-send draft.",
|
|
133
|
+
`Channel: ${context.toolName === "mail_send" ? "email" : "message"}.`,
|
|
134
|
+
`Current To: ${context.to.join(", ") || "(none)"}`,
|
|
135
|
+
];
|
|
136
|
+
if (context.toolName === "mail_send") {
|
|
137
|
+
lines.push(`Current Cc: ${context.cc.join(", ") || "(none)"}`);
|
|
138
|
+
lines.push(`Current Subject: ${context.subject || "(none)"}`);
|
|
139
|
+
}
|
|
140
|
+
lines.push("Current Body:");
|
|
141
|
+
lines.push(context.body || "(empty)");
|
|
142
|
+
lines.push("");
|
|
143
|
+
lines.push("Apply the user's latest instruction to this draft.");
|
|
144
|
+
lines.push("Keep recipients/channel unchanged unless user explicitly requests changes.");
|
|
145
|
+
lines.push("Use mail_send or messages_send with updated args so the draft template refreshes.");
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildToolSystemPrompt(
|
|
150
|
+
system: string | undefined,
|
|
151
|
+
messages: ChatMessageInput[],
|
|
152
|
+
draftEditContext?: DraftEditPromptContext,
|
|
153
|
+
): string {
|
|
121
154
|
const base = system?.trim();
|
|
122
155
|
const actionHint = buildDetectedActionHint(messages);
|
|
156
|
+
const draftEditHint = draftEditContext ? buildDraftEditPromptContextMessage(draftEditContext) : null;
|
|
157
|
+
const extraBlocks = [actionHint, draftEditHint].filter(Boolean).join("\n\n");
|
|
158
|
+
|
|
123
159
|
if (!base) {
|
|
124
|
-
return
|
|
125
|
-
? `${LOCAL_TOOL_SYSTEM_INSTRUCTIONS}\n\n${
|
|
160
|
+
return extraBlocks
|
|
161
|
+
? `${LOCAL_TOOL_SYSTEM_INSTRUCTIONS}\n\n${extraBlocks}`
|
|
126
162
|
: LOCAL_TOOL_SYSTEM_INSTRUCTIONS;
|
|
127
163
|
}
|
|
128
|
-
return
|
|
129
|
-
? `${base}\n\n${LOCAL_TOOL_SYSTEM_INSTRUCTIONS}\n\n${
|
|
164
|
+
return extraBlocks
|
|
165
|
+
? `${base}\n\n${LOCAL_TOOL_SYSTEM_INSTRUCTIONS}\n\n${extraBlocks}`
|
|
130
166
|
: `${base}\n\n${LOCAL_TOOL_SYSTEM_INSTRUCTIONS}`;
|
|
131
167
|
}
|
|
132
168
|
|
|
@@ -210,6 +246,68 @@ type RuntimeConnection = {
|
|
|
210
246
|
supportsTools: boolean;
|
|
211
247
|
};
|
|
212
248
|
|
|
249
|
+
type NormalizedRequestMeta = {
|
|
250
|
+
threadId?: string;
|
|
251
|
+
conversationId?: string;
|
|
252
|
+
assistantMessageId?: string;
|
|
253
|
+
forceToolRouting: boolean;
|
|
254
|
+
draftEditContext?: DraftEditPromptContext;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
function sanitizeChatRequestMeta(meta: ChatRequest["meta"] | undefined): NormalizedRequestMeta {
|
|
258
|
+
const threadId = typeof meta?.threadId === "string" && meta.threadId.trim()
|
|
259
|
+
? meta.threadId.trim()
|
|
260
|
+
: undefined;
|
|
261
|
+
const conversationId = typeof meta?.conversationId === "string" && meta.conversationId.trim()
|
|
262
|
+
? meta.conversationId.trim()
|
|
263
|
+
: undefined;
|
|
264
|
+
const assistantMessageId = typeof meta?.assistantMessageId === "string" && meta.assistantMessageId.trim()
|
|
265
|
+
? meta.assistantMessageId.trim()
|
|
266
|
+
: undefined;
|
|
267
|
+
const forceToolRouting = meta?.forceToolRouting === true;
|
|
268
|
+
|
|
269
|
+
let draftEditContext: DraftEditPromptContext | undefined;
|
|
270
|
+
const rawDraft = meta?.draftEditContext;
|
|
271
|
+
if (rawDraft && typeof rawDraft === "object" && !Array.isArray(rawDraft)) {
|
|
272
|
+
const record = rawDraft as {
|
|
273
|
+
toolName?: unknown;
|
|
274
|
+
to?: unknown;
|
|
275
|
+
cc?: unknown;
|
|
276
|
+
subject?: unknown;
|
|
277
|
+
body?: unknown;
|
|
278
|
+
};
|
|
279
|
+
const toolName =
|
|
280
|
+
record.toolName === "mail_send" || record.toolName === "messages_send"
|
|
281
|
+
? record.toolName
|
|
282
|
+
: null;
|
|
283
|
+
const to = Array.isArray(record.to)
|
|
284
|
+
? record.to.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
285
|
+
: [];
|
|
286
|
+
const cc = Array.isArray(record.cc)
|
|
287
|
+
? record.cc.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
288
|
+
: [];
|
|
289
|
+
const subject = typeof record.subject === "string" ? record.subject : "";
|
|
290
|
+
const body = typeof record.body === "string" ? record.body : "";
|
|
291
|
+
if (toolName && to.length > 0 && body.trim().length > 0) {
|
|
292
|
+
draftEditContext = {
|
|
293
|
+
toolName,
|
|
294
|
+
to,
|
|
295
|
+
cc,
|
|
296
|
+
subject,
|
|
297
|
+
body,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
threadId,
|
|
304
|
+
conversationId,
|
|
305
|
+
assistantMessageId,
|
|
306
|
+
forceToolRouting,
|
|
307
|
+
draftEditContext,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
213
311
|
function headersArrayToRecord(headers: RuntimeConnection["headers"]): Record<string, string> | undefined {
|
|
214
312
|
if (!Array.isArray(headers) || headers.length === 0) {
|
|
215
313
|
return undefined;
|
|
@@ -311,6 +409,10 @@ function normalizeLocalTools(settings: LocalToolsSettings | undefined): LocalToo
|
|
|
311
409
|
enabled: Boolean(settings.enabled),
|
|
312
410
|
approvalMode: settings.approvalMode ?? DEFAULT_LOCAL_TOOLS_SETTINGS.approvalMode,
|
|
313
411
|
safetyProfile: settings.safetyProfile ?? DEFAULT_LOCAL_TOOLS_SETTINGS.safetyProfile,
|
|
412
|
+
preSendDraftReview:
|
|
413
|
+
typeof settings.preSendDraftReview === "boolean"
|
|
414
|
+
? settings.preSendDraftReview
|
|
415
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.preSendDraftReview,
|
|
314
416
|
allowedRoots:
|
|
315
417
|
Array.isArray(settings.allowedRoots) && settings.allowedRoots.length > 0
|
|
316
418
|
? settings.allowedRoots
|
|
@@ -363,6 +465,7 @@ function normalizeLocalTools(settings: LocalToolsSettings | undefined): LocalToo
|
|
|
363
465
|
normalized.enabled === true &&
|
|
364
466
|
normalized.approvalMode === "always_confirm_writes" &&
|
|
365
467
|
normalized.safetyProfile === "balanced" &&
|
|
468
|
+
normalized.preSendDraftReview === DEFAULT_LOCAL_TOOLS_SETTINGS.preSendDraftReview &&
|
|
366
469
|
normalized.allowedRoots.length === DEFAULT_LOCAL_TOOLS_SETTINGS.allowedRoots.length &&
|
|
367
470
|
normalized.allowedRoots.every(
|
|
368
471
|
(value, index) => value === DEFAULT_LOCAL_TOOLS_SETTINGS.allowedRoots[index],
|
|
@@ -535,10 +638,103 @@ function normalizeWebSourcesEnabled(value: boolean | undefined): boolean {
|
|
|
535
638
|
return value !== false;
|
|
536
639
|
}
|
|
537
640
|
|
|
641
|
+
type WebSearchMode = "off" | "auto" | "required";
|
|
642
|
+
|
|
538
643
|
function supportsWebSourcesViaOpenAI(connection: RuntimeConnection): boolean {
|
|
539
644
|
return connection.kind === "builtin" && connection.provider === "openai";
|
|
540
645
|
}
|
|
541
646
|
|
|
647
|
+
function getLastUserMessageText(messages: ChatMessageInput[]): string {
|
|
648
|
+
return [...messages].reverse().find((message) => message.role === "user")?.content?.trim() ?? "";
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function decideWebSearchMode(params: {
|
|
652
|
+
enabled: boolean;
|
|
653
|
+
connection: RuntimeConnection;
|
|
654
|
+
messages: ChatMessageInput[];
|
|
655
|
+
}): WebSearchMode {
|
|
656
|
+
if (!params.enabled || !supportsWebSourcesViaOpenAI(params.connection)) {
|
|
657
|
+
return "off";
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const text = getLastUserMessageText(params.messages).replace(/\s+/g, " ").trim().toLowerCase();
|
|
661
|
+
if (!text) {
|
|
662
|
+
return "auto";
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const disableSearchRequested =
|
|
666
|
+
/\b(?:no|without|skip)\s+(?:web\s+)?(?:search|browsing|browse|lookup|look up)\b/.test(text) ||
|
|
667
|
+
/\b(?:don't|do not)\s+(?:web\s+)?(?:search|browse|look up|lookup)\b/.test(text) ||
|
|
668
|
+
/\boffline only\b/.test(text);
|
|
669
|
+
if (disableSearchRequested) {
|
|
670
|
+
return "off";
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const explicitSearchRequested =
|
|
674
|
+
/\bsearch(?:\s+the)?\s+web\b/.test(text) ||
|
|
675
|
+
/\blook(?:\s+it)?\s+up\b/.test(text) ||
|
|
676
|
+
/\bcheck(?:\s+it)?\s+online\b/.test(text) ||
|
|
677
|
+
/\bverify(?:\s+it)?\s+online\b/.test(text) ||
|
|
678
|
+
/\bbrowse(?:\s+the)?\s+web\b/.test(text) ||
|
|
679
|
+
/\bgoogle\s+(?:it|this)\b/.test(text);
|
|
680
|
+
if (explicitSearchRequested) {
|
|
681
|
+
return "required";
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const asksForSources =
|
|
685
|
+
/\b(?:source|sources|cite|citation|citations|reference|references|link|links)\b/.test(text);
|
|
686
|
+
const freshnessSignals =
|
|
687
|
+
/\b(?:latest|current|currently|today|right now|as of|recent|recently|up[- ]to[- ]date|breaking)\b/.test(
|
|
688
|
+
text,
|
|
689
|
+
);
|
|
690
|
+
const volatileDomainSignals =
|
|
691
|
+
/\b(?:price|pricing|cost|fee|subscription|per token|rate limit|stock|share price|market cap|forecast|weather|score|standings|odds|election|poll)\b/.test(
|
|
692
|
+
text,
|
|
693
|
+
);
|
|
694
|
+
const leadershipSignals =
|
|
695
|
+
/\b(?:who is|current)\s+(?:the\s+)?(?:ceo|president|prime minister|governor|mayor)\b/.test(text);
|
|
696
|
+
const metricSignals =
|
|
697
|
+
/\b(?:average|median|how many|number of|count|rate|percentage|percent|statistics?|stats?)\b/.test(
|
|
698
|
+
text,
|
|
699
|
+
);
|
|
700
|
+
const metricEntitySignals =
|
|
701
|
+
/\b(?:users?|people|prompts?|requests?|queries?|api|model|gpt|traffic|revenue|downloads?|adoption|daily|weekly|monthly|yearly|per day|per week|per month)\b/.test(
|
|
702
|
+
text,
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
if (
|
|
706
|
+
asksForSources ||
|
|
707
|
+
freshnessSignals ||
|
|
708
|
+
volatileDomainSignals ||
|
|
709
|
+
leadershipSignals ||
|
|
710
|
+
(metricSignals && metricEntitySignals)
|
|
711
|
+
) {
|
|
712
|
+
return "required";
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const likelyFactualQuestion =
|
|
716
|
+
text.includes("?") || /^(?:what|who|when|where|which|how)\b/.test(text);
|
|
717
|
+
const uncertaintySignals =
|
|
718
|
+
/\b(?:not sure|unsure|uncertain|verify|double-check|confirm|accurate|accuracy)\b/.test(text);
|
|
719
|
+
if (likelyFactualQuestion && uncertaintySignals) {
|
|
720
|
+
return "auto";
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return "off";
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function withWebSearchGuidance(system: string | undefined, mode: WebSearchMode): string | undefined {
|
|
727
|
+
const trimmed = system?.trim() || "";
|
|
728
|
+
if (mode === "off") {
|
|
729
|
+
return trimmed || undefined;
|
|
730
|
+
}
|
|
731
|
+
const guidance =
|
|
732
|
+
mode === "required"
|
|
733
|
+
? "Web search is required for this turn. Use web_search before answering and cite the sources you relied on."
|
|
734
|
+
: "Use web_search when the request depends on current or uncertain facts (pricing, statistics, recency). For stable/general knowledge, answer directly without web_search.";
|
|
735
|
+
return trimmed ? `${trimmed}\n\n${guidance}` : guidance;
|
|
736
|
+
}
|
|
737
|
+
|
|
542
738
|
function mergeCitationSources(
|
|
543
739
|
target: Map<string, ProviderCitationSource>,
|
|
544
740
|
sources: ProviderCitationSource[] | undefined,
|
|
@@ -731,6 +927,10 @@ function summarizeApprovalReason(toolName: string, args: Record<string, unknown>
|
|
|
731
927
|
return `Approval required for ${toolName}.`;
|
|
732
928
|
}
|
|
733
929
|
|
|
930
|
+
function isPreSendDraftTool(toolName: string): boolean {
|
|
931
|
+
return toolName === "mail_send" || toolName === "messages_send";
|
|
932
|
+
}
|
|
933
|
+
|
|
734
934
|
function normalizeAliasText(input: string): string {
|
|
735
935
|
return input
|
|
736
936
|
.toLowerCase()
|
|
@@ -994,6 +1194,452 @@ function parseDirectMessageIntent(params: {
|
|
|
994
1194
|
};
|
|
995
1195
|
}
|
|
996
1196
|
|
|
1197
|
+
type DraftCommunicationIntent = {
|
|
1198
|
+
intentType: "explicit_draft_request" | "send_request";
|
|
1199
|
+
toolName: "mail_send" | "messages_send";
|
|
1200
|
+
args: Record<string, unknown>;
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
type DraftHintKind = "body" | "topic";
|
|
1204
|
+
type DraftCommunicationIntentType = "explicit_draft_request" | "send_request" | "draft_edit_request";
|
|
1205
|
+
|
|
1206
|
+
function isLikelyDraftEditInstruction(input: string): boolean {
|
|
1207
|
+
const text = input.replace(/\s+/g, " ").trim();
|
|
1208
|
+
if (!text) {
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
if (
|
|
1213
|
+
/\b(?:send|write|draft|compose|create)\b[\s\S]{0,32}\b(?:email|mail|message|text)\b[\s\S]{0,40}\b(?:to|for)\b/i
|
|
1214
|
+
.test(text)
|
|
1215
|
+
) {
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const editVerb = /\b(edit|rewrite|change|update|improve|polish|adjust|expand|shorten|lengthen|make)\b/i.test(text);
|
|
1220
|
+
const draftField = /\b(email|mail|message|text|draft|subject|body)\b/i.test(text);
|
|
1221
|
+
const toneOrLength =
|
|
1222
|
+
/\b(longer|shorter|more formal|less formal|more casual|more polite|more professional|concise|friendlier)\b/i
|
|
1223
|
+
.test(text);
|
|
1224
|
+
const pronounEdit = /\b(make|rewrite|edit|change|update)\s+(?:it|this)\b/i.test(text);
|
|
1225
|
+
|
|
1226
|
+
return (editVerb && draftField) || toneOrLength || pronounEdit;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function classifyDraftCommunicationIntent(input: string): DraftCommunicationIntentType | null {
|
|
1230
|
+
const text = input.replace(/\s+/g, " ").trim();
|
|
1231
|
+
if (!text) {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
if (isLikelyDraftEditInstruction(text)) {
|
|
1235
|
+
return "draft_edit_request";
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const communicationMention = /\b(email|mail|text|message|imessage|sms|draft)\b/i.test(text);
|
|
1239
|
+
if (!communicationMention) {
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const explicitDraftPattern =
|
|
1244
|
+
/\b(?:write|draft|compose|create)\b/i.test(text) ||
|
|
1245
|
+
/\b(?:email|mail|message|text)\s+draft\b/i.test(text) ||
|
|
1246
|
+
/\bdraft\s+(?:an?\s+)?(?:email|mail|message|text)\b/i.test(text);
|
|
1247
|
+
if (explicitDraftPattern) {
|
|
1248
|
+
return "explicit_draft_request";
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (/\bsend\b/i.test(text) || /^\s*(?:email|mail|text(?:\s+message)?|message)\b/i.test(text)) {
|
|
1252
|
+
return "send_request";
|
|
1253
|
+
}
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function normalizeRecipientList(
|
|
1258
|
+
recipientChunk: string,
|
|
1259
|
+
peopleAliases: MemoryContextPayload["aliases"]["people"] | undefined,
|
|
1260
|
+
resolveAliases: boolean,
|
|
1261
|
+
): string[] {
|
|
1262
|
+
const recipients = recipientChunk
|
|
1263
|
+
.replace(/^to\s+/i, "")
|
|
1264
|
+
.split(/\s*,\s*|\s+and\s+/i)
|
|
1265
|
+
.map((value) =>
|
|
1266
|
+
value
|
|
1267
|
+
.trim()
|
|
1268
|
+
.replace(/[,:;]+$/g, "")
|
|
1269
|
+
.replace(/^['"]|['"]$/g, ""),
|
|
1270
|
+
)
|
|
1271
|
+
.filter(Boolean)
|
|
1272
|
+
.slice(0, 3);
|
|
1273
|
+
|
|
1274
|
+
const normalized = recipients
|
|
1275
|
+
.map((value) => {
|
|
1276
|
+
const compact = normalizeAliasText(value);
|
|
1277
|
+
if (!compact || compact === "me" || compact === "myself" || compact === "us") {
|
|
1278
|
+
return "";
|
|
1279
|
+
}
|
|
1280
|
+
if (!resolveAliases) {
|
|
1281
|
+
return value;
|
|
1282
|
+
}
|
|
1283
|
+
return resolvePersonAlias(value, peopleAliases) ?? value;
|
|
1284
|
+
})
|
|
1285
|
+
.filter(Boolean);
|
|
1286
|
+
return [...new Set(normalized)];
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function normalizeDraftText(input: string): string {
|
|
1290
|
+
return input
|
|
1291
|
+
.replace(/\s+/g, " ")
|
|
1292
|
+
.trim()
|
|
1293
|
+
.replace(/^['"]|['"]$/g, "")
|
|
1294
|
+
.replace(/^[,.;:\-]+|[,.;:\-]+$/g, "")
|
|
1295
|
+
.trim();
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function capitalizeFirst(input: string): string {
|
|
1299
|
+
if (!input) {
|
|
1300
|
+
return input;
|
|
1301
|
+
}
|
|
1302
|
+
return input.charAt(0).toUpperCase() + input.slice(1);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function toSentence(input: string): string {
|
|
1306
|
+
const normalized = normalizeDraftText(input);
|
|
1307
|
+
if (!normalized) {
|
|
1308
|
+
return "";
|
|
1309
|
+
}
|
|
1310
|
+
const withPronoun = normalized
|
|
1311
|
+
.replace(/\bi\b/g, "I")
|
|
1312
|
+
.replace(/^cant\b/i, "can't")
|
|
1313
|
+
.replace(/^dont\b/i, "don't")
|
|
1314
|
+
.replace(/^wont\b/i, "won't")
|
|
1315
|
+
.replace(/^im\b/i, "I'm");
|
|
1316
|
+
const withSubject = /^(can't|cannot|won't|will not|unable|am unable|not able|couldn't|didn't|don't)\b/i.test(withPronoun)
|
|
1317
|
+
? `I ${withPronoun}`
|
|
1318
|
+
: withPronoun;
|
|
1319
|
+
const capitalized = capitalizeFirst(withSubject);
|
|
1320
|
+
return /[.!?]$/.test(capitalized) ? capitalized : `${capitalized}.`;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function normalizeNameLabel(input: string): string {
|
|
1324
|
+
return input
|
|
1325
|
+
.split(/\s+/)
|
|
1326
|
+
.map((token) => {
|
|
1327
|
+
if (!token) {
|
|
1328
|
+
return token;
|
|
1329
|
+
}
|
|
1330
|
+
if (token.includes("@") || /\d/.test(token)) {
|
|
1331
|
+
return token;
|
|
1332
|
+
}
|
|
1333
|
+
return token.charAt(0).toUpperCase() + token.slice(1).toLowerCase();
|
|
1334
|
+
})
|
|
1335
|
+
.join(" ");
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function toClause(input: string): string {
|
|
1339
|
+
const sentence = toSentence(input);
|
|
1340
|
+
return sentence ? sentence.replace(/[.!?]+$/g, "").trim() : "";
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function ensureFirstPersonClause(input: string): string {
|
|
1344
|
+
const clause = toClause(input);
|
|
1345
|
+
if (!clause) {
|
|
1346
|
+
return "";
|
|
1347
|
+
}
|
|
1348
|
+
if (/^i\b/i.test(clause)) {
|
|
1349
|
+
return `I${clause.slice(1)}`;
|
|
1350
|
+
}
|
|
1351
|
+
if (/^(can't|cannot|won't|will not|am|have|had|need|would|could|should|must|did|do|was|were)\b/i.test(clause)) {
|
|
1352
|
+
return `I ${clause}`;
|
|
1353
|
+
}
|
|
1354
|
+
return clause;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function inferEmailSalutation(recipient: string): string {
|
|
1358
|
+
const compact = normalizeAliasText(recipient);
|
|
1359
|
+
if (/^(?:a|an|the)?\s*(?:professor|teacher|instructor|ta|lecturer|advisor)\b/i.test(compact)) {
|
|
1360
|
+
return "Dear Professor [Last Name],";
|
|
1361
|
+
}
|
|
1362
|
+
if (/^(?:a|an|the)?\s*(?:dr|doctor)\b/i.test(compact)) {
|
|
1363
|
+
return "Dear Dr. [Last Name],";
|
|
1364
|
+
}
|
|
1365
|
+
if (/@|\d{7,}/.test(recipient)) {
|
|
1366
|
+
return "Hello,";
|
|
1367
|
+
}
|
|
1368
|
+
const label = normalizeNameLabel(recipient).replace(/^(?:A|An|The)\s+/g, "").trim();
|
|
1369
|
+
if (!label) {
|
|
1370
|
+
return "Hello,";
|
|
1371
|
+
}
|
|
1372
|
+
return `Dear ${label},`;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function inferEmailSubject(params: { explicitSubject: string; bodyText: string; topicText: string }): string {
|
|
1376
|
+
const explicit = normalizeDraftText(params.explicitSubject);
|
|
1377
|
+
if (explicit) {
|
|
1378
|
+
return explicit;
|
|
1379
|
+
}
|
|
1380
|
+
const basis = normalizeDraftText(params.bodyText || params.topicText);
|
|
1381
|
+
const lower = basis.toLowerCase();
|
|
1382
|
+
if (!basis) {
|
|
1383
|
+
return "Quick Update";
|
|
1384
|
+
}
|
|
1385
|
+
if (/can't make it today|cannot make it today|unable to make it today|won't be able to make it today/.test(lower)) {
|
|
1386
|
+
return "Unable to Make It Today";
|
|
1387
|
+
}
|
|
1388
|
+
if (/can't make it|cannot make it|unable to make it|won't be able to/.test(lower)) {
|
|
1389
|
+
return "Unable to Make It";
|
|
1390
|
+
}
|
|
1391
|
+
if (/miss(ing)? class.*today|today.*miss(ing)? class/.test(lower)) {
|
|
1392
|
+
return "Absence from Class Today";
|
|
1393
|
+
}
|
|
1394
|
+
if (/miss(ing)? class|absence|absent/.test(lower)) {
|
|
1395
|
+
return "Absence from Class";
|
|
1396
|
+
}
|
|
1397
|
+
if (/running late|late/.test(lower)) {
|
|
1398
|
+
return "Running Late";
|
|
1399
|
+
}
|
|
1400
|
+
const clipped = basis.length > 64 ? basis.slice(0, 64).trim() : basis;
|
|
1401
|
+
return titleCase(clipped.replace(/[.!?]+$/g, ""));
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function extractRecipientAndHint(input: string): {
|
|
1405
|
+
recipientChunk: string;
|
|
1406
|
+
hintKind: DraftHintKind | null;
|
|
1407
|
+
hintText: string;
|
|
1408
|
+
} {
|
|
1409
|
+
const compact = normalizeDraftText(input);
|
|
1410
|
+
if (!compact) {
|
|
1411
|
+
return { recipientChunk: "", hintKind: null, hintText: "" };
|
|
1412
|
+
}
|
|
1413
|
+
const lowered = compact.toLowerCase();
|
|
1414
|
+
const separators: Array<{ token: string; kind: DraftHintKind }> = [
|
|
1415
|
+
{ token: " that says ", kind: "body" },
|
|
1416
|
+
{ token: " saying that ", kind: "body" },
|
|
1417
|
+
{ token: " saying ", kind: "body" },
|
|
1418
|
+
{ token: " with message ", kind: "body" },
|
|
1419
|
+
{ token: " message: ", kind: "body" },
|
|
1420
|
+
{ token: " about ", kind: "topic" },
|
|
1421
|
+
{ token: " regarding ", kind: "topic" },
|
|
1422
|
+
{ token: " re: ", kind: "topic" },
|
|
1423
|
+
{ token: " that ", kind: "body" },
|
|
1424
|
+
];
|
|
1425
|
+
|
|
1426
|
+
let bestIndex = -1;
|
|
1427
|
+
let bestSeparator: { token: string; kind: DraftHintKind } | null = null;
|
|
1428
|
+
for (const separator of separators) {
|
|
1429
|
+
const index = lowered.indexOf(separator.token);
|
|
1430
|
+
if (index <= 0) {
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
if (bestIndex === -1 || index < bestIndex) {
|
|
1434
|
+
bestIndex = index;
|
|
1435
|
+
bestSeparator = separator;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
if (!bestSeparator) {
|
|
1440
|
+
return {
|
|
1441
|
+
recipientChunk: compact,
|
|
1442
|
+
hintKind: null,
|
|
1443
|
+
hintText: "",
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
return {
|
|
1447
|
+
recipientChunk: compact.slice(0, bestIndex).trim(),
|
|
1448
|
+
hintKind: bestSeparator.kind,
|
|
1449
|
+
hintText: compact.slice(bestIndex + bestSeparator.token.length).trim(),
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function buildEmailDraftBody(params: {
|
|
1454
|
+
topicText: string;
|
|
1455
|
+
explicitBody: string;
|
|
1456
|
+
recipient: string;
|
|
1457
|
+
sourceText?: string;
|
|
1458
|
+
}): string {
|
|
1459
|
+
const salutation = inferEmailSalutation(params.recipient);
|
|
1460
|
+
const combined = normalizeDraftText([params.sourceText ?? "", params.explicitBody, params.topicText].join(" "));
|
|
1461
|
+
const loweredCombined = combined.toLowerCase();
|
|
1462
|
+
const wantsLong = /\b(long|detailed|thorough)\b/i.test(loweredCombined);
|
|
1463
|
+
const wantsShort = /\b(short|brief|concise)\b/i.test(loweredCombined);
|
|
1464
|
+
const absenceLike = /\b(can't make|cannot make|unable to|won't be able|miss(?:ing)? class|absence|absent|not attend)\b/i
|
|
1465
|
+
.test(loweredCombined);
|
|
1466
|
+
|
|
1467
|
+
if (absenceLike) {
|
|
1468
|
+
const baselineAbsence = /\btoday\b/i.test(loweredCombined)
|
|
1469
|
+
? "I won't be able to attend class today"
|
|
1470
|
+
: "I won't be able to attend class";
|
|
1471
|
+
const absenceClause = ensureFirstPersonClause(params.explicitBody) || baselineAbsence;
|
|
1472
|
+
const detailParagraph = wantsLong
|
|
1473
|
+
? "I will review the material covered and catch up on any notes or assignments. If there is anything specific you would like me to prioritize, please let me know."
|
|
1474
|
+
: "I will review the material covered and catch up on any notes or assignments.";
|
|
1475
|
+
|
|
1476
|
+
if (wantsShort) {
|
|
1477
|
+
return `${salutation}\n\n${absenceClause}.\n\nThank you for your understanding.\n\nBest,\n[Your Name]`;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
return `${salutation}\n\nI hope you are doing well. I wanted to let you know that ${absenceClause}.\n\n${detailParagraph}\n\nThank you for your understanding.\n\nBest,\n[Your Name]`;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const explicitSentence = toSentence(params.explicitBody);
|
|
1484
|
+
if (explicitSentence) {
|
|
1485
|
+
return `${salutation}\n\n${explicitSentence}\n\nPlease let me know if you need anything else from me.\n\nBest,\n[Your Name]`;
|
|
1486
|
+
}
|
|
1487
|
+
const topicClause = toClause(params.topicText);
|
|
1488
|
+
if (topicClause) {
|
|
1489
|
+
const detailLine = wantsLong
|
|
1490
|
+
? "I wanted to share a bit more context so you have the full picture."
|
|
1491
|
+
: "I wanted to share a quick update.";
|
|
1492
|
+
return `${salutation}\n\n${detailLine} I wanted to reach out regarding ${topicClause}.\n\nThank you for your time.\n\nBest,\n[Your Name]`;
|
|
1493
|
+
}
|
|
1494
|
+
return `${salutation}\n\nI wanted to share a quick update.\n\nThank you for your time.\n\nBest,\n[Your Name]`;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function buildMessageDraftBody(params: { topicText: string; explicitBody: string; recipient: string }): string {
|
|
1498
|
+
const firstLabel = normalizeNameLabel(params.recipient).split(/\s+/)[0] || params.recipient;
|
|
1499
|
+
const explicitSentence = toSentence(params.explicitBody);
|
|
1500
|
+
if (explicitSentence) {
|
|
1501
|
+
const apology = /can't make|cannot make|unable to|won't be able|miss class|absence|late/i.test(explicitSentence)
|
|
1502
|
+
? " Sorry for the short notice."
|
|
1503
|
+
: "";
|
|
1504
|
+
return `Hey ${firstLabel}, ${explicitSentence}${apology}`;
|
|
1505
|
+
}
|
|
1506
|
+
const topicSentence = toSentence(params.topicText);
|
|
1507
|
+
if (topicSentence) {
|
|
1508
|
+
return `Hey ${firstLabel}, just wanted to message you about ${topicSentence}`;
|
|
1509
|
+
}
|
|
1510
|
+
return `Hey ${firstLabel}, wanted to check in when you have a minute.`;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function parseDraftCommunicationIntent(params: {
|
|
1514
|
+
text: string;
|
|
1515
|
+
peopleAliases?: MemoryContextPayload["aliases"]["people"];
|
|
1516
|
+
}): DraftCommunicationIntent | null {
|
|
1517
|
+
const text = params.text.replace(/\s+/g, " ").trim();
|
|
1518
|
+
if (!text) {
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
const intentType = classifyDraftCommunicationIntent(text);
|
|
1522
|
+
if (intentType === "draft_edit_request" || !intentType) {
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
const actionVerbMention = /\b(write|draft|compose|create|send)\b/i.test(text);
|
|
1526
|
+
const communicationNounMention = /\b(email|mail|text|message|imessage|sms|draft)\b/i.test(text);
|
|
1527
|
+
if (!actionVerbMention && !communicationNounMention) {
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
const emailMention = /\b(email|mail)\b/i.test(text);
|
|
1532
|
+
const messageMention = /\b(text|message|imessage|sms)\b/i.test(text);
|
|
1533
|
+
const genericDraftToMention =
|
|
1534
|
+
/(?:write|draft|compose|create)(?:\s+me)?\s+(?:an?\s+)?(?:[a-z]+\s+){0,3}draft\s+(?:for|to)\b/i.test(text) ||
|
|
1535
|
+
/\bdraft\s+(?:for|to)\b/i.test(text) ||
|
|
1536
|
+
/(?:write|draft|compose|create)\s+to\b/i.test(text);
|
|
1537
|
+
if (!emailMention && !messageMention && !genericDraftToMention) {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
let channel: "email" | "message" = emailMention ? "email" : "message";
|
|
1542
|
+
if (!emailMention && !messageMention) {
|
|
1543
|
+
channel = "email";
|
|
1544
|
+
}
|
|
1545
|
+
if (emailMention && messageMention) {
|
|
1546
|
+
const lowered = text.toLowerCase();
|
|
1547
|
+
const emailIndex = lowered.search(/\b(email|mail)\b/);
|
|
1548
|
+
const messageIndex = lowered.search(/\b(text|message|imessage|sms)\b/);
|
|
1549
|
+
channel = emailIndex !== -1 && (messageIndex === -1 || emailIndex < messageIndex) ? "email" : "message";
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
let recipientChunk = "";
|
|
1553
|
+
let hintKind: DraftHintKind | null = null;
|
|
1554
|
+
let hintText = "";
|
|
1555
|
+
const recipientPatterns = [
|
|
1556
|
+
/(?:write|draft|compose|create|send)(?:\s+me)?\s+(?:an?\s+)?(?:[a-z]+\s+){0,2}(?:email|mail|text(?:\s+message)?|message)(?:\s+draft)?\s+(?:for|to)\s+(.+)$/i,
|
|
1557
|
+
/(?:write|draft|compose|create)(?:\s+me)?\s+(?:an?\s+)?(?:[a-z]+\s+){0,3}draft\s+(?:for|to)\s+(.+)$/i,
|
|
1558
|
+
/(?:email|mail|text(?:\s+message)?|message)\s+(?:for|to)\s+(.+)$/i,
|
|
1559
|
+
/^\s*(?:email|mail|text(?:\s+message)?|message)\s+(.+)$/i,
|
|
1560
|
+
/\bdraft\s+(?:for|to)\s+(.+)$/i,
|
|
1561
|
+
/(?:write|draft|compose|create)\s+to\s+(.+)$/i,
|
|
1562
|
+
];
|
|
1563
|
+
for (const pattern of recipientPatterns) {
|
|
1564
|
+
const match = text.match(pattern);
|
|
1565
|
+
if (match?.[1]) {
|
|
1566
|
+
const extracted = extractRecipientAndHint(match[1].trim());
|
|
1567
|
+
recipientChunk = extracted.recipientChunk;
|
|
1568
|
+
hintKind = extracted.hintKind;
|
|
1569
|
+
hintText = extracted.hintText;
|
|
1570
|
+
break;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
if (!recipientChunk) {
|
|
1574
|
+
const fallbackRecipientMatch = text.match(/\b(?:to|for)\s+(.+)$/i);
|
|
1575
|
+
if (fallbackRecipientMatch?.[1]) {
|
|
1576
|
+
const extracted = extractRecipientAndHint(fallbackRecipientMatch[1].trim());
|
|
1577
|
+
recipientChunk = extracted.recipientChunk;
|
|
1578
|
+
hintKind = extracted.hintKind;
|
|
1579
|
+
hintText = extracted.hintText;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
if (!recipientChunk) {
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const recipients = normalizeRecipientList(
|
|
1587
|
+
recipientChunk,
|
|
1588
|
+
params.peopleAliases,
|
|
1589
|
+
channel === "message",
|
|
1590
|
+
);
|
|
1591
|
+
if (recipients.length === 0) {
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
const subjectMatch = text.match(/\bsubject(?:\s*[:\-]\s*|\s+)(.+?)(?=\s+(?:body|message|that says|saying)\b|$)/i);
|
|
1596
|
+
const messageMatch = text.match(/\b(?:that says|saying(?: that)?|with message|message:)\s+(.+)$/i);
|
|
1597
|
+
const topicMatch = text.match(/\b(?:about|regarding|re(?:\:)?)(?:\s+the)?\s+(.+)$/i);
|
|
1598
|
+
const topic = normalizeDraftText(
|
|
1599
|
+
(topicMatch?.[1] ?? "") || (hintKind === "topic" ? hintText : ""),
|
|
1600
|
+
);
|
|
1601
|
+
const explicitBody = normalizeDraftText(
|
|
1602
|
+
(messageMatch?.[1] ?? "") || (hintKind === "body" ? hintText : ""),
|
|
1603
|
+
);
|
|
1604
|
+
|
|
1605
|
+
if (channel === "email") {
|
|
1606
|
+
const subject = inferEmailSubject({
|
|
1607
|
+
explicitSubject: subjectMatch?.[1] ?? "",
|
|
1608
|
+
bodyText: explicitBody,
|
|
1609
|
+
topicText: topic,
|
|
1610
|
+
});
|
|
1611
|
+
const body = buildEmailDraftBody({
|
|
1612
|
+
topicText: topic,
|
|
1613
|
+
explicitBody,
|
|
1614
|
+
recipient: recipients[0],
|
|
1615
|
+
sourceText: text,
|
|
1616
|
+
});
|
|
1617
|
+
return {
|
|
1618
|
+
intentType,
|
|
1619
|
+
toolName: "mail_send",
|
|
1620
|
+
args: {
|
|
1621
|
+
to: recipients,
|
|
1622
|
+
subject,
|
|
1623
|
+
body,
|
|
1624
|
+
},
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const body = buildMessageDraftBody({
|
|
1629
|
+
topicText: topic,
|
|
1630
|
+
explicitBody,
|
|
1631
|
+
recipient: recipients[0],
|
|
1632
|
+
});
|
|
1633
|
+
return {
|
|
1634
|
+
intentType,
|
|
1635
|
+
toolName: "messages_send",
|
|
1636
|
+
args: {
|
|
1637
|
+
to: recipients,
|
|
1638
|
+
body,
|
|
1639
|
+
},
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
|
|
997
1643
|
function extractDirectMusicIntent(messages: ChatMessageInput[], aliases?: MemoryMusicAlias[]): {
|
|
998
1644
|
query: string;
|
|
999
1645
|
title: string | null;
|
|
@@ -1482,59 +2128,111 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1482
2128
|
}
|
|
1483
2129
|
}
|
|
1484
2130
|
|
|
1485
|
-
|
|
1486
|
-
const messageIntent = parseDirectMessageIntent({
|
|
1487
|
-
text: lastUserText,
|
|
1488
|
-
peopleAliases: params.memory?.enabled ? params.memory.aliases.people : [],
|
|
1489
|
-
});
|
|
1490
|
-
if (messageIntent) {
|
|
2131
|
+
const runDraftableCommunication = async (intent: DraftCommunicationIntent): Promise<boolean> => {
|
|
1491
2132
|
if (!params.localTools.enableMail) {
|
|
1492
2133
|
params.send({
|
|
1493
2134
|
type: "token",
|
|
1494
2135
|
value:
|
|
1495
|
-
"
|
|
2136
|
+
"Mail/messages tools are disabled. Enable **mail/messages tools** in Settings > Local Tools, then retry.",
|
|
1496
2137
|
});
|
|
1497
2138
|
return true;
|
|
1498
2139
|
}
|
|
1499
2140
|
|
|
1500
|
-
const
|
|
1501
|
-
if (
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
}
|
|
2141
|
+
const tool = findToolByName(tools, intent.toolName);
|
|
2142
|
+
if (!tool) {
|
|
2143
|
+
return false;
|
|
2144
|
+
}
|
|
1505
2145
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
2146
|
+
const requiresDraftReview =
|
|
2147
|
+
isPreSendDraftTool(tool.name) &&
|
|
2148
|
+
(params.localTools.preSendDraftReview || intent.intentType === "explicit_draft_request");
|
|
2149
|
+
if (!requiresDraftReview && requiresApprovalForRisk(tool.risk, params.localTools.approvalMode)) {
|
|
2150
|
+
return false;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
const callId = intent.toolName === "mail_send" ? "fastpath_mail_send" : "fastpath_messages_send";
|
|
2154
|
+
let executionArgs: Record<string, unknown> = { ...intent.args };
|
|
2155
|
+
params.send({
|
|
2156
|
+
type: "tool_call",
|
|
2157
|
+
callId,
|
|
2158
|
+
name: tool.name,
|
|
2159
|
+
args: intent.args,
|
|
2160
|
+
});
|
|
2161
|
+
|
|
2162
|
+
if (requiresDraftReview) {
|
|
2163
|
+
const approval = createApprovalRequest();
|
|
1511
2164
|
params.send({
|
|
1512
|
-
type: "
|
|
2165
|
+
type: "approval_requested",
|
|
2166
|
+
approvalId: approval.approvalId,
|
|
1513
2167
|
callId,
|
|
1514
|
-
name:
|
|
1515
|
-
args,
|
|
2168
|
+
name: tool.name,
|
|
2169
|
+
args: intent.args,
|
|
2170
|
+
reason: summarizeApprovalReason(tool.name, intent.args),
|
|
1516
2171
|
});
|
|
2172
|
+
const approvalResolution = await approval.promise;
|
|
1517
2173
|
params.send({
|
|
1518
|
-
type: "
|
|
2174
|
+
type: "approval_resolved",
|
|
2175
|
+
approvalId: approval.approvalId,
|
|
1519
2176
|
callId,
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
2177
|
+
decision: approvalResolution.decision,
|
|
2178
|
+
message:
|
|
2179
|
+
approvalResolution.decision === "approve"
|
|
2180
|
+
? "Approved"
|
|
2181
|
+
: approvalResolution.decision === "supersede"
|
|
2182
|
+
? "Replaced"
|
|
2183
|
+
: approvalResolution.decision === "timeout"
|
|
2184
|
+
? "Timed out"
|
|
2185
|
+
: "Denied",
|
|
1523
2186
|
});
|
|
2187
|
+
if (approvalResolution.decision !== "approve") {
|
|
2188
|
+
if (approvalResolution.decision === "deny") {
|
|
2189
|
+
params.send({
|
|
2190
|
+
type: "token",
|
|
2191
|
+
value: "Draft discarded. Nothing was sent.",
|
|
2192
|
+
});
|
|
2193
|
+
} else if (approvalResolution.decision === "timeout") {
|
|
2194
|
+
params.send({
|
|
2195
|
+
type: "token",
|
|
2196
|
+
value: "Draft review expired. Nothing was sent.",
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
return true;
|
|
2200
|
+
}
|
|
2201
|
+
if (
|
|
2202
|
+
approvalResolution.args &&
|
|
2203
|
+
typeof approvalResolution.args === "object" &&
|
|
2204
|
+
!Array.isArray(approvalResolution.args)
|
|
2205
|
+
) {
|
|
2206
|
+
executionArgs = approvalResolution.args;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
1524
2209
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
2210
|
+
const progressMessage = tool.name === "mail_send" ? "Sending email." : "Sending message.";
|
|
2211
|
+
params.send({
|
|
2212
|
+
type: "tool_progress",
|
|
2213
|
+
callId,
|
|
2214
|
+
name: tool.name,
|
|
2215
|
+
status: "started",
|
|
2216
|
+
message: progressMessage,
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2219
|
+
try {
|
|
2220
|
+
const output = await tool.execute(executionArgs, {
|
|
2221
|
+
localTools: params.localTools,
|
|
2222
|
+
signal: params.signal,
|
|
2223
|
+
});
|
|
2224
|
+
params.send({
|
|
2225
|
+
type: "tool_result",
|
|
2226
|
+
callId,
|
|
2227
|
+
name: tool.name,
|
|
2228
|
+
ok: true,
|
|
2229
|
+
result: output,
|
|
2230
|
+
});
|
|
1537
2231
|
|
|
2232
|
+
const argsRecipients = Array.isArray(executionArgs.to)
|
|
2233
|
+
? executionArgs.to.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
2234
|
+
: [];
|
|
2235
|
+
if (tool.name === "messages_send") {
|
|
1538
2236
|
const outputRecord =
|
|
1539
2237
|
output && typeof output === "object"
|
|
1540
2238
|
? (output as { recipients?: Array<{ name?: string | null; input?: string; handle?: string }> })
|
|
@@ -1542,30 +2240,66 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1542
2240
|
const recipientLabel =
|
|
1543
2241
|
outputRecord?.recipients?.map((recipient) => recipient.name || recipient.input || recipient.handle)
|
|
1544
2242
|
.filter(Boolean)
|
|
1545
|
-
.join(", ") ||
|
|
1546
|
-
|
|
2243
|
+
.join(", ") || argsRecipients.join(", ");
|
|
1547
2244
|
params.send({
|
|
1548
2245
|
type: "token",
|
|
1549
2246
|
value: `**Done.** Sent message to ${recipientLabel}.`,
|
|
1550
2247
|
});
|
|
1551
|
-
}
|
|
1552
|
-
const
|
|
1553
|
-
messageTool.name,
|
|
1554
|
-
error,
|
|
1555
|
-
"Could not send message.",
|
|
1556
|
-
);
|
|
1557
|
-
params.send({
|
|
1558
|
-
type: "tool_result",
|
|
1559
|
-
callId,
|
|
1560
|
-
name: messageTool.name,
|
|
1561
|
-
ok: false,
|
|
1562
|
-
result: { error: message },
|
|
1563
|
-
});
|
|
2248
|
+
} else {
|
|
2249
|
+
const recipientLabel = argsRecipients.join(", ");
|
|
1564
2250
|
params.send({
|
|
1565
2251
|
type: "token",
|
|
1566
|
-
value:
|
|
2252
|
+
value: `**Done.** Sent email to ${recipientLabel}.`,
|
|
1567
2253
|
});
|
|
1568
2254
|
}
|
|
2255
|
+
} catch (error) {
|
|
2256
|
+
const fallback = tool.name === "mail_send" ? "Could not send email." : "Could not send message.";
|
|
2257
|
+
const message = formatToolErrorForUser(tool.name, error, fallback);
|
|
2258
|
+
params.send({
|
|
2259
|
+
type: "tool_result",
|
|
2260
|
+
callId,
|
|
2261
|
+
name: tool.name,
|
|
2262
|
+
ok: false,
|
|
2263
|
+
result: { error: message },
|
|
2264
|
+
});
|
|
2265
|
+
params.send({
|
|
2266
|
+
type: "token",
|
|
2267
|
+
value:
|
|
2268
|
+
tool.name === "mail_send"
|
|
2269
|
+
? `I couldn't send that email: ${message}.`
|
|
2270
|
+
: `I couldn't send that message: ${message}.`,
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
return true;
|
|
2274
|
+
};
|
|
2275
|
+
|
|
2276
|
+
// Fast path 2: direct text/iMessage
|
|
2277
|
+
const messageIntent = parseDirectMessageIntent({
|
|
2278
|
+
text: lastUserText,
|
|
2279
|
+
peopleAliases: params.memory?.enabled ? params.memory.aliases.people : [],
|
|
2280
|
+
});
|
|
2281
|
+
if (messageIntent) {
|
|
2282
|
+
const handled = await runDraftableCommunication({
|
|
2283
|
+
intentType: "send_request",
|
|
2284
|
+
toolName: "messages_send",
|
|
2285
|
+
args: {
|
|
2286
|
+
to: messageIntent.to,
|
|
2287
|
+
body: messageIntent.body,
|
|
2288
|
+
},
|
|
2289
|
+
});
|
|
2290
|
+
if (handled) {
|
|
2291
|
+
return true;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// Fast path 2b: draft or general write/send email/message
|
|
2296
|
+
const draftCommunicationIntent = parseDraftCommunicationIntent({
|
|
2297
|
+
text: lastUserText,
|
|
2298
|
+
peopleAliases: params.memory?.enabled ? params.memory.aliases.people : [],
|
|
2299
|
+
});
|
|
2300
|
+
if (draftCommunicationIntent) {
|
|
2301
|
+
const handled = await runDraftableCommunication(draftCommunicationIntent);
|
|
2302
|
+
if (handled) {
|
|
1569
2303
|
return true;
|
|
1570
2304
|
}
|
|
1571
2305
|
}
|
|
@@ -1846,6 +2580,7 @@ async function streamPlainChat(params: {
|
|
|
1846
2580
|
system?: string;
|
|
1847
2581
|
messages: ChatMessageInput[];
|
|
1848
2582
|
enableWebSources: boolean;
|
|
2583
|
+
webSearchMode?: WebSearchMode;
|
|
1849
2584
|
send: (chunk: ChatStreamChunk) => void;
|
|
1850
2585
|
signal?: AbortSignal;
|
|
1851
2586
|
}) {
|
|
@@ -1895,20 +2630,30 @@ async function streamPlainChat(params: {
|
|
|
1895
2630
|
baseURL: openAIBaseURL,
|
|
1896
2631
|
defaultHeaders: openAIHeaders,
|
|
1897
2632
|
});
|
|
1898
|
-
const useWebSources =
|
|
1899
|
-
|
|
2633
|
+
const useWebSources = params.enableWebSources && supportsWebSourcesViaOpenAI(params.connection);
|
|
2634
|
+
const webSearchMode: WebSearchMode = useWebSources
|
|
2635
|
+
? params.webSearchMode === "required"
|
|
2636
|
+
? "required"
|
|
2637
|
+
: "auto"
|
|
2638
|
+
: "off";
|
|
2639
|
+
const instructions = withWebSearchGuidance(params.system, webSearchMode);
|
|
1900
2640
|
const response = await client.responses.create(
|
|
1901
2641
|
{
|
|
1902
2642
|
model: params.model,
|
|
1903
|
-
instructions
|
|
2643
|
+
instructions,
|
|
1904
2644
|
input: params.messages.map((message) => ({
|
|
1905
2645
|
type: "message",
|
|
1906
2646
|
role: message.role,
|
|
1907
2647
|
content: message.content,
|
|
1908
2648
|
})),
|
|
1909
|
-
tools:
|
|
1910
|
-
tool_choice:
|
|
1911
|
-
|
|
2649
|
+
tools: webSearchMode !== "off" ? [{ type: "web_search" }] : undefined,
|
|
2650
|
+
tool_choice:
|
|
2651
|
+
webSearchMode === "required"
|
|
2652
|
+
? "required"
|
|
2653
|
+
: webSearchMode === "auto"
|
|
2654
|
+
? "auto"
|
|
2655
|
+
: undefined,
|
|
2656
|
+
include: webSearchMode !== "off" ? ["web_search_call.action.sources"] : undefined,
|
|
1912
2657
|
stream: true,
|
|
1913
2658
|
},
|
|
1914
2659
|
{ signal: params.signal },
|
|
@@ -2040,13 +2785,15 @@ async function runToolOrchestrator(params: {
|
|
|
2040
2785
|
messages: ChatMessageInput[];
|
|
2041
2786
|
localTools: LocalToolsSettings;
|
|
2042
2787
|
enableWebSources: boolean;
|
|
2788
|
+
forceDraftReviewForSend?: boolean;
|
|
2789
|
+
draftEditContext?: DraftEditPromptContext;
|
|
2043
2790
|
send: (chunk: ChatStreamChunk) => void;
|
|
2044
2791
|
signal?: AbortSignal;
|
|
2045
2792
|
}) {
|
|
2046
2793
|
const { tools, session } = getToolSession({
|
|
2047
2794
|
connection: params.connection,
|
|
2048
2795
|
model: params.model,
|
|
2049
|
-
system: buildToolSystemPrompt(params.system, params.messages),
|
|
2796
|
+
system: buildToolSystemPrompt(params.system, params.messages, params.draftEditContext),
|
|
2050
2797
|
messages: params.messages,
|
|
2051
2798
|
localTools: params.localTools,
|
|
2052
2799
|
enableWebSources: params.enableWebSources,
|
|
@@ -2157,7 +2904,10 @@ async function runToolOrchestrator(params: {
|
|
|
2157
2904
|
continue;
|
|
2158
2905
|
}
|
|
2159
2906
|
|
|
2160
|
-
|
|
2907
|
+
const requiresDraftReview =
|
|
2908
|
+
isPreSendDraftTool(call.name) &&
|
|
2909
|
+
(params.localTools.preSendDraftReview || params.forceDraftReviewForSend === true);
|
|
2910
|
+
if (requiresDraftReview || requiresApprovalForRisk(tool.risk, params.localTools.approvalMode)) {
|
|
2161
2911
|
const approval = createApprovalRequest();
|
|
2162
2912
|
params.send({
|
|
2163
2913
|
type: "approval_requested",
|
|
@@ -2169,23 +2919,47 @@ async function runToolOrchestrator(params: {
|
|
|
2169
2919
|
});
|
|
2170
2920
|
|
|
2171
2921
|
emitThinkingUpdate(`Waiting for approval to run ${call.name}.`);
|
|
2172
|
-
const
|
|
2922
|
+
const approvalResolution = await approval.promise;
|
|
2173
2923
|
params.send({
|
|
2174
2924
|
type: "approval_resolved",
|
|
2175
2925
|
approvalId: approval.approvalId,
|
|
2176
2926
|
callId: call.id,
|
|
2177
|
-
decision,
|
|
2927
|
+
decision: approvalResolution.decision,
|
|
2178
2928
|
message:
|
|
2179
|
-
decision === "approve"
|
|
2929
|
+
approvalResolution.decision === "approve"
|
|
2180
2930
|
? "Approved"
|
|
2181
|
-
: decision === "
|
|
2931
|
+
: approvalResolution.decision === "supersede"
|
|
2932
|
+
? "Replaced"
|
|
2933
|
+
: approvalResolution.decision === "timeout"
|
|
2182
2934
|
? "Timed out"
|
|
2183
2935
|
: "Denied",
|
|
2184
2936
|
});
|
|
2185
2937
|
|
|
2186
|
-
if (decision !== "approve") {
|
|
2938
|
+
if (approvalResolution.decision !== "approve") {
|
|
2939
|
+
if (requiresDraftReview && isPreSendDraftTool(call.name)) {
|
|
2940
|
+
const message =
|
|
2941
|
+
approvalResolution.decision === "deny"
|
|
2942
|
+
? "Draft discarded. Nothing was sent."
|
|
2943
|
+
: approvalResolution.decision === "timeout"
|
|
2944
|
+
? "Draft review expired. Nothing was sent."
|
|
2945
|
+
: "Draft replaced by a newer instruction.";
|
|
2946
|
+
if (approvalResolution.decision !== "supersede") {
|
|
2947
|
+
params.send({
|
|
2948
|
+
type: "token",
|
|
2949
|
+
value: message,
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
session.addToolResult({
|
|
2953
|
+
callId: call.id,
|
|
2954
|
+
name: call.name,
|
|
2955
|
+
result: message,
|
|
2956
|
+
isError: false,
|
|
2957
|
+
});
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2187
2961
|
const message =
|
|
2188
|
-
decision === "timeout"
|
|
2962
|
+
approvalResolution.decision === "timeout"
|
|
2189
2963
|
? "Tool call timed out waiting for approval."
|
|
2190
2964
|
: "Tool call denied by user.";
|
|
2191
2965
|
params.send({
|
|
@@ -2203,6 +2977,13 @@ async function runToolOrchestrator(params: {
|
|
|
2203
2977
|
});
|
|
2204
2978
|
continue;
|
|
2205
2979
|
}
|
|
2980
|
+
if (
|
|
2981
|
+
approvalResolution.args &&
|
|
2982
|
+
typeof approvalResolution.args === "object" &&
|
|
2983
|
+
!Array.isArray(approvalResolution.args)
|
|
2984
|
+
) {
|
|
2985
|
+
callInput = approvalResolution.args;
|
|
2986
|
+
}
|
|
2206
2987
|
}
|
|
2207
2988
|
|
|
2208
2989
|
params.send({
|
|
@@ -2326,10 +3107,15 @@ export async function POST(request: Request) {
|
|
|
2326
3107
|
}
|
|
2327
3108
|
|
|
2328
3109
|
const webSourcesEnabled = normalizeWebSourcesEnabled(body.enableWebSources);
|
|
2329
|
-
const
|
|
2330
|
-
webSourcesEnabled
|
|
3110
|
+
const webSearchMode = decideWebSearchMode({
|
|
3111
|
+
enabled: webSourcesEnabled,
|
|
3112
|
+
connection,
|
|
3113
|
+
messages: sanitizedMessages,
|
|
3114
|
+
});
|
|
3115
|
+
const enableWebSourcesForConnection = webSearchMode !== "off";
|
|
2331
3116
|
const localToolsRequested = normalizeLocalTools(body.localTools);
|
|
2332
3117
|
const memoryContext = sanitizeMemoryContext(body.memory);
|
|
3118
|
+
const requestMeta = sanitizeChatRequestMeta(body.meta);
|
|
2333
3119
|
const encoder = new TextEncoder();
|
|
2334
3120
|
|
|
2335
3121
|
const stream = new ReadableStream({
|
|
@@ -2369,28 +3155,36 @@ export async function POST(request: Request) {
|
|
|
2369
3155
|
}
|
|
2370
3156
|
|
|
2371
3157
|
if (localTools.enabled) {
|
|
2372
|
-
const
|
|
3158
|
+
const lastUserText =
|
|
3159
|
+
[...sanitizedMessages].reverse().find((message) => message.role === "user")?.content ?? "";
|
|
3160
|
+
const communicationIntentType = classifyDraftCommunicationIntent(lastUserText);
|
|
3161
|
+
const forceDraftReviewForSend =
|
|
3162
|
+
communicationIntentType === "explicit_draft_request" || Boolean(requestMeta.draftEditContext);
|
|
3163
|
+
const routeThroughTools =
|
|
3164
|
+
requestMeta.forceToolRouting || shouldRouteThroughToolOrchestrator(sanitizedMessages);
|
|
2373
3165
|
if (routeThroughTools) {
|
|
2374
3166
|
sendThinking("update", "Checking whether this can run through a direct action path.");
|
|
2375
3167
|
let fastHandled = false;
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
3168
|
+
if (!requestMeta.forceToolRouting) {
|
|
3169
|
+
try {
|
|
3170
|
+
fastHandled = await tryDirectAutomationFastPath({
|
|
3171
|
+
localTools,
|
|
3172
|
+
messages: sanitizedMessages,
|
|
3173
|
+
memory: memoryContext,
|
|
3174
|
+
send,
|
|
3175
|
+
signal: request.signal,
|
|
3176
|
+
});
|
|
3177
|
+
} catch (error) {
|
|
3178
|
+
const message =
|
|
3179
|
+
error instanceof Error ? truncateMessage(error.message) : "Fast-path unavailable.";
|
|
3180
|
+
send({
|
|
3181
|
+
type: "tool_progress",
|
|
3182
|
+
callId: "tooling",
|
|
3183
|
+
name: "tooling",
|
|
3184
|
+
status: "warning",
|
|
3185
|
+
message: `Direct automation fast-path failed: ${message}. Continuing with tool orchestration.`,
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
2394
3188
|
}
|
|
2395
3189
|
|
|
2396
3190
|
if (fastHandled) {
|
|
@@ -2407,6 +3201,8 @@ export async function POST(request: Request) {
|
|
|
2407
3201
|
messages: sanitizedMessages,
|
|
2408
3202
|
localTools,
|
|
2409
3203
|
enableWebSources: enableWebSourcesForConnection,
|
|
3204
|
+
forceDraftReviewForSend,
|
|
3205
|
+
draftEditContext: requestMeta.draftEditContext,
|
|
2410
3206
|
send,
|
|
2411
3207
|
signal: request.signal,
|
|
2412
3208
|
});
|
|
@@ -2423,6 +3219,7 @@ export async function POST(request: Request) {
|
|
|
2423
3219
|
system,
|
|
2424
3220
|
messages: sanitizedMessages,
|
|
2425
3221
|
enableWebSources: enableWebSourcesForConnection,
|
|
3222
|
+
webSearchMode,
|
|
2426
3223
|
send,
|
|
2427
3224
|
signal: request.signal,
|
|
2428
3225
|
});
|