@kognitivedev/backend-cloud 0.2.29
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/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-test.log +14 -0
- package/CHANGELOG.md +11 -0
- package/README.md +88 -0
- package/dist/cloud-voice-parameters.d.ts +11 -0
- package/dist/cloud-voice-parameters.js +219 -0
- package/dist/cloud-voice-prompt-service.d.ts +24 -0
- package/dist/cloud-voice-prompt-service.js +382 -0
- package/dist/cloud-voice-runtime-service.d.ts +73 -0
- package/dist/cloud-voice-runtime-service.js +443 -0
- package/dist/cloud-voice.d.ts +36 -0
- package/dist/cloud-voice.js +683 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +26 -0
- package/dist/phone-control.d.ts +50 -0
- package/dist/phone-control.js +97 -0
- package/dist/phone-runtime/audio-playout-tracker.d.ts +51 -0
- package/dist/phone-runtime/audio-playout-tracker.js +93 -0
- package/dist/phone-runtime/openai-twilio-realtime.d.ts +95 -0
- package/dist/phone-runtime/openai-twilio-realtime.js +1074 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +216 -0
- package/dist/types.d.ts +468 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +14 -0
- package/package.json +47 -0
- package/src/__tests__/audio-playout-tracker.test.ts +46 -0
- package/src/__tests__/cloud-voice.test.ts +1006 -0
- package/src/__tests__/openai-twilio-realtime.test.ts +1193 -0
- package/src/__tests__/phone-control.test.ts +105 -0
- package/src/cloud-voice-parameters.ts +236 -0
- package/src/cloud-voice-prompt-service.ts +493 -0
- package/src/cloud-voice-runtime-service.ts +465 -0
- package/src/cloud-voice.ts +831 -0
- package/src/index.ts +10 -0
- package/src/phone-control.ts +156 -0
- package/src/phone-runtime/audio-playout-tracker.ts +132 -0
- package/src/phone-runtime/openai-twilio-realtime.ts +1250 -0
- package/src/tools.ts +227 -0
- package/src/types.ts +529 -0
- package/src/utils.ts +11 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
CLOUD_VOICE_PHONE_HANGUP_TOOL,
|
|
4
|
+
CLOUD_VOICE_SIP_TRANSFER_TOOL,
|
|
5
|
+
buildPhoneOpeningPrompt,
|
|
6
|
+
compileCloudVoiceGraphConfig,
|
|
7
|
+
compileVoiceGraphToInstructions,
|
|
8
|
+
createPhonePrepareSnapshot,
|
|
9
|
+
executeCloudVoiceToolBinding,
|
|
10
|
+
normalizeCloudVoiceHumanizationConfig,
|
|
11
|
+
normalizeCloudVoicePipelineConfig,
|
|
12
|
+
prepareCloudVoiceSessionConfig,
|
|
13
|
+
resolveCloudVoiceChannel,
|
|
14
|
+
resolveCloudVoiceParameters,
|
|
15
|
+
toOpenAITurnDetection,
|
|
16
|
+
} from "../index";
|
|
17
|
+
import type { CloudVoiceAgentConfig } from "../types";
|
|
18
|
+
|
|
19
|
+
const baseConfig: CloudVoiceAgentConfig = {
|
|
20
|
+
instructions: "Help customers.",
|
|
21
|
+
provider: "openai-realtime",
|
|
22
|
+
model: "gpt-4o-realtime-preview",
|
|
23
|
+
voice: "alloy",
|
|
24
|
+
transport: "websocket",
|
|
25
|
+
transcription: {},
|
|
26
|
+
turnDetection: { type: "server_vad" },
|
|
27
|
+
inputNoiseReduction: null,
|
|
28
|
+
tools: [{
|
|
29
|
+
id: "lookup_order",
|
|
30
|
+
type: "external_webhook",
|
|
31
|
+
name: "Lookup order",
|
|
32
|
+
description: "Look up an order",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: { orderNumber: { type: "string" } },
|
|
36
|
+
},
|
|
37
|
+
config: {
|
|
38
|
+
url: "https://example.test/tools/lookup-order",
|
|
39
|
+
},
|
|
40
|
+
}],
|
|
41
|
+
knowledgeBases: [],
|
|
42
|
+
widget: { title: "Talk" },
|
|
43
|
+
channels: { web: true, iframe: true, script: true, phone: true, sip: false, outbound: true },
|
|
44
|
+
metadata: {
|
|
45
|
+
speech: {
|
|
46
|
+
language: "tr",
|
|
47
|
+
accent: "standard Istanbul Turkish",
|
|
48
|
+
style: "warm",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
describe("@kognitivedev/backend-cloud voice preparation", () => {
|
|
54
|
+
it("resolves supported channels and rejects unknown channels", () => {
|
|
55
|
+
expect(resolveCloudVoiceChannel(undefined)).toBe("web");
|
|
56
|
+
expect(resolveCloudVoiceChannel("outbound")).toBe("outbound");
|
|
57
|
+
expect(() => resolveCloudVoiceChannel("fax")).toThrow("Unsupported cloud voice channel");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("prepares web sessions without phone control tools", () => {
|
|
61
|
+
const prepare = prepareCloudVoiceSessionConfig(baseConfig, {
|
|
62
|
+
agentName: "Support",
|
|
63
|
+
sessionId: "voice_session_1",
|
|
64
|
+
resourceId: { userId: "user-1" },
|
|
65
|
+
channel: "web",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(prepare.toolManifest.map((tool) => tool.name)).toEqual(["lookup_order"]);
|
|
69
|
+
expect(prepare.system).toContain("## Konuşma Yönlendirmesi");
|
|
70
|
+
expect(prepare.system).toContain("Dil: Turkish");
|
|
71
|
+
expect(prepare.system).toContain("Aksan: standard Istanbul Turkish");
|
|
72
|
+
expect(prepare.system).toContain("## Kişilik, Ton ve Tempo");
|
|
73
|
+
expect(prepare.system).toContain("## Araç Konuşması");
|
|
74
|
+
expect(prepare.voiceConfig.system).toBe(prepare.system);
|
|
75
|
+
expect(prepare.voiceConfig.transcription).toEqual({ language: "tr" });
|
|
76
|
+
expect(prepare.voiceConfig.speech).toEqual({
|
|
77
|
+
language: "tr",
|
|
78
|
+
accent: "standard Istanbul Turkish",
|
|
79
|
+
style: "warm",
|
|
80
|
+
});
|
|
81
|
+
expect(prepare.voiceConfig.languageCode).toBe("tr-TR");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("passes model temperature and max output tokens into prepared voice config", () => {
|
|
85
|
+
const prepare = prepareCloudVoiceSessionConfig({
|
|
86
|
+
...baseConfig,
|
|
87
|
+
providerOptions: {
|
|
88
|
+
temperature: 0.7,
|
|
89
|
+
maxOutputTokens: 640,
|
|
90
|
+
},
|
|
91
|
+
}, {
|
|
92
|
+
agentName: "Support",
|
|
93
|
+
sessionId: "voice_session_1",
|
|
94
|
+
resourceId: { userId: "user-1" },
|
|
95
|
+
channel: "web",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(prepare.voiceConfig.temperature).toBe(0.7);
|
|
99
|
+
expect(prepare.voiceConfig.maxOutputTokens).toBe(640);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("defaults OpenAI phone sessions to near-field input noise reduction when unset", () => {
|
|
103
|
+
const prepare = prepareCloudVoiceSessionConfig({
|
|
104
|
+
...baseConfig,
|
|
105
|
+
inputNoiseReduction: undefined,
|
|
106
|
+
}, {
|
|
107
|
+
agentName: "Support",
|
|
108
|
+
sessionId: "voice_session_1",
|
|
109
|
+
resourceId: { userId: "user-1" },
|
|
110
|
+
channel: "phone",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(prepare.voiceConfig.inputNoiseReduction).toEqual({ type: "near_field" });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("preserves explicit disabled noise reduction for OpenAI phone sessions", () => {
|
|
117
|
+
const prepare = prepareCloudVoiceSessionConfig({
|
|
118
|
+
...baseConfig,
|
|
119
|
+
inputNoiseReduction: null,
|
|
120
|
+
}, {
|
|
121
|
+
agentName: "Support",
|
|
122
|
+
sessionId: "voice_session_1",
|
|
123
|
+
resourceId: { userId: "user-1" },
|
|
124
|
+
channel: "phone",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(prepare.voiceConfig.inputNoiseReduction).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("renders resolved parameters into instructions, graph nodes, and prepared metadata", () => {
|
|
131
|
+
const prepare = prepareCloudVoiceSessionConfig({
|
|
132
|
+
...baseConfig,
|
|
133
|
+
instructions: "User calling: {{phone}} with name: {{params.name}}.",
|
|
134
|
+
metadata: {
|
|
135
|
+
flowGraph: {
|
|
136
|
+
version: 1,
|
|
137
|
+
startNodeId: "n_start",
|
|
138
|
+
nodes: [{
|
|
139
|
+
id: "n_start",
|
|
140
|
+
type: "initial",
|
|
141
|
+
title: "Greeting",
|
|
142
|
+
prompt: "Greet {{name}} at {{parameters.phone}}.",
|
|
143
|
+
}],
|
|
144
|
+
edges: [],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
parameters: [
|
|
148
|
+
{ key: "name", type: "string", source: "custom", required: true },
|
|
149
|
+
{ key: "phone", type: "string", source: "system", preset: "phone" },
|
|
150
|
+
],
|
|
151
|
+
}, {
|
|
152
|
+
agentName: "Support",
|
|
153
|
+
sessionId: "voice_session_1",
|
|
154
|
+
resourceId: { userId: "user-1" },
|
|
155
|
+
channel: "web",
|
|
156
|
+
parameters: { name: "Ada", phone: "+15551234567" },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(prepare.system).toContain("User calling: +15551234567 with name: Ada.");
|
|
160
|
+
expect(prepare.system).toContain("Greet Ada at +15551234567.");
|
|
161
|
+
expect(prepare.parameters).toEqual({ name: "Ada", phone: "+15551234567" });
|
|
162
|
+
expect(prepare.metadata.parameterKeys).toEqual(["name", "phone"]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("enforces required custom parameters in strict mode and records misses in permissive mode", () => {
|
|
166
|
+
const config: CloudVoiceAgentConfig = {
|
|
167
|
+
...baseConfig,
|
|
168
|
+
parameters: [{ key: "name", type: "string", source: "custom", required: true }],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
expect(() => resolveCloudVoiceParameters({ config, supplied: {}, system: {}, mode: "strict" })).toThrow("Cloud voice parameters are invalid");
|
|
172
|
+
expect(resolveCloudVoiceParameters({ config, supplied: {}, system: {}, mode: "permissive" })).toMatchObject({
|
|
173
|
+
values: {},
|
|
174
|
+
missingRequired: ["name"],
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("normalizes kognitive voice pipeline config and exposes it on prepared runtime", () => {
|
|
179
|
+
const config: CloudVoiceAgentConfig = {
|
|
180
|
+
...baseConfig,
|
|
181
|
+
provider: "kognitive-voice",
|
|
182
|
+
model: "unused-realtime-model",
|
|
183
|
+
voice: "alloy",
|
|
184
|
+
transport: "websocket",
|
|
185
|
+
pipeline: {
|
|
186
|
+
transport: { type: "webrtc", provider: "daily" },
|
|
187
|
+
stt: { provider: "deepgram", model: "nova-3", language: "en" },
|
|
188
|
+
llm: { provider: "openai", model: "gpt-4o-mini" },
|
|
189
|
+
tts: { provider: "cartesia", model: "sonic-3", voice: "voice_123" },
|
|
190
|
+
turn: { interruptResponse: true, silenceDurationMs: 650 },
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
expect(normalizeCloudVoicePipelineConfig(config)).toMatchObject({
|
|
195
|
+
transport: { type: "webrtc", provider: "daily" },
|
|
196
|
+
stt: { provider: "deepgram", model: "nova-3", language: "en" },
|
|
197
|
+
llm: { provider: "openai", model: "gpt-4o-mini" },
|
|
198
|
+
tts: { provider: "cartesia", model: "sonic-3", voice: "voice_123" },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const prepare = prepareCloudVoiceSessionConfig(config, {
|
|
202
|
+
agentName: "Support",
|
|
203
|
+
sessionId: "voice_session_1",
|
|
204
|
+
resourceId: { userId: "user-1" },
|
|
205
|
+
channel: "web",
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(prepare.runtime).toMatchObject({
|
|
209
|
+
provider: "kognitive-voice",
|
|
210
|
+
mode: "pipeline",
|
|
211
|
+
transport: "webrtc",
|
|
212
|
+
model: "gpt-4o-mini",
|
|
213
|
+
voice: "voice_123",
|
|
214
|
+
pipeline: {
|
|
215
|
+
transport: { type: "webrtc", provider: "daily" },
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
expect(prepare.voiceConfig).toMatchObject({
|
|
219
|
+
model: "gpt-4o-mini",
|
|
220
|
+
voice: "voice_123",
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("compiles flow graphs into routing instructions and matched tool manifests", () => {
|
|
225
|
+
const compiled = compileVoiceGraphToInstructions({
|
|
226
|
+
startNodeId: "n_start",
|
|
227
|
+
nodes: [{
|
|
228
|
+
id: "n_start",
|
|
229
|
+
type: "initial",
|
|
230
|
+
title: "Welcome and intent",
|
|
231
|
+
prompt: "Greet the caller and classify intent.",
|
|
232
|
+
firstMessage: "Hi, how can I help?",
|
|
233
|
+
outputs: [{
|
|
234
|
+
id: "o_order",
|
|
235
|
+
label: "Order status",
|
|
236
|
+
desc: "Caller asks about an order, tracking, or shipment.",
|
|
237
|
+
}],
|
|
238
|
+
}, {
|
|
239
|
+
id: "n_lookup",
|
|
240
|
+
type: "tool",
|
|
241
|
+
title: "Lookup order",
|
|
242
|
+
prompt: "Collect the order number before calling the tool.",
|
|
243
|
+
toolId: "lookup_order",
|
|
244
|
+
}, {
|
|
245
|
+
id: "n_end",
|
|
246
|
+
type: "end",
|
|
247
|
+
title: "Close call",
|
|
248
|
+
prompt: "Close politely.",
|
|
249
|
+
}],
|
|
250
|
+
edges: [
|
|
251
|
+
{ from: "n_start", fromPort: "o_order", to: "n_lookup" },
|
|
252
|
+
{ from: "n_lookup", fromPort: null, to: "n_end" },
|
|
253
|
+
],
|
|
254
|
+
}, { tools: baseConfig.tools });
|
|
255
|
+
|
|
256
|
+
expect(compiled).toMatchObject({
|
|
257
|
+
startNodeId: "n_start",
|
|
258
|
+
nodeCount: 3,
|
|
259
|
+
edgeCount: 2,
|
|
260
|
+
referencedToolIds: ["lookup_order"],
|
|
261
|
+
missingToolIds: [],
|
|
262
|
+
});
|
|
263
|
+
expect(compiled?.toolManifest).toEqual([expect.objectContaining({
|
|
264
|
+
name: "lookup_order",
|
|
265
|
+
parameters: baseConfig.tools[0].inputSchema,
|
|
266
|
+
})]);
|
|
267
|
+
expect(compiled?.instructions).toContain("Greet the caller and classify intent.");
|
|
268
|
+
expect(compiled?.instructions).toContain("say exactly this as your first sentence");
|
|
269
|
+
expect(compiled?.instructions).toContain("Hi, how can I help?");
|
|
270
|
+
expect(compiled?.instructions).toContain("Do not prepend, replace, or append an internal agent name");
|
|
271
|
+
expect(compiled?.instructions).toContain("When Caller asks about an order, tracking, or shipment.");
|
|
272
|
+
expect(compiled?.instructions).toContain("Use Lookup order (`lookup_order`)");
|
|
273
|
+
expect(compiled?.instructions).toContain("Then end with: Close politely.");
|
|
274
|
+
expect(compiled?.compilerVersion).toBe("voice-flow-paths-v1");
|
|
275
|
+
expect(compiled?.graphSignature).toEqual(expect.any(String));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("selects the matching initial flow node for each call direction", () => {
|
|
279
|
+
const graph = {
|
|
280
|
+
startNodeId: "n_incoming",
|
|
281
|
+
nodes: [{
|
|
282
|
+
id: "n_incoming",
|
|
283
|
+
type: "initial",
|
|
284
|
+
entryMode: "incoming",
|
|
285
|
+
title: "Incoming start",
|
|
286
|
+
prompt: "Handle incoming conversations.",
|
|
287
|
+
firstMessage: "Thanks for calling.",
|
|
288
|
+
}, {
|
|
289
|
+
id: "n_outgoing",
|
|
290
|
+
type: "initial",
|
|
291
|
+
entryMode: "outgoing",
|
|
292
|
+
title: "Outgoing start",
|
|
293
|
+
prompt: "Handle outbound conversations.",
|
|
294
|
+
firstMessage: "Hi, I am calling about your appointment.",
|
|
295
|
+
}],
|
|
296
|
+
edges: [],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const incoming = compileVoiceGraphToInstructions(graph, { channel: "phone" });
|
|
300
|
+
const outgoing = compileVoiceGraphToInstructions(graph, { channel: "outbound" });
|
|
301
|
+
|
|
302
|
+
expect(incoming).toMatchObject({ startNodeId: "n_incoming", entryMode: "incoming" });
|
|
303
|
+
expect(incoming?.instructions).toContain("Handle incoming conversations.");
|
|
304
|
+
expect(incoming?.instructions).toContain("Thanks for calling.");
|
|
305
|
+
expect(incoming?.instructions).not.toContain("Handle outbound conversations.");
|
|
306
|
+
expect(outgoing).toMatchObject({ startNodeId: "n_outgoing", entryMode: "outgoing" });
|
|
307
|
+
expect(outgoing?.instructions).toContain("Handle outbound conversations.");
|
|
308
|
+
expect(outgoing?.instructions).toContain("Hi, I am calling about your appointment.");
|
|
309
|
+
expect(outgoing?.instructions).not.toContain("Handle incoming conversations.");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("compiles sub-agent nodes as blocking tool calls with outcome routing", () => {
|
|
313
|
+
const subAgentTool = {
|
|
314
|
+
id: "voice_agent_boss_approval",
|
|
315
|
+
type: "cloud_voice_agent" as const,
|
|
316
|
+
name: "Voice Agent: Boss approval",
|
|
317
|
+
description: "Call the boss and wait for a decision.",
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: "object",
|
|
320
|
+
required: ["discount"],
|
|
321
|
+
properties: { discount: { type: "number" } },
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
const compiled = compileVoiceGraphToInstructions({
|
|
325
|
+
startNodeId: "n_start",
|
|
326
|
+
nodes: [{
|
|
327
|
+
id: "n_start",
|
|
328
|
+
type: "initial",
|
|
329
|
+
title: "Welcome",
|
|
330
|
+
prompt: "Negotiate the price.",
|
|
331
|
+
outputs: [{ id: "o_discount", label: "Discount", desc: "Broker asks for a discount." }],
|
|
332
|
+
}, {
|
|
333
|
+
id: "n_ask_boss",
|
|
334
|
+
type: "sub_agent",
|
|
335
|
+
title: "Ask boss",
|
|
336
|
+
prompt: "Ask if the requested discount can be accepted.",
|
|
337
|
+
subAgentToolId: "voice_agent_boss_approval",
|
|
338
|
+
outputs: [
|
|
339
|
+
{ id: "outcome_accept", label: "accept", desc: "Boss accepts." },
|
|
340
|
+
{ id: "outcome_decline", label: "decline", desc: "Boss declines." },
|
|
341
|
+
],
|
|
342
|
+
}],
|
|
343
|
+
edges: [{ from: "n_start", fromPort: "o_discount", to: "n_ask_boss" }],
|
|
344
|
+
}, { tools: [subAgentTool] });
|
|
345
|
+
|
|
346
|
+
expect(compiled).toMatchObject({
|
|
347
|
+
referencedToolIds: ["voice_agent_boss_approval"],
|
|
348
|
+
missingToolIds: [],
|
|
349
|
+
});
|
|
350
|
+
expect(compiled?.toolManifest).toEqual([expect.objectContaining({
|
|
351
|
+
name: "voice_agent_boss_approval",
|
|
352
|
+
parameters: subAgentTool.inputSchema,
|
|
353
|
+
})]);
|
|
354
|
+
expect(compiled?.instructions).toContain("Call sub-agent tool Voice Agent: Boss approval (`voice_agent_boss_approval`) after collecting: discount. Wait for its final result, read the returned `outcome`, and continue through the matching outcome branch.");
|
|
355
|
+
expect(compiled?.instructions).toContain("Sub-agent outcome branches: accept (Boss accepts.); decline (Boss declines.).");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("appends metadata flow graphs to prepared system instructions for legacy uncompiled configs", () => {
|
|
359
|
+
const prepare = prepareCloudVoiceSessionConfig({
|
|
360
|
+
...baseConfig,
|
|
361
|
+
metadata: {
|
|
362
|
+
...baseConfig.metadata,
|
|
363
|
+
flowGraph: {
|
|
364
|
+
nodes: [{
|
|
365
|
+
id: "n_start",
|
|
366
|
+
type: "initial",
|
|
367
|
+
title: "Welcome",
|
|
368
|
+
prompt: "Greet and route.",
|
|
369
|
+
outputs: [{ id: "o_order", label: "Order status", desc: "Order questions." }],
|
|
370
|
+
}, {
|
|
371
|
+
id: "n_lookup",
|
|
372
|
+
type: "tool",
|
|
373
|
+
title: "Lookup order",
|
|
374
|
+
toolId: "lookup_order",
|
|
375
|
+
}],
|
|
376
|
+
edges: [{ from: "n_start", fromPort: "o_order", to: "n_lookup" }],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
}, {
|
|
380
|
+
agentName: "Support",
|
|
381
|
+
sessionId: "voice_session_1",
|
|
382
|
+
resourceId: { userId: "user-1" },
|
|
383
|
+
channel: "web",
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
expect(prepare.system).toContain("Greet and route.");
|
|
387
|
+
expect(prepare.system).toContain("When Order questions.");
|
|
388
|
+
expect(prepare.metadata.flowGraph).toEqual({
|
|
389
|
+
startNodeId: "n_start",
|
|
390
|
+
entryMode: "both",
|
|
391
|
+
nodeCount: 2,
|
|
392
|
+
edgeCount: 1,
|
|
393
|
+
referencedToolIds: ["lookup_order"],
|
|
394
|
+
missingToolIds: [],
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("compiles flow graph instructions into config and does not append them twice at runtime", () => {
|
|
399
|
+
const compiledConfig = compileCloudVoiceGraphConfig({
|
|
400
|
+
...baseConfig,
|
|
401
|
+
instructions: "Old dashboard prompt.",
|
|
402
|
+
metadata: {
|
|
403
|
+
...baseConfig.metadata,
|
|
404
|
+
flowGraph: {
|
|
405
|
+
nodes: [{
|
|
406
|
+
id: "n_start",
|
|
407
|
+
type: "initial",
|
|
408
|
+
title: "Welcome",
|
|
409
|
+
prompt: "You are Aria. Route callers by intent.",
|
|
410
|
+
outputs: [{ id: "o_refund", label: "Refund request", desc: "Caller asks for a refund." }],
|
|
411
|
+
}, {
|
|
412
|
+
id: "n_lookup",
|
|
413
|
+
type: "tool",
|
|
414
|
+
title: "Lookup order",
|
|
415
|
+
prompt: "Confirm the order before checking refund policy.",
|
|
416
|
+
toolId: "lookup_order",
|
|
417
|
+
}, {
|
|
418
|
+
id: "n_end",
|
|
419
|
+
type: "end",
|
|
420
|
+
title: "Close",
|
|
421
|
+
prompt: "Explain the result and close politely.",
|
|
422
|
+
}],
|
|
423
|
+
edges: [
|
|
424
|
+
{ from: "n_start", fromPort: "o_refund", to: "n_lookup" },
|
|
425
|
+
{ from: "n_lookup", fromPort: null, to: "n_end" },
|
|
426
|
+
],
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
expect(compiledConfig.instructions).not.toContain("Old dashboard prompt.");
|
|
432
|
+
expect(compiledConfig.instructions).toContain("When Caller asks for a refund.");
|
|
433
|
+
expect(compiledConfig.instructions).toContain("Use Lookup order (`lookup_order`)");
|
|
434
|
+
expect(compiledConfig.metadata?.flowGraphInstructionCompiler).toMatchObject({
|
|
435
|
+
version: "voice-flow-paths-v1",
|
|
436
|
+
graphSignature: expect.any(String),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const prepare = prepareCloudVoiceSessionConfig(compiledConfig, {
|
|
440
|
+
agentName: "Support",
|
|
441
|
+
sessionId: "voice_session_1",
|
|
442
|
+
resourceId: { userId: "user-1" },
|
|
443
|
+
channel: "web",
|
|
444
|
+
});
|
|
445
|
+
expect(prepare.system.match(/When Caller asks for a refund\./g)).toHaveLength(1);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("uses initial flow node runtime settings when preparing and publishing graph configs", () => {
|
|
449
|
+
const graphConfig = {
|
|
450
|
+
...baseConfig,
|
|
451
|
+
provider: "openai-realtime" as const,
|
|
452
|
+
model: "gpt-realtime-2",
|
|
453
|
+
voice: "marin",
|
|
454
|
+
transport: "webrtc" as const,
|
|
455
|
+
providerOptions: { reasoning: { effort: "low" } },
|
|
456
|
+
metadata: {
|
|
457
|
+
...baseConfig.metadata,
|
|
458
|
+
flowGraph: {
|
|
459
|
+
nodes: [{
|
|
460
|
+
id: "n_start",
|
|
461
|
+
type: "initial",
|
|
462
|
+
title: "Welcome",
|
|
463
|
+
prompt: "Route callers.",
|
|
464
|
+
provider: "xai-realtime" as const,
|
|
465
|
+
model: "grok-voice-think-fast-1.0",
|
|
466
|
+
voice: "eve",
|
|
467
|
+
transport: "websocket" as const,
|
|
468
|
+
providerOptions: {},
|
|
469
|
+
transcription: null,
|
|
470
|
+
inputNoiseReduction: null,
|
|
471
|
+
}],
|
|
472
|
+
edges: [],
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const compiledConfig = compileCloudVoiceGraphConfig(graphConfig);
|
|
478
|
+
expect(compiledConfig).toMatchObject({
|
|
479
|
+
provider: "xai-realtime",
|
|
480
|
+
model: "grok-voice-think-fast-1.0",
|
|
481
|
+
voice: "eve",
|
|
482
|
+
transport: "websocket",
|
|
483
|
+
providerOptions: {},
|
|
484
|
+
transcription: null,
|
|
485
|
+
inputNoiseReduction: null,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const prepare = prepareCloudVoiceSessionConfig(graphConfig, {
|
|
489
|
+
agentName: "Support",
|
|
490
|
+
sessionId: "voice_session_1",
|
|
491
|
+
resourceId: { userId: "caller-1" },
|
|
492
|
+
channel: "outbound",
|
|
493
|
+
});
|
|
494
|
+
expect(prepare.runtime).toMatchObject({
|
|
495
|
+
provider: "xai-realtime",
|
|
496
|
+
model: "grok-voice-think-fast-1.0",
|
|
497
|
+
voice: "eve",
|
|
498
|
+
transport: "websocket",
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("prepares direction-specific prompts and runtime settings from separate initial nodes", () => {
|
|
503
|
+
const graphConfig: CloudVoiceAgentConfig = {
|
|
504
|
+
...baseConfig,
|
|
505
|
+
provider: "openai-realtime",
|
|
506
|
+
model: "gpt-realtime-2",
|
|
507
|
+
voice: "marin",
|
|
508
|
+
transport: "webrtc",
|
|
509
|
+
metadata: {
|
|
510
|
+
...baseConfig.metadata,
|
|
511
|
+
flowGraph: {
|
|
512
|
+
startNodeId: "n_incoming",
|
|
513
|
+
nodes: [{
|
|
514
|
+
id: "n_incoming",
|
|
515
|
+
type: "initial",
|
|
516
|
+
entryMode: "incoming",
|
|
517
|
+
title: "Incoming start",
|
|
518
|
+
prompt: "Use the inbound service flow.",
|
|
519
|
+
provider: "openai-realtime",
|
|
520
|
+
model: "gpt-realtime-2",
|
|
521
|
+
voice: "marin",
|
|
522
|
+
transport: "webrtc",
|
|
523
|
+
}, {
|
|
524
|
+
id: "n_outgoing",
|
|
525
|
+
type: "initial",
|
|
526
|
+
entryMode: "outgoing",
|
|
527
|
+
title: "Outgoing start",
|
|
528
|
+
prompt: "Use the outbound reminder flow.",
|
|
529
|
+
provider: "xai-realtime",
|
|
530
|
+
model: "grok-voice-think-fast-1.0",
|
|
531
|
+
voice: "eve",
|
|
532
|
+
transport: "websocket",
|
|
533
|
+
transcription: null,
|
|
534
|
+
inputNoiseReduction: null,
|
|
535
|
+
}],
|
|
536
|
+
edges: [],
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const incoming = prepareCloudVoiceSessionConfig(graphConfig, {
|
|
542
|
+
agentName: "Support",
|
|
543
|
+
sessionId: "voice_session_in",
|
|
544
|
+
resourceId: { userId: "caller-1" },
|
|
545
|
+
channel: "phone",
|
|
546
|
+
});
|
|
547
|
+
const outgoing = prepareCloudVoiceSessionConfig(graphConfig, {
|
|
548
|
+
agentName: "Support",
|
|
549
|
+
sessionId: "voice_session_out",
|
|
550
|
+
resourceId: { userId: "caller-1" },
|
|
551
|
+
channel: "outbound",
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
expect(incoming.runtime).toMatchObject({
|
|
555
|
+
provider: "openai-realtime",
|
|
556
|
+
model: "gpt-realtime-2",
|
|
557
|
+
voice: "marin",
|
|
558
|
+
transport: "webrtc",
|
|
559
|
+
});
|
|
560
|
+
expect(incoming.system).toContain("Use the inbound service flow.");
|
|
561
|
+
expect(incoming.system).not.toContain("Use the outbound reminder flow.");
|
|
562
|
+
expect(incoming.metadata.flowGraph).toMatchObject({
|
|
563
|
+
startNodeId: "n_incoming",
|
|
564
|
+
entryMode: "incoming",
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
expect(outgoing.runtime).toMatchObject({
|
|
568
|
+
provider: "xai-realtime",
|
|
569
|
+
model: "grok-voice-think-fast-1.0",
|
|
570
|
+
voice: "eve",
|
|
571
|
+
transport: "websocket",
|
|
572
|
+
});
|
|
573
|
+
expect(outgoing.voiceConfig.transcription).toBeNull();
|
|
574
|
+
expect(outgoing.voiceConfig.inputNoiseReduction).toBeNull();
|
|
575
|
+
expect(outgoing.system).toContain("Use the outbound reminder flow.");
|
|
576
|
+
expect(outgoing.system).not.toContain("Use the inbound service flow.");
|
|
577
|
+
expect(outgoing.metadata.flowGraph).toMatchObject({
|
|
578
|
+
startNodeId: "n_outgoing",
|
|
579
|
+
entryMode: "outgoing",
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("injects SIP transfer control when transfer destinations are configured", () => {
|
|
584
|
+
const prepare = prepareCloudVoiceSessionConfig({
|
|
585
|
+
...baseConfig,
|
|
586
|
+
metadata: {
|
|
587
|
+
...baseConfig.metadata,
|
|
588
|
+
transferDestinations: [{
|
|
589
|
+
id: "support_queue",
|
|
590
|
+
name: "Support queue",
|
|
591
|
+
provider: "telnyx",
|
|
592
|
+
destination: { type: "sip_uri", uri: "sip:support@example.com" },
|
|
593
|
+
policy: { mode: "warm", timeoutSeconds: 25, fallback: "return_to_ai" },
|
|
594
|
+
}],
|
|
595
|
+
},
|
|
596
|
+
}, {
|
|
597
|
+
agentName: "Support",
|
|
598
|
+
sessionId: "voice_session_1",
|
|
599
|
+
resourceId: { userId: "caller-1" },
|
|
600
|
+
channel: "sip",
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
expect(prepare.toolManifest).toEqual(expect.arrayContaining([
|
|
604
|
+
CLOUD_VOICE_SIP_TRANSFER_TOOL,
|
|
605
|
+
]));
|
|
606
|
+
expect(prepare.metadata.transferDestinations).toEqual(["support_queue"]);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("preserves xAI realtime provider defaults in prepared sessions", () => {
|
|
610
|
+
const prepare = prepareCloudVoiceSessionConfig({
|
|
611
|
+
...baseConfig,
|
|
612
|
+
provider: "xai-realtime",
|
|
613
|
+
model: "grok-voice-think-fast-1.0",
|
|
614
|
+
voice: "eve",
|
|
615
|
+
transport: "websocket",
|
|
616
|
+
transcription: null,
|
|
617
|
+
inputNoiseReduction: null,
|
|
618
|
+
}, {
|
|
619
|
+
agentName: "Support",
|
|
620
|
+
sessionId: "voice_session_1",
|
|
621
|
+
resourceId: { userId: "user-1" },
|
|
622
|
+
channel: "web",
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
expect(prepare.runtime).toEqual(expect.objectContaining({
|
|
626
|
+
provider: "xai-realtime",
|
|
627
|
+
mode: "realtime",
|
|
628
|
+
model: "grok-voice-think-fast-1.0",
|
|
629
|
+
voice: "eve",
|
|
630
|
+
transport: "websocket",
|
|
631
|
+
}));
|
|
632
|
+
expect(prepare.voiceConfig.transcription).toBeNull();
|
|
633
|
+
expect(prepare.voiceConfig.inputNoiseReduction).toBeNull();
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it("normalizes humanization and can omit naturalness rules", () => {
|
|
637
|
+
expect(normalizeCloudVoiceHumanizationConfig(undefined)).toEqual({
|
|
638
|
+
enabled: true,
|
|
639
|
+
openingMode: "auto",
|
|
640
|
+
openingStyle: "brief",
|
|
641
|
+
fillerStyle: "light",
|
|
642
|
+
backchannelFrequency: "low",
|
|
643
|
+
disfluency: "rare",
|
|
644
|
+
toolLatencyFillerMs: 700,
|
|
645
|
+
conversationProfile: {
|
|
646
|
+
personality: "warm",
|
|
647
|
+
tone: "professional",
|
|
648
|
+
pacing: "concise",
|
|
649
|
+
unclearAudio: "ask_repeat",
|
|
650
|
+
confirmation: "critical_fields",
|
|
651
|
+
escalation: "when_blocked",
|
|
652
|
+
numberReadback: true,
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
const prepare = prepareCloudVoiceSessionConfig({
|
|
657
|
+
...baseConfig,
|
|
658
|
+
humanization: { enabled: false },
|
|
659
|
+
}, {
|
|
660
|
+
agentName: "Support",
|
|
661
|
+
sessionId: "voice_session_1",
|
|
662
|
+
resourceId: {},
|
|
663
|
+
channel: "phone",
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
expect(prepare.system).not.toContain("Conversation naturalness:");
|
|
667
|
+
expect(prepare.system).toContain("yalnızca bir soruyu cevapladın veya bir işi tamamladın diye çağrıyı bitirme");
|
|
668
|
+
expect(prepare.system).toContain("hang_up_call aracını yalnızca arayan açıkça çağrıyı bitirmek isterse");
|
|
669
|
+
expect(prepare.system).toContain("hang_up_call aracını çağırmadan hemen önce");
|
|
670
|
+
expect(prepare.system).toContain("Arayan hangi araçlara, fonksiyonlara, yeteneklere veya işlemlere erişebildiğini sorarsa");
|
|
671
|
+
expect(prepare.system).toContain("dahili agent adını");
|
|
672
|
+
expect(prepare.system).not.toContain("You may introduce yourself as Support");
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("builds phone opening prompts from humanization and speech config", () => {
|
|
676
|
+
expect(buildPhoneOpeningPrompt({
|
|
677
|
+
config: {
|
|
678
|
+
humanization: { openingMode: "auto", openingStyle: "warm" },
|
|
679
|
+
metadata: { speech: { language: "tr" } },
|
|
680
|
+
},
|
|
681
|
+
agentName: "Support",
|
|
682
|
+
channel: "outbound",
|
|
683
|
+
})).toContain("Turkish dilinde sıcak ve samimi bir selamlamayla başlat");
|
|
684
|
+
expect(buildPhoneOpeningPrompt({
|
|
685
|
+
config: {
|
|
686
|
+
humanization: { openingMode: "auto", openingStyle: "warm" },
|
|
687
|
+
metadata: {
|
|
688
|
+
flowGraph: {
|
|
689
|
+
startNodeId: "n_start",
|
|
690
|
+
nodes: [{
|
|
691
|
+
id: "n_start",
|
|
692
|
+
type: "initial",
|
|
693
|
+
firstMessage: "Merhaba, ben Evital'dan Vahit, sizi yaklaşan randevunuzu hatırlatmak için arıyorum.",
|
|
694
|
+
}],
|
|
695
|
+
edges: [],
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
agentName: "Voice agent 11",
|
|
700
|
+
channel: "outbound",
|
|
701
|
+
})).toContain("say exactly this first sentence");
|
|
702
|
+
expect(buildPhoneOpeningPrompt({
|
|
703
|
+
config: {
|
|
704
|
+
humanization: { openingMode: "auto", openingStyle: "warm" },
|
|
705
|
+
metadata: {
|
|
706
|
+
flowGraph: {
|
|
707
|
+
startNodeId: "n_start",
|
|
708
|
+
nodes: [{
|
|
709
|
+
id: "n_start",
|
|
710
|
+
type: "initial",
|
|
711
|
+
firstMessage: "Merhaba, ben Evital'dan Vahit, sizi yaklaşan randevunuzu hatırlatmak için arıyorum.",
|
|
712
|
+
}],
|
|
713
|
+
edges: [],
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
agentName: "Voice agent 11",
|
|
718
|
+
channel: "outbound",
|
|
719
|
+
})).not.toContain("Voice agent 11");
|
|
720
|
+
|
|
721
|
+
expect(buildPhoneOpeningPrompt({
|
|
722
|
+
config: {
|
|
723
|
+
humanization: { openingMode: "auto", openingStyle: "warm" },
|
|
724
|
+
metadata: {
|
|
725
|
+
flowGraph: {
|
|
726
|
+
startNodeId: "n_incoming",
|
|
727
|
+
nodes: [{
|
|
728
|
+
id: "n_incoming",
|
|
729
|
+
type: "initial",
|
|
730
|
+
entryMode: "incoming",
|
|
731
|
+
firstMessage: "Thanks for calling Acme.",
|
|
732
|
+
}, {
|
|
733
|
+
id: "n_outgoing",
|
|
734
|
+
type: "initial",
|
|
735
|
+
entryMode: "outgoing",
|
|
736
|
+
firstMessage: "Hi, I am calling from Acme.",
|
|
737
|
+
}],
|
|
738
|
+
edges: [],
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
agentName: "Voice agent 11",
|
|
743
|
+
channel: "outbound",
|
|
744
|
+
})).toContain("Hi, I am calling from Acme.");
|
|
745
|
+
|
|
746
|
+
expect(buildPhoneOpeningPrompt({
|
|
747
|
+
config: {
|
|
748
|
+
humanization: { openingMode: "wait" },
|
|
749
|
+
metadata: { speech: { language: "en" } },
|
|
750
|
+
},
|
|
751
|
+
agentName: "Support",
|
|
752
|
+
channel: "phone",
|
|
753
|
+
})).toBeNull();
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it("injects hangup for phone sessions and dedupes client tool manifests", () => {
|
|
757
|
+
const prepare = prepareCloudVoiceSessionConfig(baseConfig, {
|
|
758
|
+
agentName: "Support",
|
|
759
|
+
sessionId: "voice_session_1",
|
|
760
|
+
resourceId: { userId: "caller-1" },
|
|
761
|
+
channel: "phone",
|
|
762
|
+
clientTools: [{
|
|
763
|
+
id: "lookup_order",
|
|
764
|
+
name: "Client lookup",
|
|
765
|
+
description: "Client side lookup",
|
|
766
|
+
inputSchema: { type: "object", properties: { id: { type: "string" } } },
|
|
767
|
+
}],
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
expect(prepare.toolManifest).toEqual(expect.arrayContaining([
|
|
771
|
+
expect.objectContaining({
|
|
772
|
+
name: "lookup_order",
|
|
773
|
+
description: "Client side lookup",
|
|
774
|
+
}),
|
|
775
|
+
CLOUD_VOICE_PHONE_HANGUP_TOOL,
|
|
776
|
+
]));
|
|
777
|
+
expect(prepare.toolManifest.at(-1)?.name).toBe("hang_up_call");
|
|
778
|
+
expect(prepare.metadata.clientTools).toEqual(["lookup_order"]);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it("normalizes OpenAI turn detection", () => {
|
|
782
|
+
expect(toOpenAITurnDetection({
|
|
783
|
+
type: "server_vad",
|
|
784
|
+
createResponse: true,
|
|
785
|
+
interruptResponse: false,
|
|
786
|
+
prefixPaddingMs: 250,
|
|
787
|
+
silenceDurationMs: 600,
|
|
788
|
+
threshold: 0.5,
|
|
789
|
+
eagerness: "medium",
|
|
790
|
+
})).toEqual({
|
|
791
|
+
type: "server_vad",
|
|
792
|
+
create_response: true,
|
|
793
|
+
interrupt_response: false,
|
|
794
|
+
prefix_padding_ms: 250,
|
|
795
|
+
silence_duration_ms: 600,
|
|
796
|
+
threshold: 0.5,
|
|
797
|
+
eagerness: "medium",
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it("creates stable phone prepare snapshots", () => {
|
|
802
|
+
const prepare = prepareCloudVoiceSessionConfig(baseConfig, {
|
|
803
|
+
agentName: "Support",
|
|
804
|
+
sessionId: "voice_session_1",
|
|
805
|
+
resourceId: { userId: "caller-1" },
|
|
806
|
+
channel: "phone",
|
|
807
|
+
});
|
|
808
|
+
const snapshot = createPhonePrepareSnapshot({
|
|
809
|
+
agent: {
|
|
810
|
+
id: "agent-1",
|
|
811
|
+
slug: "support",
|
|
812
|
+
name: "Support",
|
|
813
|
+
draftVersion: 3,
|
|
814
|
+
publishedVersion: 2,
|
|
815
|
+
},
|
|
816
|
+
channel: "phone",
|
|
817
|
+
config: baseConfig,
|
|
818
|
+
prepare,
|
|
819
|
+
now: new Date("2026-04-28T00:00:00.000Z"),
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
expect(snapshot).toMatchObject({
|
|
823
|
+
schemaVersion: 1,
|
|
824
|
+
createdAt: "2026-04-28T00:00:00.000Z",
|
|
825
|
+
channel: "phone",
|
|
826
|
+
agent: {
|
|
827
|
+
id: "agent-1",
|
|
828
|
+
slug: "support",
|
|
829
|
+
version: 2,
|
|
830
|
+
},
|
|
831
|
+
runtime: prepare.runtime,
|
|
832
|
+
voiceConfig: prepare.voiceConfig,
|
|
833
|
+
toolManifest: prepare.toolManifest,
|
|
834
|
+
});
|
|
835
|
+
expect(snapshot.config).toBe(baseConfig);
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
describe("@kognitivedev/backend-cloud tool execution", () => {
|
|
840
|
+
it("dispatches managed tools to supplied handlers", async () => {
|
|
841
|
+
await expect(executeCloudVoiceToolBinding({
|
|
842
|
+
projectId: "project-1",
|
|
843
|
+
sessionId: "session-1",
|
|
844
|
+
tool: {
|
|
845
|
+
id: "search",
|
|
846
|
+
type: "web_search",
|
|
847
|
+
name: "Search",
|
|
848
|
+
config: { parameters: { maxResults: 3 } },
|
|
849
|
+
},
|
|
850
|
+
args: { query: "status page" },
|
|
851
|
+
executeWebSearch: async (input) => ({ query: input.query, parameters: input.parameters }),
|
|
852
|
+
})).resolves.toEqual({
|
|
853
|
+
result: { query: "status page", parameters: { maxResults: 3 } },
|
|
854
|
+
metadata: {},
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it("dispatches calendar action tools to supplied handlers", async () => {
|
|
859
|
+
await expect(executeCloudVoiceToolBinding({
|
|
860
|
+
projectId: "project-1",
|
|
861
|
+
sessionId: "session-1",
|
|
862
|
+
tool: {
|
|
863
|
+
id: "calendar_hold_slot",
|
|
864
|
+
type: "calendar_action",
|
|
865
|
+
name: "Hold calendar slot",
|
|
866
|
+
config: { action: "calendar_hold_slot", resourceId: "resource-1" },
|
|
867
|
+
},
|
|
868
|
+
args: { serviceId: "service-1", startAt: "2026-05-20T17:00:00.000Z" },
|
|
869
|
+
executeCalendarAction: async (input) => input,
|
|
870
|
+
})).resolves.toEqual({
|
|
871
|
+
result: {
|
|
872
|
+
action: "calendar_hold_slot",
|
|
873
|
+
args: { serviceId: "service-1", startAt: "2026-05-20T17:00:00.000Z" },
|
|
874
|
+
config: { action: "calendar_hold_slot", resourceId: "resource-1" },
|
|
875
|
+
},
|
|
876
|
+
metadata: {},
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it("injects configured parameter bindings into managed tool arguments", async () => {
|
|
881
|
+
await expect(executeCloudVoiceToolBinding({
|
|
882
|
+
projectId: "project-1",
|
|
883
|
+
sessionId: "session-1",
|
|
884
|
+
tool: {
|
|
885
|
+
id: "lookup_order",
|
|
886
|
+
type: "external_webhook",
|
|
887
|
+
name: "Lookup order",
|
|
888
|
+
config: { url: "https://example.test/tools/lookup" },
|
|
889
|
+
parameterBindings: {
|
|
890
|
+
userId: { parameter: "user_id" },
|
|
891
|
+
phone: { parameter: "phone" },
|
|
892
|
+
},
|
|
893
|
+
},
|
|
894
|
+
args: { orderNumber: "KV-1042" },
|
|
895
|
+
parameters: { user_id: "user-1", phone: "+15551234567" },
|
|
896
|
+
executeExternalWebhook: async (input) => ({ args: input.args }),
|
|
897
|
+
})).resolves.toEqual({
|
|
898
|
+
result: { args: { orderNumber: "KV-1042", userId: "user-1", phone: "+15551234567" } },
|
|
899
|
+
metadata: {},
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it("dispatches SIP transfer tools to supplied handlers", async () => {
|
|
904
|
+
await expect(executeCloudVoiceToolBinding({
|
|
905
|
+
projectId: "project-1",
|
|
906
|
+
sessionId: "session-1",
|
|
907
|
+
tool: {
|
|
908
|
+
id: "transfer_to_support",
|
|
909
|
+
type: "sip_transfer",
|
|
910
|
+
name: "Transfer to support",
|
|
911
|
+
config: {
|
|
912
|
+
destination: { type: "sip_uri", uri: "sip:support@example.com" },
|
|
913
|
+
mode: "warm",
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
args: {
|
|
917
|
+
providerCallId: "call_1",
|
|
918
|
+
reason: "caller needs live support",
|
|
919
|
+
},
|
|
920
|
+
executeSipTransfer: async (input) => ({
|
|
921
|
+
provider: "telnyx",
|
|
922
|
+
providerCallId: input.providerCallId,
|
|
923
|
+
status: "active",
|
|
924
|
+
destination: input.destination,
|
|
925
|
+
mode: input.mode ?? "blind",
|
|
926
|
+
transferId: "transfer_1",
|
|
927
|
+
}),
|
|
928
|
+
})).resolves.toMatchObject({
|
|
929
|
+
result: {
|
|
930
|
+
provider: "telnyx",
|
|
931
|
+
providerCallId: "call_1",
|
|
932
|
+
status: "active",
|
|
933
|
+
transferId: "transfer_1",
|
|
934
|
+
mode: "warm",
|
|
935
|
+
},
|
|
936
|
+
metadata: {
|
|
937
|
+
transfer: {
|
|
938
|
+
providerCallId: "call_1",
|
|
939
|
+
transferId: "transfer_1",
|
|
940
|
+
mode: "warm",
|
|
941
|
+
status: "active",
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it("dispatches cloud voice agent tools with mapped parameters", async () => {
|
|
948
|
+
const executeCloudVoiceAgent = vi.fn(async (input) => ({
|
|
949
|
+
childSessionId: "child-session-1",
|
|
950
|
+
status: "completed",
|
|
951
|
+
outcome: "accept",
|
|
952
|
+
input,
|
|
953
|
+
}));
|
|
954
|
+
|
|
955
|
+
await expect(executeCloudVoiceToolBinding({
|
|
956
|
+
projectId: "project-1",
|
|
957
|
+
sessionId: "session-1",
|
|
958
|
+
tool: {
|
|
959
|
+
id: "voice_agent_boss_approval",
|
|
960
|
+
type: "cloud_voice_agent",
|
|
961
|
+
name: "Voice Agent: Boss approval",
|
|
962
|
+
config: {
|
|
963
|
+
agentSlug: "boss-approval",
|
|
964
|
+
fromNumberId: "pn_1",
|
|
965
|
+
toTemplate: "{{params.boss_phone}}",
|
|
966
|
+
timeoutMs: 600_000,
|
|
967
|
+
parameterMappings: {
|
|
968
|
+
discount: { mode: "parent_parameter", value: "requested_discount" },
|
|
969
|
+
reason: { mode: "literal", value: "broker requested a discount" },
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
parameterBindings: {
|
|
973
|
+
dealId: { parameter: "deal_id" },
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
args: { notes: "urgent" },
|
|
977
|
+
parameters: {
|
|
978
|
+
boss_phone: "+15550001111",
|
|
979
|
+
requested_discount: 12,
|
|
980
|
+
deal_id: "deal_1",
|
|
981
|
+
},
|
|
982
|
+
executeCloudVoiceAgent,
|
|
983
|
+
})).resolves.toMatchObject({
|
|
984
|
+
result: {
|
|
985
|
+
childSessionId: "child-session-1",
|
|
986
|
+
status: "completed",
|
|
987
|
+
outcome: "accept",
|
|
988
|
+
},
|
|
989
|
+
metadata: {},
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
expect(executeCloudVoiceAgent).toHaveBeenCalledWith(expect.objectContaining({
|
|
993
|
+
agentSlug: "boss-approval",
|
|
994
|
+
fromNumberId: "pn_1",
|
|
995
|
+
to: "+15550001111",
|
|
996
|
+
timeoutMs: 600_000,
|
|
997
|
+
intervalMs: 2_000,
|
|
998
|
+
parameters: {
|
|
999
|
+
notes: "urgent",
|
|
1000
|
+
dealId: "deal_1",
|
|
1001
|
+
discount: 12,
|
|
1002
|
+
reason: "broker requested a discount",
|
|
1003
|
+
},
|
|
1004
|
+
}));
|
|
1005
|
+
});
|
|
1006
|
+
});
|