@agent-nexus/driftbench 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +32 -2
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +8 -0
- package/dist/chunk-57ZRX7PY.js +1024 -0
- package/dist/chunk-5DPBYSU3.js +11649 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +16 -0
- package/dist/random-agent-E43SQJDO.js +66 -0
- package/dist/src-5XXPWAL2.js +26 -0
- package/package.json +65 -3
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+
// ../../examples/acme/src/parse.ts
|
|
2
|
+
function normalize(text) {
|
|
3
|
+
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
4
|
+
}
|
|
5
|
+
var EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}/;
|
|
6
|
+
function extractEmail(text) {
|
|
7
|
+
return EMAIL.exec(text)?.[0].toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
var PHONE = /(?:\+?1[\s.]?)?\(?\d{3}\)?[\s.]?\d{3}[\s.]?\d{4}/;
|
|
10
|
+
function extractPhone(text) {
|
|
11
|
+
const match = PHONE.exec(text);
|
|
12
|
+
return match ? toE164(match[0]) : void 0;
|
|
13
|
+
}
|
|
14
|
+
function toE164(raw) {
|
|
15
|
+
const digits = raw.replace(/\D/g, "");
|
|
16
|
+
if (digits.length === 10) return `+1${digits}`;
|
|
17
|
+
if (digits.length === 11 && digits.startsWith("1")) return `+${digits}`;
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
function formatPhone(e164) {
|
|
21
|
+
const digits = e164.replace(/\D/g, "");
|
|
22
|
+
if (digits.length !== 11) return e164;
|
|
23
|
+
return `+${digits.slice(0, 1)} ${digits.slice(1, 4)} ${digits.slice(4, 7)} ${digits.slice(7)}`;
|
|
24
|
+
}
|
|
25
|
+
var ADDRESS = /\b\d{1,5}\s+(?:[A-Z][a-z]+\s+){1,3}(?:Road|Way|Avenue|Lane|Street|Drive|Court|Place)\b(?:,\s*[A-Z][a-z]+)?/;
|
|
26
|
+
function extractAddress(text) {
|
|
27
|
+
return ADDRESS.exec(text)?.[0];
|
|
28
|
+
}
|
|
29
|
+
var FILLER = /^(?:my name is|my name's|i am|i'm|it's|it is|this is|name:?)\s+/i;
|
|
30
|
+
var FULL_NAME = /^[A-Z][a-z]+(?:\s+[A-Z][a-z']+)+$/;
|
|
31
|
+
function extractName(text) {
|
|
32
|
+
const withoutEmail = text.replace(EMAIL, " ");
|
|
33
|
+
for (const part of withoutEmail.split(/[,;\n]|\band\b/)) {
|
|
34
|
+
const candidate = part.trim().replace(FILLER, "").replace(/[.!]+$/, "").trim();
|
|
35
|
+
if (FULL_NAME.test(candidate)) return candidate;
|
|
36
|
+
}
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
function firstName(name) {
|
|
40
|
+
return name?.split(" ")[0];
|
|
41
|
+
}
|
|
42
|
+
var has = (pattern) => (text) => pattern.test(normalize(text));
|
|
43
|
+
var isBlank = (text) => text.trim() === "";
|
|
44
|
+
var NUDGE_LINE = /^(?:hello|hi|hey|are you (?:still )?there|anyone there|still there)\W*$/;
|
|
45
|
+
function isNudge(text) {
|
|
46
|
+
const lines = text.split("\n").map((line) => normalize(line)).filter((line) => line !== "");
|
|
47
|
+
return lines.every((line) => NUDGE_LINE.test(line));
|
|
48
|
+
}
|
|
49
|
+
var isInjection = has(
|
|
50
|
+
/\bignore (?:all |any |your |the |my )?(?:previous |prior |earlier |above )?(?:rules|instructions|prompt)\b|\bsystem prompt\b|\byou are now\b|\bdisregard (?:all |your |the )?(?:rules|instructions)\b/
|
|
51
|
+
);
|
|
52
|
+
var FOREIGN_WORDS = /* @__PURE__ */ new Set([
|
|
53
|
+
"hola",
|
|
54
|
+
"gracias",
|
|
55
|
+
"quiero",
|
|
56
|
+
"tienen",
|
|
57
|
+
"cu\xE1nto",
|
|
58
|
+
"cuesta",
|
|
59
|
+
"barato",
|
|
60
|
+
"m\xE1s",
|
|
61
|
+
"bonjour",
|
|
62
|
+
"merci",
|
|
63
|
+
"combien",
|
|
64
|
+
"voudrais",
|
|
65
|
+
"hallo",
|
|
66
|
+
"bedankt",
|
|
67
|
+
"goedkoper",
|
|
68
|
+
"danke",
|
|
69
|
+
"bitte"
|
|
70
|
+
]);
|
|
71
|
+
function isForeign(text) {
|
|
72
|
+
if (/[¿¡]/.test(text)) return true;
|
|
73
|
+
return normalize(text).split(/[^\p{L}]+/u).some((word) => FOREIGN_WORDS.has(word));
|
|
74
|
+
}
|
|
75
|
+
var isCareMatter = has(
|
|
76
|
+
/\b(?:bill|billing|billed|charged|invoice|refund|outage|no signal|not working|cancel my (?:contract|plan|line))\b/
|
|
77
|
+
);
|
|
78
|
+
var isExplicitConfirm = (text) => /^(?:yes )?(?:i )?confirm(?: it| please| the order| my order)?$/.test(
|
|
79
|
+
normalize(text).replace(/[^a-z ]/g, " ").replace(/\s+/g, " ").trim()
|
|
80
|
+
);
|
|
81
|
+
var isLooseYes = has(
|
|
82
|
+
/\b(?:ok|okay|sure|sounds good|perfect|great|yes|yep|yeah|go ahead|do it|fine|alright)\b/
|
|
83
|
+
);
|
|
84
|
+
var isReturn = has(
|
|
85
|
+
/\b(?:where were we|let'?s continue|let us continue|back to it|back to (?:the|my) (?:plan|order)|what were the options|carry on)\b/
|
|
86
|
+
);
|
|
87
|
+
var isStoreHours = (text) => has(/\b(?:store|stores|shop|shops)\b/)(text) && has(/\b(?:open|opening|hours|close|closed)\b/)(text);
|
|
88
|
+
var isCallbackRequest = has(/\b(?:call me|call back|callback|ring me|contact me|someone call)\b/);
|
|
89
|
+
var isRetry = has(/\b(?:try (?:it )?again|try once more|check again|retry|one more try)\b/);
|
|
90
|
+
var isDone = has(/\b(?:thanks|thank you|that'?s all|nothing else|bye|goodbye)\b/);
|
|
91
|
+
var isQuestion = (text) => text.includes("?");
|
|
92
|
+
var INTENTS = [
|
|
93
|
+
["porting", /\b(?:keep my (?:phone )?number|port(?:ing)? my number|bring my number|transfer my number)\b/],
|
|
94
|
+
[
|
|
95
|
+
"upgrade",
|
|
96
|
+
/\b(?:upgrade|more data|running out of data|bigger plan|existing customer|(?:already|i'?m|i am) (?:an |a )?acme customer)\b/
|
|
97
|
+
],
|
|
98
|
+
["home", /\b(?:home internet|broadband|internet at home|fiber|fibre|wifi)\b/],
|
|
99
|
+
["mobile", /\b(?:phone plan|mobile plan|sim|new line|mobile)\b/]
|
|
100
|
+
];
|
|
101
|
+
function detectIntent(text) {
|
|
102
|
+
const normalized = normalize(text);
|
|
103
|
+
return INTENTS.find(([, pattern]) => pattern.test(normalized))?.[0];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ../../examples/acme/src/store.ts
|
|
107
|
+
var ACME = {
|
|
108
|
+
name: "Acme Telecom",
|
|
109
|
+
carePhone: "+12025550100",
|
|
110
|
+
salesPhone: "+12025550101",
|
|
111
|
+
storeHours: "Monday to Saturday, 9 am to 6 pm"
|
|
112
|
+
};
|
|
113
|
+
var SEED = {
|
|
114
|
+
plans: [
|
|
115
|
+
{ id: "mobile-s", name: "Acme Mobile S", category: "mobile", monthly_usd: 15, data_gb: 10 },
|
|
116
|
+
{ id: "mobile-m", name: "Acme Mobile M", category: "mobile", monthly_usd: 25, data_gb: 40 },
|
|
117
|
+
{ id: "mobile-l", name: "Acme Mobile L", category: "mobile", monthly_usd: 35, data_gb: null },
|
|
118
|
+
{
|
|
119
|
+
id: "fiber-300",
|
|
120
|
+
name: "Acme Fiber 300",
|
|
121
|
+
category: "home",
|
|
122
|
+
monthly_usd: 40,
|
|
123
|
+
speed_mbps: 300,
|
|
124
|
+
access: "fiber"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "fiber-1000",
|
|
128
|
+
name: "Acme Fiber 1000",
|
|
129
|
+
category: "home",
|
|
130
|
+
monthly_usd: 60,
|
|
131
|
+
speed_mbps: 1e3,
|
|
132
|
+
access: "fiber"
|
|
133
|
+
},
|
|
134
|
+
{ id: "air-5g", name: "Acme Air 5G", category: "home", monthly_usd: 35, speed_mbps: 200, access: "5g" }
|
|
135
|
+
],
|
|
136
|
+
addresses: [
|
|
137
|
+
{ id: "addr-101", label: "14 Harbor Road, Acmeville", fiber_mbps: 1e3, five_g: true },
|
|
138
|
+
{ id: "addr-102", label: "3 Orchard Way, Acmeville", fiber_mbps: 300, five_g: true },
|
|
139
|
+
{ id: "addr-103", label: "88 Summit Avenue, Acmeville", fiber_mbps: 0, five_g: true },
|
|
140
|
+
{ id: "addr-104", label: "7 Quarry Lane, Acmeville", fiber_mbps: 0, five_g: false }
|
|
141
|
+
],
|
|
142
|
+
customers: [
|
|
143
|
+
{
|
|
144
|
+
id: "cust-001",
|
|
145
|
+
name: "Sam Rivera",
|
|
146
|
+
email: "sam.rivera@example.com",
|
|
147
|
+
phone: "+12025550142",
|
|
148
|
+
plan_id: "mobile-s"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "cust-002",
|
|
152
|
+
name: "Lee Jensen",
|
|
153
|
+
email: "lee.jensen@example.com",
|
|
154
|
+
phone: "+12025550118",
|
|
155
|
+
plan_id: "fiber-300",
|
|
156
|
+
address_id: "addr-102"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "cust-003",
|
|
160
|
+
name: "Morgan Diaz",
|
|
161
|
+
email: "morgan.diaz@example.com",
|
|
162
|
+
phone: "+12025550125",
|
|
163
|
+
plan_id: "mobile-l"
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
port_numbers: [
|
|
167
|
+
{
|
|
168
|
+
number: "+12025550160",
|
|
169
|
+
eligible: false,
|
|
170
|
+
reason: "the number has a transfer lock at its current carrier"
|
|
171
|
+
},
|
|
172
|
+
{ number: "+12025550165", eligible: true }
|
|
173
|
+
]
|
|
174
|
+
};
|
|
175
|
+
function createStore() {
|
|
176
|
+
return { ...structuredClone(SEED), payloads: [] };
|
|
177
|
+
}
|
|
178
|
+
function withSeed(store, seed) {
|
|
179
|
+
return {
|
|
180
|
+
plans: overlay(store.plans, seed?.plans, "id"),
|
|
181
|
+
addresses: overlay(store.addresses, seed?.addresses, "id"),
|
|
182
|
+
customers: overlay(store.customers, seed?.customers, "id"),
|
|
183
|
+
port_numbers: overlay(store.port_numbers, seed?.port_numbers, "number")
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function overlay(base, seed, key) {
|
|
187
|
+
if (!Array.isArray(seed)) return base;
|
|
188
|
+
const extra = seed.filter(
|
|
189
|
+
(entry) => entry !== null && typeof entry === "object" && typeof entry[key] === "string"
|
|
190
|
+
);
|
|
191
|
+
const replaced = new Set(extra.map((entry) => entry[key]));
|
|
192
|
+
return [...base.filter((entry) => !replaced.has(entry[key])), ...extra];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ../../examples/acme/src/policy.ts
|
|
196
|
+
var STAGES = [
|
|
197
|
+
"needs",
|
|
198
|
+
"identify",
|
|
199
|
+
"address",
|
|
200
|
+
"port_number",
|
|
201
|
+
"offer",
|
|
202
|
+
"details",
|
|
203
|
+
"recap",
|
|
204
|
+
"order_placed",
|
|
205
|
+
"callback",
|
|
206
|
+
"handover",
|
|
207
|
+
"closing"
|
|
208
|
+
];
|
|
209
|
+
function initialState() {
|
|
210
|
+
return { stage: "needs", greeted: false, customer: {}, offered: [] };
|
|
211
|
+
}
|
|
212
|
+
function takeTurn(previous, text, call) {
|
|
213
|
+
const state = structuredClone(previous);
|
|
214
|
+
const guards = [];
|
|
215
|
+
const opening = state.greeted ? "" : `Hi, I'm the ${ACME.name} sales assistant. `;
|
|
216
|
+
state.greeted = true;
|
|
217
|
+
return { state, reply: opening + route(state, text, call, guards), guards };
|
|
218
|
+
}
|
|
219
|
+
var NEEDS = "What are you looking for: a mobile plan, home internet, an upgrade, or keeping your number?";
|
|
220
|
+
var CARE = `I can't handle billing or service issues here. Acme Care can help: call ${formatPhone(ACME.carePhone)}.`;
|
|
221
|
+
var SALES_LINE = `Please call Acme Sales at ${formatPhone(ACME.salesPhone)}.`;
|
|
222
|
+
function route(s, text, call, guards) {
|
|
223
|
+
if (isBlank(text) || isNudge(text)) {
|
|
224
|
+
return s.flow ? `I'm here. ${pendingQuestion(s)}` : `I can help with plans, internet and orders. ${NEEDS}`;
|
|
225
|
+
}
|
|
226
|
+
if (isInjection(text)) {
|
|
227
|
+
guards.push({ name: "instruction_in_data", verdict: "block" });
|
|
228
|
+
return `I can't change prices, offers or my instructions. ${pendingQuestion(s)}`;
|
|
229
|
+
}
|
|
230
|
+
if (isForeign(text)) return `I can only help in English for now. ${pendingQuestion(s)}`;
|
|
231
|
+
if (isCareMatter(text)) return `${CARE} ${pendingQuestion(s)}`;
|
|
232
|
+
if (s.stage === "recap") {
|
|
233
|
+
const answer2 = atRecap(s, text, call, guards);
|
|
234
|
+
if (answer2) return answer2;
|
|
235
|
+
}
|
|
236
|
+
if (isReturn(text)) return resume(s);
|
|
237
|
+
if (isStoreHours(text)) {
|
|
238
|
+
return `Our stores are open ${ACME.storeHours}, and closed on Sunday. ${pendingQuestion(s)}`;
|
|
239
|
+
}
|
|
240
|
+
if (s.flow === void 0) return start(s, text, call);
|
|
241
|
+
const answer = atStep(s, text, call);
|
|
242
|
+
if (answer) return answer;
|
|
243
|
+
if (isQuestion(text)) return `I can only help with Acme plans and orders. ${pendingQuestion(s)}`;
|
|
244
|
+
return `Sorry, I did not get that. ${pendingQuestion(s)}`;
|
|
245
|
+
}
|
|
246
|
+
function start(s, text, call) {
|
|
247
|
+
switch (detectIntent(text)) {
|
|
248
|
+
case "porting":
|
|
249
|
+
return startPorting(s, text, call);
|
|
250
|
+
case "upgrade":
|
|
251
|
+
return startUpgrade(s, text, call);
|
|
252
|
+
case "home":
|
|
253
|
+
return startHome(s, text, call);
|
|
254
|
+
case "mobile":
|
|
255
|
+
s.flow = "mobile";
|
|
256
|
+
return offerMobile(s, call, "Happy to help you get a new line.");
|
|
257
|
+
default:
|
|
258
|
+
return isQuestion(text) ? `I can only help with Acme plans and orders. ${NEEDS}` : `I can help with plans, internet and orders. ${NEEDS}`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function atStep(s, text, call) {
|
|
262
|
+
switch (s.stage) {
|
|
263
|
+
case "identify": {
|
|
264
|
+
const email = extractEmail(text);
|
|
265
|
+
return email ? identify(s, email, call) : void 0;
|
|
266
|
+
}
|
|
267
|
+
case "address": {
|
|
268
|
+
const address = extractAddress(text);
|
|
269
|
+
if (address) return checkCoverage(s, address, text, call);
|
|
270
|
+
return isQuestion(text) ? void 0 : "I need the street address, for example 14 Harbor Road, Acmeville.";
|
|
271
|
+
}
|
|
272
|
+
case "port_number": {
|
|
273
|
+
const number = extractPhone(text);
|
|
274
|
+
return number ? checkPort(s, number, call) : void 0;
|
|
275
|
+
}
|
|
276
|
+
case "offer":
|
|
277
|
+
return atOffer(s, text);
|
|
278
|
+
case "details":
|
|
279
|
+
return atDetails(s, text);
|
|
280
|
+
case "callback":
|
|
281
|
+
if (isRetry(text) && canRetryCoverage(s)) return retryCoverage(s, call);
|
|
282
|
+
return atCallback(s, text, call);
|
|
283
|
+
case "order_placed":
|
|
284
|
+
case "handover":
|
|
285
|
+
case "closing":
|
|
286
|
+
if (isRetry(text) && canRetryCoverage(s)) return retryCoverage(s, call);
|
|
287
|
+
if (isDone(text)) {
|
|
288
|
+
s.stage = "closing";
|
|
289
|
+
const name = firstName(s.customer.name);
|
|
290
|
+
return `You're welcome${name ? `, ${name}` : ""}. Have a good day.`;
|
|
291
|
+
}
|
|
292
|
+
return void 0;
|
|
293
|
+
default:
|
|
294
|
+
return void 0;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function startUpgrade(s, text, call) {
|
|
298
|
+
s.flow = "upgrade";
|
|
299
|
+
const email = extractEmail(text);
|
|
300
|
+
if (email) return identify(s, email, call);
|
|
301
|
+
s.stage = "identify";
|
|
302
|
+
return `I can help you move to a plan with more data. ${pendingQuestion(s)}`;
|
|
303
|
+
}
|
|
304
|
+
function identify(s, email, call) {
|
|
305
|
+
s.customer.email = email;
|
|
306
|
+
const catalog = readCatalog(
|
|
307
|
+
callWithRetry(call, "catalog.search", { category: "mobile", customer_email: email })
|
|
308
|
+
);
|
|
309
|
+
if (!catalog) {
|
|
310
|
+
return askPhone(
|
|
311
|
+
s,
|
|
312
|
+
{ reason: "tool_failure", summary: `Plan lookup failed for ${email}. The customer wants more data.` },
|
|
313
|
+
"I can't look up your plan right now: our catalog is not responding. A sales advisor can call you back. What phone number should they use?"
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const customer = catalog.customer;
|
|
317
|
+
if (!customer) {
|
|
318
|
+
s.customer.email = void 0;
|
|
319
|
+
s.stage = "identify";
|
|
320
|
+
return `I can't find an Acme account with ${email}. Please check the email address on your Acme account.`;
|
|
321
|
+
}
|
|
322
|
+
s.customer = { ...s.customer, id: customer.id, name: customer.name };
|
|
323
|
+
const current = catalog.plans.find((plan) => plan.id === customer.current_plan_id);
|
|
324
|
+
const name = firstName(customer.name);
|
|
325
|
+
if (!current) {
|
|
326
|
+
s.offered = catalog.plans;
|
|
327
|
+
s.stage = "offer";
|
|
328
|
+
return `Thanks, ${name}. Our mobile plans: ${list(catalog.plans)}. ${offerQuestion(s)}`;
|
|
329
|
+
}
|
|
330
|
+
s.currentPlan = current;
|
|
331
|
+
const upgrades = catalog.plans.filter((plan) => dataOf(plan) > dataOf(current));
|
|
332
|
+
if (upgrades.length === 0) {
|
|
333
|
+
s.stage = "closing";
|
|
334
|
+
return `Thanks, ${name}. You are on ${describe(current)}, which already has the most data. Is there anything else I can help with?`;
|
|
335
|
+
}
|
|
336
|
+
s.offered = upgrades;
|
|
337
|
+
s.stage = "offer";
|
|
338
|
+
return `Thanks, ${name}. You are on ${describe(current)}. Plans with more data: ${list(upgrades)}. ${offerQuestion(s)}`;
|
|
339
|
+
}
|
|
340
|
+
function dataOf(plan) {
|
|
341
|
+
if (plan.data_gb === null) return Number.POSITIVE_INFINITY;
|
|
342
|
+
return plan.data_gb ?? 0;
|
|
343
|
+
}
|
|
344
|
+
function startHome(s, text, call) {
|
|
345
|
+
s.flow = "home";
|
|
346
|
+
const address = extractAddress(text);
|
|
347
|
+
if (address) return checkCoverage(s, address, text, call);
|
|
348
|
+
s.stage = "address";
|
|
349
|
+
return `I can check what is available at your address. ${pendingQuestion(s)}`;
|
|
350
|
+
}
|
|
351
|
+
function checkCoverage(s, address, text, call) {
|
|
352
|
+
s.address = address;
|
|
353
|
+
const coverage = readCoverage(callWithRetry(call, "coverage.check", { address }));
|
|
354
|
+
if (!coverage) {
|
|
355
|
+
return askPhone(
|
|
356
|
+
s,
|
|
357
|
+
{
|
|
358
|
+
reason: "tool_failure",
|
|
359
|
+
summary: `Coverage check failed for ${address}. The customer wants home internet.`
|
|
360
|
+
},
|
|
361
|
+
`I can't check coverage at ${address} right now: our coverage service is not responding. A sales advisor can check it and call you back. What phone number should they use?`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
return afterCoverage(s, coverage, text, call);
|
|
365
|
+
}
|
|
366
|
+
function canRetryCoverage(s) {
|
|
367
|
+
return s.flow === "home" && s.coverage === void 0 && s.address !== void 0;
|
|
368
|
+
}
|
|
369
|
+
function retryCoverage(s, call) {
|
|
370
|
+
const address = s.address ?? "";
|
|
371
|
+
const coverage = readCoverage(callWithRetry(call, "coverage.check", { address }));
|
|
372
|
+
if (!coverage) return `Still no answer from the coverage service. ${leadStatus(s)}`;
|
|
373
|
+
return afterCoverage(s, coverage, "", call);
|
|
374
|
+
}
|
|
375
|
+
function afterCoverage(s, coverage, text, call) {
|
|
376
|
+
if (!coverage.found) {
|
|
377
|
+
s.stage = "address";
|
|
378
|
+
return `I can't find ${s.address} in our coverage map. Please give the street number, street name and town, for example 14 Harbor Road, Acmeville.`;
|
|
379
|
+
}
|
|
380
|
+
const { found: _found, ...known } = coverage;
|
|
381
|
+
s.coverage = known;
|
|
382
|
+
const unreachable = `No Acme home internet plan is available at ${coverage.address} yet. A sales advisor can call you when that changes. What phone number should they use?`;
|
|
383
|
+
const noCoverage = {
|
|
384
|
+
reason: "no_coverage",
|
|
385
|
+
summary: `No Acme home internet at ${coverage.address}. The customer wants a call when that changes.`
|
|
386
|
+
};
|
|
387
|
+
if (coverage.fiber_mbps === 0 && !coverage.five_g) return askPhone(s, noCoverage, unreachable);
|
|
388
|
+
const catalog = readCatalog(
|
|
389
|
+
callWithRetry(call, "catalog.search", { category: "home", address_id: coverage.address_id })
|
|
390
|
+
);
|
|
391
|
+
if (!catalog) {
|
|
392
|
+
return askPhone(
|
|
393
|
+
s,
|
|
394
|
+
{ reason: "tool_failure", summary: `Plan search failed for ${coverage.address}.` },
|
|
395
|
+
`${coverageSentence(coverage)} But I can't load the plans right now: our catalog is not responding. A sales advisor can call you back with the offers. What phone number should they use?`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (catalog.plans.length === 0) return askPhone(s, noCoverage, unreachable);
|
|
399
|
+
s.offered = catalog.plans;
|
|
400
|
+
const named = mentionedPlans(text, catalog.plans);
|
|
401
|
+
if (named.length === 1 && named[0]) {
|
|
402
|
+
s.plan = named[0];
|
|
403
|
+
s.stage = "details";
|
|
404
|
+
return `${named[0].name} is available at ${coverage.address}: ${terms(named[0])}. ${detailsQuestion(s)}`;
|
|
405
|
+
}
|
|
406
|
+
s.stage = "offer";
|
|
407
|
+
const plans = catalog.plans.length === 1 ? `The plan available there: ${list(catalog.plans)}.` : `Plans available there: ${list(catalog.plans)}.`;
|
|
408
|
+
return `${coverageSentence(coverage)} ${plans} ${offerQuestion(s)}`;
|
|
409
|
+
}
|
|
410
|
+
function coverageSentence(coverage) {
|
|
411
|
+
return coverage.fiber_mbps > 0 ? `Good news: ${coverage.address} has fiber up to ${coverage.fiber_mbps} Mbps.` : `Fiber is not available at ${coverage.address} yet, but 5G home internet is.`;
|
|
412
|
+
}
|
|
413
|
+
function startPorting(s, text, call) {
|
|
414
|
+
s.flow = "porting";
|
|
415
|
+
const number = extractPhone(text);
|
|
416
|
+
if (number) return checkPort(s, number, call);
|
|
417
|
+
s.stage = "port_number";
|
|
418
|
+
return `Happy to help you switch and keep your number. ${pendingQuestion(s)}`;
|
|
419
|
+
}
|
|
420
|
+
function checkPort(s, number, call) {
|
|
421
|
+
const port = readPort(callWithRetry(call, "port.check", { number }));
|
|
422
|
+
if (!port) {
|
|
423
|
+
return askPhone(
|
|
424
|
+
s,
|
|
425
|
+
{ reason: "tool_failure", summary: `Number transfer check failed for ${formatPhone(number)}.` },
|
|
426
|
+
"I can't check that number right now: our transfer service is not responding. A sales advisor can check it and call you back. What phone number should they use?"
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const shown = formatPhone(port.number);
|
|
430
|
+
if (port.eligible) {
|
|
431
|
+
s.portNumber = port.number;
|
|
432
|
+
return offerMobile(
|
|
433
|
+
s,
|
|
434
|
+
call,
|
|
435
|
+
`Good news: ${shown} can move to Acme. The transfer takes one business day after your order.`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
s.portNumber = void 0;
|
|
439
|
+
return offerMobile(
|
|
440
|
+
s,
|
|
441
|
+
call,
|
|
442
|
+
`${shown} can't move to Acme right now: ${port.reason ?? "the transfer check said no"}. You can ask your current carrier to remove the lock, or take a new Acme number.`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
function offerMobile(s, call, intro) {
|
|
446
|
+
const catalog = readCatalog(callWithRetry(call, "catalog.search", { category: "mobile" }));
|
|
447
|
+
if (!catalog || catalog.plans.length === 0) {
|
|
448
|
+
return askPhone(
|
|
449
|
+
s,
|
|
450
|
+
{ reason: "tool_failure", summary: "Mobile plan search failed." },
|
|
451
|
+
`${intro} But I can't load the plans right now: our catalog is not responding. A sales advisor can call you back with the offers. What phone number should they use?`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
s.offered = catalog.plans;
|
|
455
|
+
s.stage = "offer";
|
|
456
|
+
return `${intro} Our mobile plans: ${list(catalog.plans)}. ${offerQuestion(s)}`;
|
|
457
|
+
}
|
|
458
|
+
function atOffer(s, text) {
|
|
459
|
+
const named = mentionedPlans(text, s.offered);
|
|
460
|
+
if (named.length === 1 && named[0]) return choosePlan(s, named[0]);
|
|
461
|
+
if (named.length > 1) return `Which one would you like: ${names(named)}?`;
|
|
462
|
+
if (isCallbackRequest(text)) {
|
|
463
|
+
const where = s.coverage?.address ?? s.address;
|
|
464
|
+
const wants = /\bfib(?:er|re)\b/.test(normalize(text)) ? "fiber" : "a call";
|
|
465
|
+
const summary = where ? `New customer at ${where} wants ${wants}. Offered: ${s.offered.map((plan) => plan.name).join(", ")}.` : "The customer asks for a call.";
|
|
466
|
+
return askPhone(
|
|
467
|
+
s,
|
|
468
|
+
{ reason: "callback", summary },
|
|
469
|
+
"Sure. A sales advisor can call you back. What phone number should they use?"
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
const only = s.offered.length === 1 ? s.offered[0] : void 0;
|
|
473
|
+
if (only && isLooseYes(text)) return choosePlan(s, only);
|
|
474
|
+
return void 0;
|
|
475
|
+
}
|
|
476
|
+
function choosePlan(s, plan) {
|
|
477
|
+
s.plan = plan;
|
|
478
|
+
if (s.flow === "upgrade" || s.customer.name && s.customer.email) return recap(s);
|
|
479
|
+
s.stage = "details";
|
|
480
|
+
return `Good choice: ${describe(plan)}. ${detailsQuestion(s)}`;
|
|
481
|
+
}
|
|
482
|
+
function atDetails(s, text) {
|
|
483
|
+
const email = extractEmail(text);
|
|
484
|
+
const name = extractName(text);
|
|
485
|
+
if (!email && !name) return void 0;
|
|
486
|
+
if (email) s.customer.email = email;
|
|
487
|
+
if (name) s.customer.name = name;
|
|
488
|
+
if (s.customer.name && s.customer.email) return recap(s);
|
|
489
|
+
return detailsQuestion(s);
|
|
490
|
+
}
|
|
491
|
+
function detailsQuestion(s) {
|
|
492
|
+
if (!s.customer.name && !s.customer.email) {
|
|
493
|
+
return "To set up the order I need your full name and email address.";
|
|
494
|
+
}
|
|
495
|
+
return s.customer.name ? "What is your email address?" : "What is your full name?";
|
|
496
|
+
}
|
|
497
|
+
function recap(s) {
|
|
498
|
+
const plan = s.plan;
|
|
499
|
+
if (!plan) {
|
|
500
|
+
s.stage = "offer";
|
|
501
|
+
return offerQuestion(s);
|
|
502
|
+
}
|
|
503
|
+
s.stage = "recap";
|
|
504
|
+
const ask = "Reply CONFIRM to place the order.";
|
|
505
|
+
if (s.flow === "upgrade") {
|
|
506
|
+
const from = s.currentPlan ? `${s.currentPlan.name} to ` : "";
|
|
507
|
+
return `Here is the change: ${from}${describe(plan)}, from your next billing cycle. ${ask}`;
|
|
508
|
+
}
|
|
509
|
+
const who = `for ${s.customer.name} (${s.customer.email})`;
|
|
510
|
+
if (s.flow === "home") {
|
|
511
|
+
return `Here is your order: ${describe(plan)}, at ${s.coverage?.address}, ${who}. ${ask}`;
|
|
512
|
+
}
|
|
513
|
+
const number = s.portNumber ? `keeping ${formatPhone(s.portNumber)}` : "with a new Acme number";
|
|
514
|
+
return `Here is your order: ${describe(plan)}, ${who}, ${number}. ${ask}`;
|
|
515
|
+
}
|
|
516
|
+
function atRecap(s, text, call, guards) {
|
|
517
|
+
if (isExplicitConfirm(text)) {
|
|
518
|
+
guards.push({ name: "explicit_confirmation", verdict: "pass" });
|
|
519
|
+
return placeOrder(s, call);
|
|
520
|
+
}
|
|
521
|
+
const other = mentionedPlans(text, s.offered).filter((plan) => plan.id !== s.plan?.id);
|
|
522
|
+
if (other.length === 1 && other[0]) {
|
|
523
|
+
s.plan = other[0];
|
|
524
|
+
return recap(s);
|
|
525
|
+
}
|
|
526
|
+
if (isLooseYes(text)) {
|
|
527
|
+
guards.push({ name: "explicit_confirmation", verdict: "block" });
|
|
528
|
+
return "I need an explicit go before I place anything: reply CONFIRM to place the order, or tell me what to change.";
|
|
529
|
+
}
|
|
530
|
+
return void 0;
|
|
531
|
+
}
|
|
532
|
+
function placeOrder(s, call) {
|
|
533
|
+
const plan = s.plan;
|
|
534
|
+
if (!plan) return recap(s);
|
|
535
|
+
const outcome = call("order.create", orderInput(s, plan));
|
|
536
|
+
const orderId = readString(outcome, "order_id");
|
|
537
|
+
if (orderId) {
|
|
538
|
+
s.orderId = orderId;
|
|
539
|
+
s.stage = "order_placed";
|
|
540
|
+
return placedText(s, plan, orderId);
|
|
541
|
+
}
|
|
542
|
+
const ticketId = sendLead(
|
|
543
|
+
s,
|
|
544
|
+
{
|
|
545
|
+
reason: "order_failed",
|
|
546
|
+
summary: `${plan.name} for ${s.customer.name ?? s.customer.email} could not be ordered: ${failure(outcome)}.`
|
|
547
|
+
},
|
|
548
|
+
call
|
|
549
|
+
);
|
|
550
|
+
if (!ticketId)
|
|
551
|
+
return `I could not place the order, and I could not reach our sales team either. ${SALES_LINE}`;
|
|
552
|
+
return `I could not place the order: our order system returned an error. I passed your request to a sales advisor, who will contact you at ${s.customer.email}. Your reference is ${ticketId}.`;
|
|
553
|
+
}
|
|
554
|
+
function orderInput(s, plan) {
|
|
555
|
+
if (s.flow === "upgrade") return { type: "upgrade", customer_id: s.customer.id, plan_id: plan.id };
|
|
556
|
+
const customer = { name: s.customer.name, email: s.customer.email };
|
|
557
|
+
if (s.flow === "home") {
|
|
558
|
+
return { type: "new", plan_id: plan.id, address_id: s.coverage?.address_id, customer };
|
|
559
|
+
}
|
|
560
|
+
return s.portNumber ? { type: "new", plan_id: plan.id, customer, port_number: s.portNumber } : { type: "new", plan_id: plan.id, customer };
|
|
561
|
+
}
|
|
562
|
+
function placedText(s, plan, orderId) {
|
|
563
|
+
const done = `Done. Your order ${orderId} is placed`;
|
|
564
|
+
switch (s.flow) {
|
|
565
|
+
case "upgrade":
|
|
566
|
+
return `${done}: ${plan.name} starts on your next billing cycle. Is there anything else I can help with?`;
|
|
567
|
+
case "home":
|
|
568
|
+
return `${done}: ${plan.name} at ${s.coverage?.address}. A technician will contact you at ${s.customer.email} to schedule the installation.`;
|
|
569
|
+
default:
|
|
570
|
+
return s.portNumber ? `${done}: ${plan.name}. Your number ${formatPhone(s.portNumber)} moves to Acme one business day after the order.` : `${done}: ${plan.name}. The next steps are on their way to ${s.customer.email}.`;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function askPhone(s, lead, question) {
|
|
574
|
+
s.pendingLead = lead;
|
|
575
|
+
s.stage = "callback";
|
|
576
|
+
return question;
|
|
577
|
+
}
|
|
578
|
+
function atCallback(s, text, call) {
|
|
579
|
+
const phone = extractPhone(text);
|
|
580
|
+
if (!phone) return void 0;
|
|
581
|
+
s.customer.phone = phone;
|
|
582
|
+
const lead = s.pendingLead ?? { reason: "callback", summary: "The customer asks for a call." };
|
|
583
|
+
const ticketId = sendLead(s, lead, call);
|
|
584
|
+
if (!ticketId) {
|
|
585
|
+
const either = lead.reason === "callback" ? "" : " either";
|
|
586
|
+
return `I could not book the callback${either}: our sales system is not responding. ${SALES_LINE}`;
|
|
587
|
+
}
|
|
588
|
+
return `Done. A sales advisor will call you at ${formatPhone(phone)}${purpose(s, lead)}. Your reference is ${ticketId}.`;
|
|
589
|
+
}
|
|
590
|
+
function purpose(s, lead) {
|
|
591
|
+
const where = s.coverage?.address ?? s.address;
|
|
592
|
+
switch (lead.reason) {
|
|
593
|
+
case "tool_failure":
|
|
594
|
+
return where && !s.coverage ? ` once coverage at ${where} is checked` : " once the check is done";
|
|
595
|
+
case "no_coverage":
|
|
596
|
+
return ` when Acme home internet reaches ${where}`;
|
|
597
|
+
case "callback":
|
|
598
|
+
return where ? ` about home internet at ${where}` : "";
|
|
599
|
+
default:
|
|
600
|
+
return "";
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function sendLead(s, lead, call) {
|
|
604
|
+
const contact = {};
|
|
605
|
+
if (s.customer.name) contact.name = s.customer.name;
|
|
606
|
+
if (s.customer.email) contact.email = s.customer.email;
|
|
607
|
+
if (s.customer.phone) contact.phone = s.customer.phone;
|
|
608
|
+
const outcome = call("notify_sales", { reason: lead.reason, customer: contact, summary: lead.summary });
|
|
609
|
+
const ticketId = readString(outcome, "ticket_id");
|
|
610
|
+
s.lead = { ...lead, ticketId };
|
|
611
|
+
s.pendingLead = void 0;
|
|
612
|
+
s.stage = "handover";
|
|
613
|
+
return ticketId;
|
|
614
|
+
}
|
|
615
|
+
function leadStatus(s) {
|
|
616
|
+
if (!s.lead) return pendingQuestion(s);
|
|
617
|
+
if (!s.lead.ticketId) return `The callback could not be booked either. ${SALES_LINE}`;
|
|
618
|
+
const phone = s.customer.phone ? ` at ${formatPhone(s.customer.phone)}` : "";
|
|
619
|
+
return `Your callback request ${s.lead.ticketId} is in: an advisor will call you${phone}.`;
|
|
620
|
+
}
|
|
621
|
+
function pendingQuestion(s) {
|
|
622
|
+
switch (s.stage) {
|
|
623
|
+
case "needs":
|
|
624
|
+
return NEEDS;
|
|
625
|
+
case "identify":
|
|
626
|
+
return "What is the email address on your Acme account?";
|
|
627
|
+
case "address":
|
|
628
|
+
return "What is the street address?";
|
|
629
|
+
case "port_number":
|
|
630
|
+
return "Which number do you want to bring to Acme?";
|
|
631
|
+
case "offer":
|
|
632
|
+
return offerQuestion(s);
|
|
633
|
+
case "details":
|
|
634
|
+
return detailsQuestion(s);
|
|
635
|
+
case "recap":
|
|
636
|
+
return "Reply CONFIRM to place the order, or tell me what to change.";
|
|
637
|
+
case "callback":
|
|
638
|
+
return "What phone number should the advisor use?";
|
|
639
|
+
default:
|
|
640
|
+
return "Is there anything else I can help with?";
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function resume(s) {
|
|
644
|
+
switch (s.stage) {
|
|
645
|
+
case "needs":
|
|
646
|
+
return `We were just starting. ${NEEDS}`;
|
|
647
|
+
case "offer": {
|
|
648
|
+
const where = s.coverage ? ` at ${s.coverage.address}` : "";
|
|
649
|
+
return `We were choosing your plan${where}: ${list(s.offered)}. ${offerQuestion(s)}`;
|
|
650
|
+
}
|
|
651
|
+
case "details":
|
|
652
|
+
return `We were setting up your order for ${s.plan?.name ?? "your plan"}. ${detailsQuestion(s)}`;
|
|
653
|
+
case "recap":
|
|
654
|
+
return `We were confirming your order. ${recap(s)}`;
|
|
655
|
+
default:
|
|
656
|
+
return `Let's pick up where we left off. ${pendingQuestion(s)}`;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function offerQuestion(s) {
|
|
660
|
+
const [only] = s.offered;
|
|
661
|
+
if (s.offered.length === 1 && only) return `Would you like ${only.name}?`;
|
|
662
|
+
return `Which one would you like: ${names(s.offered)}?`;
|
|
663
|
+
}
|
|
664
|
+
function names(plans) {
|
|
665
|
+
const all = plans.map((plan) => plan.name);
|
|
666
|
+
return all.length <= 1 ? all.join("") : `${all.slice(0, -1).join(", ")} or ${all.at(-1)}`;
|
|
667
|
+
}
|
|
668
|
+
function list(plans) {
|
|
669
|
+
return plans.map(describe).join("; ");
|
|
670
|
+
}
|
|
671
|
+
function describe(plan) {
|
|
672
|
+
return `${plan.name}, ${terms(plan)}`;
|
|
673
|
+
}
|
|
674
|
+
function terms(plan) {
|
|
675
|
+
const price = `for $${plan.monthly_usd} a month`;
|
|
676
|
+
if (plan.category === "mobile") {
|
|
677
|
+
return `${plan.data_gb === null ? "unlimited data" : `${plan.data_gb} GB`} ${price}`;
|
|
678
|
+
}
|
|
679
|
+
const speed = `${plan.speed_mbps} Mbps ${price}`;
|
|
680
|
+
return plan.access === "fiber" ? speed : `up to ${speed}`;
|
|
681
|
+
}
|
|
682
|
+
function mentionedPlans(text, plans) {
|
|
683
|
+
const normalized = normalize(text);
|
|
684
|
+
return plans.filter(
|
|
685
|
+
(plan) => aliases(plan).some((alias) => new RegExp(`\\b${escapeRegExp(alias)}\\b`).test(normalized))
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
function aliases(plan) {
|
|
689
|
+
const name = plan.name.toLowerCase();
|
|
690
|
+
const found = [name, name.replace(/^acme /, "")];
|
|
691
|
+
if (plan.data_gb === null) found.push("unlimited");
|
|
692
|
+
else if (plan.data_gb !== void 0) found.push(`${plan.data_gb} gb`, `${plan.data_gb}gb`);
|
|
693
|
+
if (plan.speed_mbps !== void 0 && plan.access === "fiber") found.push(`${plan.speed_mbps} mbps`);
|
|
694
|
+
if (plan.access === "5g") found.push("5g");
|
|
695
|
+
return found;
|
|
696
|
+
}
|
|
697
|
+
function escapeRegExp(text) {
|
|
698
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
699
|
+
}
|
|
700
|
+
function callWithRetry(call, label, input) {
|
|
701
|
+
const first = call(label, input);
|
|
702
|
+
return first.ok ? first : call(label, input);
|
|
703
|
+
}
|
|
704
|
+
function readCatalog(outcome) {
|
|
705
|
+
if (!outcome.ok || !isRecord(outcome.output)) return void 0;
|
|
706
|
+
const { plans, customer } = outcome.output;
|
|
707
|
+
if (!Array.isArray(plans) || !plans.every(isPlanView)) return void 0;
|
|
708
|
+
if (customer === void 0 || customer === null) return { plans, customer };
|
|
709
|
+
if (!isRecord(customer) || typeof customer.id !== "string" || typeof customer.name !== "string" || typeof customer.current_plan_id !== "string") {
|
|
710
|
+
return void 0;
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
plans,
|
|
714
|
+
customer: { id: customer.id, name: customer.name, current_plan_id: customer.current_plan_id }
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function isPlanView(value) {
|
|
718
|
+
return isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && typeof value.category === "string" && typeof value.monthly_usd === "number";
|
|
719
|
+
}
|
|
720
|
+
function readCoverage(outcome) {
|
|
721
|
+
if (!outcome.ok || !isRecord(outcome.output)) return void 0;
|
|
722
|
+
const out = outcome.output;
|
|
723
|
+
if (out.found === false) return { found: false };
|
|
724
|
+
if (out.found !== true || typeof out.address_id !== "string" || typeof out.address !== "string" || typeof out.fiber_mbps !== "number" || typeof out.five_g !== "boolean") {
|
|
725
|
+
return void 0;
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
found: true,
|
|
729
|
+
address_id: out.address_id,
|
|
730
|
+
address: out.address,
|
|
731
|
+
fiber_mbps: out.fiber_mbps,
|
|
732
|
+
five_g: out.five_g
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function readPort(outcome) {
|
|
736
|
+
if (!outcome.ok || !isRecord(outcome.output)) return void 0;
|
|
737
|
+
const { number, eligible, reason } = outcome.output;
|
|
738
|
+
if (typeof number !== "string" || typeof eligible !== "boolean") return void 0;
|
|
739
|
+
return { number, eligible, reason: typeof reason === "string" ? reason : void 0 };
|
|
740
|
+
}
|
|
741
|
+
function readString(outcome, key) {
|
|
742
|
+
if (!outcome.ok || !isRecord(outcome.output)) return void 0;
|
|
743
|
+
const value = outcome.output[key];
|
|
744
|
+
return typeof value === "string" ? value : void 0;
|
|
745
|
+
}
|
|
746
|
+
function failure(outcome) {
|
|
747
|
+
return outcome.ok ? "an answer the agent could not read" : outcome.error;
|
|
748
|
+
}
|
|
749
|
+
function isRecord(value) {
|
|
750
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ../../examples/acme/src/tools.ts
|
|
754
|
+
import { createHash } from "crypto";
|
|
755
|
+
var TOOL_LABELS = [
|
|
756
|
+
"catalog.search",
|
|
757
|
+
"coverage.check",
|
|
758
|
+
"port.check",
|
|
759
|
+
"order.create",
|
|
760
|
+
"notify_sales"
|
|
761
|
+
];
|
|
762
|
+
var SIDE_EFFECT_TOOLS = /* @__PURE__ */ new Set(["order.create", "notify_sales"]);
|
|
763
|
+
var InvalidInput = class extends Error {
|
|
764
|
+
};
|
|
765
|
+
function callTool(label, input, ctx) {
|
|
766
|
+
let outcome;
|
|
767
|
+
if (ctx.override?.error !== void 0) {
|
|
768
|
+
outcome = { ok: false, error: ctx.override.error };
|
|
769
|
+
} else if (ctx.override !== void 0 && "result" in ctx.override) {
|
|
770
|
+
outcome = { ok: true, output: ctx.override.result };
|
|
771
|
+
} else {
|
|
772
|
+
try {
|
|
773
|
+
outcome = { ok: true, output: IMPLEMENTATIONS[label](input, withSeed(ctx.store, ctx.seed)) };
|
|
774
|
+
} catch (cause) {
|
|
775
|
+
if (!(cause instanceof InvalidInput)) throw cause;
|
|
776
|
+
outcome = { ok: false, error: cause.message };
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (outcome.ok && ctx.commit && SIDE_EFFECT_TOOLS.has(label)) {
|
|
780
|
+
ctx.store.payloads.push({ tool: label, input: structuredClone(input), output: outcome.output });
|
|
781
|
+
}
|
|
782
|
+
return outcome;
|
|
783
|
+
}
|
|
784
|
+
function isToolLabel(value) {
|
|
785
|
+
return TOOL_LABELS.includes(value);
|
|
786
|
+
}
|
|
787
|
+
var IMPLEMENTATIONS = {
|
|
788
|
+
"catalog.search": catalogSearch,
|
|
789
|
+
"coverage.check": coverageCheck,
|
|
790
|
+
"port.check": portCheck,
|
|
791
|
+
"order.create": orderCreate,
|
|
792
|
+
notify_sales: notifySales
|
|
793
|
+
};
|
|
794
|
+
function catalogSearch(input, data) {
|
|
795
|
+
allowOnly(input, ["category", "customer_email", "address_id"]);
|
|
796
|
+
const category = input.category;
|
|
797
|
+
if (category !== "mobile" && category !== "home") throw new InvalidInput("category must be mobile or home");
|
|
798
|
+
let plans = data.plans.filter((plan) => plan.category === category);
|
|
799
|
+
if (input.address_id !== void 0) {
|
|
800
|
+
if (category !== "home") throw new InvalidInput("address_id applies to home plans only");
|
|
801
|
+
const address = data.addresses.find((entry) => entry.id === input.address_id);
|
|
802
|
+
if (!address) throw new InvalidInput(`unknown address_id: ${String(input.address_id)}`);
|
|
803
|
+
plans = plans.filter(
|
|
804
|
+
(plan) => plan.access === "fiber" ? address.fiber_mbps >= (plan.speed_mbps ?? 0) : address.five_g
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const result = { plans: plans.map((plan) => ({ ...plan })) };
|
|
808
|
+
if (input.customer_email !== void 0) {
|
|
809
|
+
const email = requireString(input, "customer_email").toLowerCase();
|
|
810
|
+
const customer = data.customers.find((entry) => entry.email === email);
|
|
811
|
+
result.customer = customer ? { id: customer.id, name: customer.name, current_plan_id: customer.plan_id } : null;
|
|
812
|
+
}
|
|
813
|
+
return result;
|
|
814
|
+
}
|
|
815
|
+
function coverageCheck(input, data) {
|
|
816
|
+
allowOnly(input, ["address"]);
|
|
817
|
+
const wanted = simplify(requireString(input, "address"));
|
|
818
|
+
const address = data.addresses.find((entry) => {
|
|
819
|
+
const street = simplify(entry.label.split(",")[0] ?? entry.label);
|
|
820
|
+
return wanted === simplify(entry.label) || wanted.startsWith(street);
|
|
821
|
+
});
|
|
822
|
+
if (!address) return { found: false, address: input.address };
|
|
823
|
+
return {
|
|
824
|
+
found: true,
|
|
825
|
+
address_id: address.id,
|
|
826
|
+
address: address.label,
|
|
827
|
+
fiber_mbps: address.fiber_mbps,
|
|
828
|
+
five_g: address.five_g
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function portCheck(input, data) {
|
|
832
|
+
allowOnly(input, ["number"]);
|
|
833
|
+
const number = toE164(requireString(input, "number"));
|
|
834
|
+
if (!number) throw new InvalidInput("number must be a US phone number");
|
|
835
|
+
const record = data.port_numbers.find((entry) => entry.number === number);
|
|
836
|
+
if (!record) return { number, eligible: false, reason: "the number is not active at any carrier" };
|
|
837
|
+
return record.eligible ? { number, eligible: true } : { number, eligible: false, reason: record.reason };
|
|
838
|
+
}
|
|
839
|
+
function orderCreate(input, data) {
|
|
840
|
+
allowOnly(input, ["type", "plan_id", "customer_id", "customer", "address_id", "port_number"]);
|
|
841
|
+
const plan = data.plans.find((entry) => entry.id === input.plan_id);
|
|
842
|
+
if (!plan) throw new InvalidInput(`unknown plan_id: ${String(input.plan_id)}`);
|
|
843
|
+
if (input.type === "upgrade") {
|
|
844
|
+
const customer = data.customers.find((entry) => entry.id === input.customer_id);
|
|
845
|
+
if (!customer) throw new InvalidInput(`unknown customer_id: ${String(input.customer_id)}`);
|
|
846
|
+
const current = data.plans.find((entry) => entry.id === customer.plan_id);
|
|
847
|
+
if (current && current.category !== plan.category) {
|
|
848
|
+
throw new InvalidInput("an upgrade keeps the category of the current plan");
|
|
849
|
+
}
|
|
850
|
+
if (input.customer !== void 0 || input.port_number !== void 0) {
|
|
851
|
+
throw new InvalidInput("an upgrade takes customer_id only");
|
|
852
|
+
}
|
|
853
|
+
} else if (input.type === "new") {
|
|
854
|
+
const customer = input.customer;
|
|
855
|
+
if (!isRecord2(customer) || typeof customer.name !== "string" || typeof customer.email !== "string") {
|
|
856
|
+
throw new InvalidInput("a new order needs customer.name and customer.email");
|
|
857
|
+
}
|
|
858
|
+
checkPlanPlace(plan, input, data);
|
|
859
|
+
} else {
|
|
860
|
+
throw new InvalidInput("type must be new or upgrade");
|
|
861
|
+
}
|
|
862
|
+
return {
|
|
863
|
+
order_id: `ORD-${shortHash(input)}`,
|
|
864
|
+
status: "pending",
|
|
865
|
+
plan_id: plan.id,
|
|
866
|
+
monthly_usd: plan.monthly_usd
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function checkPlanPlace(plan, input, data) {
|
|
870
|
+
if (plan.category === "home") {
|
|
871
|
+
const address = data.addresses.find((entry) => entry.id === input.address_id);
|
|
872
|
+
if (!address) throw new InvalidInput("a home plan needs a known address_id");
|
|
873
|
+
const available = plan.access === "fiber" ? address.fiber_mbps >= (plan.speed_mbps ?? 0) : address.five_g;
|
|
874
|
+
if (!available) throw new InvalidInput(`${plan.name} is not available at ${address.label}`);
|
|
875
|
+
if (input.port_number !== void 0) throw new InvalidInput("port_number applies to mobile plans only");
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (input.address_id !== void 0) throw new InvalidInput("address_id applies to home plans only");
|
|
879
|
+
if (input.port_number !== void 0) {
|
|
880
|
+
const number = toE164(String(input.port_number));
|
|
881
|
+
const record = data.port_numbers.find((entry) => entry.number === number);
|
|
882
|
+
if (!record?.eligible) throw new InvalidInput("port_number cannot move to Acme");
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
var LEAD_REASONS = /* @__PURE__ */ new Set(["callback", "no_coverage", "tool_failure", "order_failed"]);
|
|
886
|
+
function notifySales(input) {
|
|
887
|
+
allowOnly(input, ["reason", "customer", "summary"]);
|
|
888
|
+
if (typeof input.reason !== "string" || !LEAD_REASONS.has(input.reason)) {
|
|
889
|
+
throw new InvalidInput("reason must be callback, no_coverage, tool_failure or order_failed");
|
|
890
|
+
}
|
|
891
|
+
const customer = input.customer;
|
|
892
|
+
if (!isRecord2(customer) || typeof customer.phone !== "string" && typeof customer.email !== "string") {
|
|
893
|
+
throw new InvalidInput("customer needs a phone or an email");
|
|
894
|
+
}
|
|
895
|
+
requireString(input, "summary");
|
|
896
|
+
return { ticket_id: `LEAD-${shortHash(input)}`, queued: true };
|
|
897
|
+
}
|
|
898
|
+
function allowOnly(input, keys) {
|
|
899
|
+
for (const key of Object.keys(input)) {
|
|
900
|
+
if (!keys.includes(key)) throw new InvalidInput(`unknown parameter: ${key}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function requireString(input, key) {
|
|
904
|
+
const value = input[key];
|
|
905
|
+
if (typeof value !== "string" || value.trim() === "") throw new InvalidInput(`${key} is required`);
|
|
906
|
+
return value;
|
|
907
|
+
}
|
|
908
|
+
function isRecord2(value) {
|
|
909
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
910
|
+
}
|
|
911
|
+
function simplify(text) {
|
|
912
|
+
return text.toLowerCase().replace(/[^a-z0-9 ]/g, " ").replace(/\s+/g, " ").trim();
|
|
913
|
+
}
|
|
914
|
+
function shortHash(value) {
|
|
915
|
+
return createHash("sha256").update(stableJson(value)).digest("hex").slice(0, 6).toUpperCase();
|
|
916
|
+
}
|
|
917
|
+
function stableJson(value) {
|
|
918
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
919
|
+
if (isRecord2(value)) {
|
|
920
|
+
const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
|
|
921
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
922
|
+
}
|
|
923
|
+
return JSON.stringify(value);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// ../../examples/acme/src/agent.ts
|
|
927
|
+
function createAcmeAgent(store = createStore()) {
|
|
928
|
+
return {
|
|
929
|
+
async respond(messages, world) {
|
|
930
|
+
return respond(store, messages, world);
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function respond(store, messages, world) {
|
|
935
|
+
const turns = splitTurns(messages);
|
|
936
|
+
const current = turns.at(-1);
|
|
937
|
+
if (!current || current.answered) throw new Error("the last message must come from the user");
|
|
938
|
+
let state = initialState();
|
|
939
|
+
for (const turn of turns.slice(0, -1)) {
|
|
940
|
+
state = takeTurn(state, turn.text, replay(turn.calls, store, world)).state;
|
|
941
|
+
}
|
|
942
|
+
const live = liveTools(store, world);
|
|
943
|
+
const { state: next, reply, guards } = takeTurn(state, current.text, live.call);
|
|
944
|
+
const segments = [...live.segments, { type: "text", text: reply }];
|
|
945
|
+
return {
|
|
946
|
+
segments,
|
|
947
|
+
text: reply,
|
|
948
|
+
toolCalls: live.toolCalls,
|
|
949
|
+
runtime: guards.length > 0 ? { stages: [{ name: next.stage }], guards } : { stages: [{ name: next.stage }] },
|
|
950
|
+
cost: { usd: 0 },
|
|
951
|
+
latencyMs: live.toolCalls.reduce((sum, call) => sum + (call.latencyMs ?? 0), 0),
|
|
952
|
+
raw: { state: next }
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function splitTurns(messages) {
|
|
956
|
+
const turns = [];
|
|
957
|
+
let open;
|
|
958
|
+
let texts = [];
|
|
959
|
+
for (const message of messages) {
|
|
960
|
+
if (message.role === "user") {
|
|
961
|
+
if (!open || open.answered) {
|
|
962
|
+
texts = [];
|
|
963
|
+
open = { text: "", calls: [], answered: false };
|
|
964
|
+
turns.push(open);
|
|
965
|
+
}
|
|
966
|
+
texts.push(message.content);
|
|
967
|
+
open.text = texts.join("\n");
|
|
968
|
+
} else if (message.role === "assistant" && open) {
|
|
969
|
+
open.answered = true;
|
|
970
|
+
open.calls.push(...message.tool_calls ?? []);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return turns;
|
|
974
|
+
}
|
|
975
|
+
function replay(recorded, store, world) {
|
|
976
|
+
const queue = [...recorded];
|
|
977
|
+
return (label, input) => {
|
|
978
|
+
const index = queue.findIndex((call2) => call2.label === label);
|
|
979
|
+
const [call] = index >= 0 ? queue.splice(index, 1) : [];
|
|
980
|
+
if (call?.error !== void 0) return { ok: false, error: call.error };
|
|
981
|
+
if (call && "output" in call) return { ok: true, output: call.output };
|
|
982
|
+
return callTool(label, input, { store, seed: world?.store, commit: false });
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function liveTools(store, world) {
|
|
986
|
+
const segments = [];
|
|
987
|
+
const toolCalls = [];
|
|
988
|
+
const call = (label, input) => {
|
|
989
|
+
const id = `call-${toolCalls.length + 1}`;
|
|
990
|
+
const override = world?.tools?.[label];
|
|
991
|
+
const outcome = callTool(label, input, {
|
|
992
|
+
store,
|
|
993
|
+
seed: world?.store,
|
|
994
|
+
override,
|
|
995
|
+
commit: true
|
|
996
|
+
});
|
|
997
|
+
const latencyMs = override?.latency_ms ?? 0;
|
|
998
|
+
const shown = structuredClone(input);
|
|
999
|
+
segments.push({ type: "tool_call", id, label, input: shown });
|
|
1000
|
+
if (outcome.ok) {
|
|
1001
|
+
segments.push({ type: "tool_result", id, label, output: outcome.output });
|
|
1002
|
+
toolCalls.push({ id, label, input: shown, output: outcome.output, latencyMs });
|
|
1003
|
+
} else {
|
|
1004
|
+
segments.push({ type: "tool_result", id, label, output: null, error: outcome.error });
|
|
1005
|
+
toolCalls.push({ id, label, input: shown, error: outcome.error, latencyMs });
|
|
1006
|
+
}
|
|
1007
|
+
return outcome;
|
|
1008
|
+
};
|
|
1009
|
+
return { call, segments, toolCalls };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
export {
|
|
1013
|
+
ACME,
|
|
1014
|
+
createStore,
|
|
1015
|
+
withSeed,
|
|
1016
|
+
STAGES,
|
|
1017
|
+
initialState,
|
|
1018
|
+
takeTurn,
|
|
1019
|
+
TOOL_LABELS,
|
|
1020
|
+
SIDE_EFFECT_TOOLS,
|
|
1021
|
+
callTool,
|
|
1022
|
+
isToolLabel,
|
|
1023
|
+
createAcmeAgent
|
|
1024
|
+
};
|