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
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { nanoid } from "nanoid";
|
|
2
2
|
|
|
3
|
-
export type ApprovalDecision = "approve" | "deny" | "timeout";
|
|
3
|
+
export type ApprovalDecision = "approve" | "deny" | "timeout" | "supersede";
|
|
4
|
+
export type ApprovalSource = "user" | "system";
|
|
5
|
+
export type ApprovalReasonCode = "user_cancel" | "internal_replace" | "timeout" | "other";
|
|
6
|
+
export type ApprovalResolution = {
|
|
7
|
+
decision: ApprovalDecision;
|
|
8
|
+
args?: Record<string, unknown>;
|
|
9
|
+
source?: ApprovalSource;
|
|
10
|
+
reasonCode?: ApprovalReasonCode;
|
|
11
|
+
};
|
|
4
12
|
|
|
5
13
|
type PendingApproval = {
|
|
6
|
-
resolve: (
|
|
14
|
+
resolve: (resolution: ApprovalResolution) => void;
|
|
7
15
|
timeout: NodeJS.Timeout;
|
|
8
16
|
};
|
|
9
17
|
|
|
@@ -29,8 +37,8 @@ export function createApprovalRequest(params?: { timeoutMs?: number }) {
|
|
|
29
37
|
const approvalId = nanoid();
|
|
30
38
|
const timeoutMs = params?.timeoutMs ?? 5 * 60_000;
|
|
31
39
|
|
|
32
|
-
let resolveRef: ((
|
|
33
|
-
const promise = new Promise<
|
|
40
|
+
let resolveRef: ((resolution: ApprovalResolution) => void) | null = null;
|
|
41
|
+
const promise = new Promise<ApprovalResolution>((resolve) => {
|
|
34
42
|
resolveRef = resolve;
|
|
35
43
|
});
|
|
36
44
|
|
|
@@ -40,13 +48,13 @@ export function createApprovalRequest(params?: { timeoutMs?: number }) {
|
|
|
40
48
|
return;
|
|
41
49
|
}
|
|
42
50
|
store.pendingApprovals.delete(approvalId);
|
|
43
|
-
pending.resolve("timeout");
|
|
51
|
+
pending.resolve({ decision: "timeout", source: "system", reasonCode: "timeout" });
|
|
44
52
|
}, timeoutMs);
|
|
45
53
|
|
|
46
54
|
store.pendingApprovals.set(approvalId, {
|
|
47
|
-
resolve: (
|
|
55
|
+
resolve: (resolution) => {
|
|
48
56
|
clearTimeout(timeout);
|
|
49
|
-
resolveRef?.(
|
|
57
|
+
resolveRef?.(resolution);
|
|
50
58
|
},
|
|
51
59
|
timeout,
|
|
52
60
|
});
|
|
@@ -59,7 +67,9 @@ export function createApprovalRequest(params?: { timeoutMs?: number }) {
|
|
|
59
67
|
|
|
60
68
|
export function resolveApprovalDecision(
|
|
61
69
|
approvalId: string,
|
|
62
|
-
decision: "approve" | "deny",
|
|
70
|
+
decision: "approve" | "deny" | "supersede",
|
|
71
|
+
args?: Record<string, unknown>,
|
|
72
|
+
meta?: { source?: ApprovalSource; reasonCode?: ApprovalReasonCode },
|
|
63
73
|
): boolean {
|
|
64
74
|
const store = getApprovalStore();
|
|
65
75
|
const pending = store.pendingApprovals.get(approvalId);
|
|
@@ -69,7 +79,12 @@ export function resolveApprovalDecision(
|
|
|
69
79
|
|
|
70
80
|
store.pendingApprovals.delete(approvalId);
|
|
71
81
|
clearTimeout(pending.timeout);
|
|
72
|
-
pending.resolve(
|
|
82
|
+
pending.resolve({
|
|
83
|
+
decision,
|
|
84
|
+
args,
|
|
85
|
+
source: meta?.source ?? "user",
|
|
86
|
+
reasonCode: meta?.reasonCode,
|
|
87
|
+
});
|
|
73
88
|
return true;
|
|
74
89
|
}
|
|
75
90
|
|
|
@@ -39,6 +39,20 @@ type ResolvedMessageRecipient = {
|
|
|
39
39
|
source: "contact" | "direct";
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
type ResolvedMailRecipient = {
|
|
43
|
+
input: string;
|
|
44
|
+
name: string | null;
|
|
45
|
+
address: string;
|
|
46
|
+
source: "contact" | "direct";
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type ContactAutocompleteMode = "message" | "email";
|
|
50
|
+
export type ContactAutocompleteSuggestion = {
|
|
51
|
+
name: string;
|
|
52
|
+
value: string;
|
|
53
|
+
detail: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
42
56
|
const APPLESCRIPT_TIMEOUT_MS = 45_000;
|
|
43
57
|
const CONTACT_RECORD_DELIMITER = String.fromCharCode(30);
|
|
44
58
|
const CONTACT_FIELD_DELIMITER = String.fromCharCode(31);
|
|
@@ -214,6 +228,18 @@ function selectPreferredMessageHandle(contact: ContactRecord): string | null {
|
|
|
214
228
|
return null;
|
|
215
229
|
}
|
|
216
230
|
|
|
231
|
+
function selectPreferredEmail(contact: ContactRecord): string | null {
|
|
232
|
+
const emails = dedupeList(
|
|
233
|
+
contact.emails
|
|
234
|
+
.map((value) => value.trim())
|
|
235
|
+
.filter((value) => EMAIL_REGEX.test(value)),
|
|
236
|
+
);
|
|
237
|
+
if (emails.length > 0) {
|
|
238
|
+
return emails[0];
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
217
243
|
function formatContactOption(contact: ContactRecord): string {
|
|
218
244
|
const name = contactDisplayName(contact);
|
|
219
245
|
const handle = selectPreferredMessageHandle(contact);
|
|
@@ -516,6 +542,51 @@ async function getContactsMatchingQuery(query: string, signal?: AbortSignal): Pr
|
|
|
516
542
|
}
|
|
517
543
|
}
|
|
518
544
|
|
|
545
|
+
export async function autocompleteContacts(params: {
|
|
546
|
+
query: string;
|
|
547
|
+
mode: ContactAutocompleteMode;
|
|
548
|
+
signal?: AbortSignal;
|
|
549
|
+
}): Promise<ContactAutocompleteSuggestion[]> {
|
|
550
|
+
const query = params.query.trim();
|
|
551
|
+
if (!query) {
|
|
552
|
+
return [];
|
|
553
|
+
}
|
|
554
|
+
const contacts = await getContactsMatchingQuery(query, params.signal);
|
|
555
|
+
const matches = findContactMatches(query, contacts);
|
|
556
|
+
const seen = new Set<string>();
|
|
557
|
+
const suggestions: ContactAutocompleteSuggestion[] = [];
|
|
558
|
+
|
|
559
|
+
for (const match of matches) {
|
|
560
|
+
const contact = match.contact;
|
|
561
|
+
const name = contactDisplayName(contact);
|
|
562
|
+
const phones = dedupeList(contact.phones.map(normalizePhoneHandle).filter(Boolean));
|
|
563
|
+
const emails = dedupeList(
|
|
564
|
+
contact.emails
|
|
565
|
+
.map((value) => value.trim())
|
|
566
|
+
.filter((value) => EMAIL_REGEX.test(value)),
|
|
567
|
+
);
|
|
568
|
+
const orderedHandles =
|
|
569
|
+
params.mode === "message"
|
|
570
|
+
? [...phones, ...emails]
|
|
571
|
+
: emails;
|
|
572
|
+
const value = orderedHandles[0];
|
|
573
|
+
if (!value || seen.has(value)) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
seen.add(value);
|
|
577
|
+
suggestions.push({
|
|
578
|
+
name,
|
|
579
|
+
value,
|
|
580
|
+
detail: value,
|
|
581
|
+
});
|
|
582
|
+
if (suggestions.length >= 8) {
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return suggestions;
|
|
588
|
+
}
|
|
589
|
+
|
|
519
590
|
async function resolveMessageRecipients(
|
|
520
591
|
requestedRecipients: string[],
|
|
521
592
|
signal?: AbortSignal,
|
|
@@ -540,34 +611,7 @@ async function resolveMessageRecipients(
|
|
|
540
611
|
continue;
|
|
541
612
|
}
|
|
542
613
|
|
|
543
|
-
const
|
|
544
|
-
entry,
|
|
545
|
-
...normalizeNameForMatch(entry)
|
|
546
|
-
.split(" ")
|
|
547
|
-
.filter((token) => token.length >= 3),
|
|
548
|
-
]);
|
|
549
|
-
const candidateContacts: ContactRecord[] = [];
|
|
550
|
-
const candidateFingerprints = new Set<string>();
|
|
551
|
-
for (const query of queryCandidates) {
|
|
552
|
-
const contacts = await getContactsMatchingQuery(query, signal);
|
|
553
|
-
for (const contact of contacts) {
|
|
554
|
-
const fingerprint = [
|
|
555
|
-
contact.fullName,
|
|
556
|
-
contact.firstName,
|
|
557
|
-
contact.lastName,
|
|
558
|
-
contact.phones.join("|"),
|
|
559
|
-
contact.emails.join("|"),
|
|
560
|
-
].join("||");
|
|
561
|
-
if (candidateFingerprints.has(fingerprint)) {
|
|
562
|
-
continue;
|
|
563
|
-
}
|
|
564
|
-
candidateFingerprints.add(fingerprint);
|
|
565
|
-
candidateContacts.push(contact);
|
|
566
|
-
}
|
|
567
|
-
if (candidateContacts.length >= 24) {
|
|
568
|
-
break;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
614
|
+
const candidateContacts = await collectCandidateContacts(entry, signal);
|
|
571
615
|
|
|
572
616
|
const matches = findContactMatches(entry, candidateContacts);
|
|
573
617
|
if (matches.length === 0) {
|
|
@@ -591,6 +635,85 @@ async function resolveMessageRecipients(
|
|
|
591
635
|
return resolved;
|
|
592
636
|
}
|
|
593
637
|
|
|
638
|
+
async function collectCandidateContacts(entry: string, signal?: AbortSignal): Promise<ContactRecord[]> {
|
|
639
|
+
const queryCandidates = dedupeList([
|
|
640
|
+
entry,
|
|
641
|
+
...normalizeNameForMatch(entry)
|
|
642
|
+
.split(" ")
|
|
643
|
+
.filter((token) => token.length >= 3),
|
|
644
|
+
]);
|
|
645
|
+
const candidateContacts: ContactRecord[] = [];
|
|
646
|
+
const candidateFingerprints = new Set<string>();
|
|
647
|
+
for (const query of queryCandidates) {
|
|
648
|
+
const contacts = await getContactsMatchingQuery(query, signal);
|
|
649
|
+
for (const contact of contacts) {
|
|
650
|
+
const fingerprint = [
|
|
651
|
+
contact.fullName,
|
|
652
|
+
contact.firstName,
|
|
653
|
+
contact.lastName,
|
|
654
|
+
contact.phones.join("|"),
|
|
655
|
+
contact.emails.join("|"),
|
|
656
|
+
].join("||");
|
|
657
|
+
if (candidateFingerprints.has(fingerprint)) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
candidateFingerprints.add(fingerprint);
|
|
661
|
+
candidateContacts.push(contact);
|
|
662
|
+
}
|
|
663
|
+
if (candidateContacts.length >= 24) {
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return candidateContacts;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function resolveMailRecipients(
|
|
671
|
+
requestedRecipients: string[],
|
|
672
|
+
signal?: AbortSignal,
|
|
673
|
+
): Promise<ResolvedMailRecipient[]> {
|
|
674
|
+
const recipients = requestedRecipients
|
|
675
|
+
.map((entry) => entry.trim())
|
|
676
|
+
.filter(Boolean);
|
|
677
|
+
if (recipients.length === 0) {
|
|
678
|
+
throw new Error("Missing required array field: to");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const resolved: ResolvedMailRecipient[] = [];
|
|
682
|
+
for (const entry of recipients) {
|
|
683
|
+
const direct = entry.trim();
|
|
684
|
+
if (EMAIL_REGEX.test(direct)) {
|
|
685
|
+
resolved.push({
|
|
686
|
+
input: entry,
|
|
687
|
+
name: null,
|
|
688
|
+
address: direct,
|
|
689
|
+
source: "direct",
|
|
690
|
+
});
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const candidateContacts = await collectCandidateContacts(entry, signal);
|
|
695
|
+
const matches = findContactMatches(entry, candidateContacts);
|
|
696
|
+
if (matches.length === 0) {
|
|
697
|
+
throw notFoundContactError(entry, candidateContacts);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const matchedContact = resolveContactFromMatches(entry, matches);
|
|
701
|
+
const address = selectPreferredEmail(matchedContact);
|
|
702
|
+
if (!address) {
|
|
703
|
+
throw new Error(
|
|
704
|
+
`Contact "${contactDisplayName(matchedContact)}" has no email address available for Mail.`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
resolved.push({
|
|
708
|
+
input: entry,
|
|
709
|
+
name: contactDisplayName(matchedContact),
|
|
710
|
+
address,
|
|
711
|
+
source: "contact",
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return resolved;
|
|
715
|
+
}
|
|
716
|
+
|
|
594
717
|
async function runMessagesSendScript(
|
|
595
718
|
handles: string[],
|
|
596
719
|
body: string,
|
|
@@ -674,15 +797,25 @@ async function runAppleScript(
|
|
|
674
797
|
async function runMailCreateDraft(input: unknown, context: ToolExecutionContext) {
|
|
675
798
|
ensureMacOS("Mail automation");
|
|
676
799
|
const payload = asObject(input) as MailInput;
|
|
677
|
-
const
|
|
800
|
+
const requestedTo = asStringArray(payload.to, "to");
|
|
678
801
|
const subject = asString(payload.subject, "subject");
|
|
679
802
|
const body = asString(payload.body, "body");
|
|
680
|
-
const
|
|
803
|
+
const requestedCc = Array.isArray(payload.cc)
|
|
681
804
|
? payload.cc.filter((item): item is string => typeof item === "string" && item.trim() !== "")
|
|
682
805
|
: [];
|
|
683
806
|
const attachments = Array.isArray(payload.attachments)
|
|
684
807
|
? payload.attachments.filter((item): item is string => typeof item === "string" && item.trim() !== "")
|
|
685
808
|
: [];
|
|
809
|
+
const resolvedTo = await resolveMailRecipients(requestedTo, context.signal);
|
|
810
|
+
const resolvedCc = requestedCc.length > 0
|
|
811
|
+
? await resolveMailRecipients(requestedCc, context.signal)
|
|
812
|
+
: [];
|
|
813
|
+
const to = dedupeList(resolvedTo.map((recipient) => recipient.address));
|
|
814
|
+
const cc = dedupeList(
|
|
815
|
+
resolvedCc
|
|
816
|
+
.map((recipient) => recipient.address)
|
|
817
|
+
.filter((address) => !to.includes(address)),
|
|
818
|
+
);
|
|
686
819
|
|
|
687
820
|
const resolvedAttachments: string[] = [];
|
|
688
821
|
for (const attachment of attachments) {
|
|
@@ -703,6 +836,10 @@ async function runMailCreateDraft(input: unknown, context: ToolExecutionContext)
|
|
|
703
836
|
cc,
|
|
704
837
|
subject,
|
|
705
838
|
hasBody: Boolean(body),
|
|
839
|
+
recipients: {
|
|
840
|
+
to: resolvedTo,
|
|
841
|
+
cc: resolvedCc,
|
|
842
|
+
},
|
|
706
843
|
attachments: resolvedAttachments,
|
|
707
844
|
};
|
|
708
845
|
}
|
|
@@ -729,7 +866,17 @@ async function runMailCreateDraft(input: unknown, context: ToolExecutionContext)
|
|
|
729
866
|
'end run';
|
|
730
867
|
|
|
731
868
|
await runAppleScript(script, [to.join("\n"), cc.join("\n"), subject, body], context.signal);
|
|
732
|
-
return {
|
|
869
|
+
return {
|
|
870
|
+
drafted: true,
|
|
871
|
+
to,
|
|
872
|
+
cc,
|
|
873
|
+
subject,
|
|
874
|
+
recipients: {
|
|
875
|
+
to: resolvedTo,
|
|
876
|
+
cc: resolvedCc,
|
|
877
|
+
},
|
|
878
|
+
attachments: resolvedAttachments.length,
|
|
879
|
+
};
|
|
733
880
|
}
|
|
734
881
|
|
|
735
882
|
async function runMailSend(input: unknown, context: ToolExecutionContext) {
|
|
@@ -56,6 +56,7 @@ export type LocalToolsSettings = {
|
|
|
56
56
|
enabled: boolean;
|
|
57
57
|
approvalMode: ApprovalMode;
|
|
58
58
|
safetyProfile: SafetyProfile;
|
|
59
|
+
preSendDraftReview: boolean;
|
|
59
60
|
allowedRoots: string[];
|
|
60
61
|
enableNotes: boolean;
|
|
61
62
|
enableApps: boolean;
|
|
@@ -81,6 +82,7 @@ export const DEFAULT_LOCAL_TOOLS_SETTINGS: LocalToolsSettings = {
|
|
|
81
82
|
enabled: true,
|
|
82
83
|
approvalMode: "trusted_auto",
|
|
83
84
|
safetyProfile: "balanced",
|
|
85
|
+
preSendDraftReview: true,
|
|
84
86
|
allowedRoots: [...DEFAULT_LOCAL_TOOL_ROOTS],
|
|
85
87
|
enableNotes: true,
|
|
86
88
|
enableApps: true,
|
|
@@ -196,7 +198,7 @@ export type ToolEvent = {
|
|
|
196
198
|
createdAt: number;
|
|
197
199
|
};
|
|
198
200
|
|
|
199
|
-
export type ToolApprovalStatus = "requested" | "approved" | "denied" | "timeout";
|
|
201
|
+
export type ToolApprovalStatus = "requested" | "approved" | "denied" | "timeout" | "superseded";
|
|
200
202
|
|
|
201
203
|
export type ToolApproval = {
|
|
202
204
|
id: string;
|
|
@@ -238,6 +240,14 @@ export type ChatRequest = {
|
|
|
238
240
|
threadId?: string;
|
|
239
241
|
conversationId?: string;
|
|
240
242
|
assistantMessageId?: string;
|
|
243
|
+
forceToolRouting?: boolean;
|
|
244
|
+
draftEditContext?: {
|
|
245
|
+
toolName: "mail_send" | "messages_send";
|
|
246
|
+
to: string[];
|
|
247
|
+
cc?: string[];
|
|
248
|
+
subject?: string;
|
|
249
|
+
body: string;
|
|
250
|
+
};
|
|
241
251
|
};
|
|
242
252
|
};
|
|
243
253
|
|
|
@@ -281,7 +291,7 @@ export type ApprovalResolvedEvent = {
|
|
|
281
291
|
type: "approval_resolved";
|
|
282
292
|
approvalId: string;
|
|
283
293
|
callId: string;
|
|
284
|
-
decision: "approve" | "deny" | "timeout";
|
|
294
|
+
decision: "approve" | "deny" | "timeout" | "supersede";
|
|
285
295
|
message?: string;
|
|
286
296
|
};
|
|
287
297
|
|