@guava-ai/guava-sdk 0.23.0 → 0.25.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/README.md +13 -3
- package/bin/example-runner.ts +138 -11
- package/dist/bin/example-runner.d.ts +23 -6
- package/dist/bin/example-runner.js +112 -9
- package/dist/bin/example-runner.js.map +1 -1
- package/dist/examples/{example.test.js → agent-testing.test.js} +2 -2
- package/dist/examples/agent-testing.test.js.map +1 -0
- package/dist/examples/help-desk.js +22 -8
- package/dist/examples/help-desk.js.map +1 -1
- package/dist/examples/multiple-agents.d.ts +1 -0
- package/dist/examples/multiple-agents.js +52 -0
- package/dist/examples/multiple-agents.js.map +1 -0
- package/dist/examples/property-insurance.js +14 -3
- package/dist/examples/property-insurance.js.map +1 -1
- package/dist/examples/restaurant-waitlist.js +9 -1
- package/dist/examples/restaurant-waitlist.js.map +1 -1
- package/dist/examples/scheduling-outbound.js +5 -6
- package/dist/examples/scheduling-outbound.js.map +1 -1
- package/dist/src/action-item.d.ts +4 -4
- package/dist/src/action-item.js +1 -1
- package/dist/src/action-item.js.map +1 -1
- package/dist/src/agent.d.ts +31 -11
- package/dist/src/agent.js +278 -135
- package/dist/src/agent.js.map +1 -1
- package/dist/src/call.d.ts +12 -1
- package/dist/src/call.js +87 -24
- package/dist/src/call.js.map +1 -1
- package/dist/src/client.d.ts +3 -0
- package/dist/src/client.js +6 -0
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands.d.ts +37 -9
- package/dist/src/commands.js +13 -3
- package/dist/src/commands.js.map +1 -1
- package/dist/src/events.d.ts +28 -1
- package/dist/src/events.js +10 -2
- package/dist/src/events.js.map +1 -1
- package/dist/src/example-utils.d.ts +2 -0
- package/dist/src/example-utils.js +115 -0
- package/dist/src/example-utils.js.map +1 -0
- package/dist/src/health.d.ts +35 -0
- package/dist/src/health.js +99 -0
- package/dist/src/health.js.map +1 -0
- package/dist/src/helpers/index.d.ts +1 -0
- package/dist/src/helpers/index.js +9 -0
- package/dist/src/helpers/index.js.map +1 -0
- package/dist/src/helpers/llm.d.ts +43 -1
- package/dist/src/helpers/llm.js +247 -0
- package/dist/src/helpers/llm.js.map +1 -1
- package/dist/src/helpers/server-rag.d.ts +25 -0
- package/dist/src/helpers/server-rag.js +119 -0
- package/dist/src/helpers/server-rag.js.map +1 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/runner.d.ts +10 -0
- package/dist/src/runner.js +101 -0
- package/dist/src/runner.js.map +1 -0
- package/dist/src/socket/protocol.d.ts +4 -4
- package/dist/src/testing/mocks.d.ts +6 -0
- package/dist/src/testing/mocks.js +12 -0
- package/dist/src/testing/mocks.js.map +1 -1
- package/dist/src/testing/session.d.ts +7 -0
- package/dist/src/testing/session.js +3 -22
- package/dist/src/testing/session.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/examples/{example.test.ts → agent-testing.test.ts} +1 -1
- package/examples/help-desk.ts +21 -12
- package/examples/multiple-agents.ts +18 -0
- package/examples/property-insurance.ts +15 -3
- package/examples/restaurant-waitlist.ts +10 -1
- package/examples/scheduling-outbound.ts +5 -6
- package/package.json +9 -1
- package/src/action-item.ts +1 -1
- package/src/agent.ts +273 -147
- package/src/call.ts +115 -24
- package/src/client.ts +7 -0
- package/src/commands.ts +16 -2
- package/src/events.ts +11 -1
- package/src/example-utils.ts +87 -0
- package/src/health.ts +120 -0
- package/src/helpers/index.ts +2 -0
- package/src/helpers/llm.ts +170 -1
- package/src/helpers/server-rag.ts +132 -0
- package/src/index.ts +3 -1
- package/src/runner.ts +39 -0
- package/src/testing/mocks.ts +17 -0
- package/src/testing/session.ts +2 -22
- package/src/version.ts +1 -1
- package/dist/examples/example.test.js.map +0 -1
- /package/dist/examples/{example.test.d.ts → agent-testing.test.d.ts} +0 -0
package/src/agent.ts
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
-
import { Call, Client } from "./index.ts";
|
|
3
|
-
import { getDefaultLogger, type Logger } from "./logging.ts";
|
|
4
|
-
import { getBaseUrl, fetchOrThrow } from "./utils.ts";
|
|
5
|
-
import { runWebrtcHelper } from "./webrtc-helper.ts";
|
|
6
2
|
import {
|
|
3
|
+
ActionSuggestionCommand,
|
|
7
4
|
AnswerQuestionCommand,
|
|
8
5
|
ChoiceResultCommand,
|
|
9
|
-
RegisteredHooksCommand,
|
|
10
|
-
ActionSuggestionCommand,
|
|
11
6
|
type Command,
|
|
7
|
+
RegisteredHooksCommand,
|
|
12
8
|
} from "./commands.ts";
|
|
13
9
|
import {
|
|
14
|
-
type GuavaEvent,
|
|
15
|
-
type CallerSpeechEvent,
|
|
16
10
|
type AgentSpeechEvent,
|
|
17
11
|
type BotSessionEnded,
|
|
12
|
+
type CallerSpeechEvent,
|
|
18
13
|
type DTMFPressedEvent,
|
|
14
|
+
type EscalateEvent,
|
|
15
|
+
type GuavaEvent,
|
|
19
16
|
decodeEventDict,
|
|
20
17
|
} from "./events.ts";
|
|
21
|
-
import { telemetryClient } from "./telemetry.ts";
|
|
22
|
-
import { GuavaSocket, GuavaSocketClosedError } from "./socket/client.ts";
|
|
23
18
|
import {
|
|
24
19
|
type ClientMessage as DialerClientMessage,
|
|
25
20
|
type ServerMessage as DialerServerMessage,
|
|
26
21
|
decodeServerMessage as decodeDialerServerMessage,
|
|
27
22
|
} from "./guavadialer-events.ts";
|
|
28
|
-
import
|
|
23
|
+
import { HealthContext, getHealthServer } from "./health.ts";
|
|
24
|
+
import { _generate } from "./helpers/llm.ts";
|
|
25
|
+
import { Call, Client } from "./index.ts";
|
|
26
|
+
import { type Logger, getDefaultLogger } from "./logging.ts";
|
|
29
27
|
import type { CallInfo } from "./socket/call-info.ts";
|
|
30
|
-
import {
|
|
31
|
-
import
|
|
28
|
+
import { GuavaSocket, GuavaSocketClosedError } from "./socket/client.ts";
|
|
29
|
+
import * as ListenInbound from "./socket/listen-inbound.ts";
|
|
30
|
+
import { telemetryClient } from "./telemetry.ts";
|
|
32
31
|
import { runChat } from "./testing/chat.ts";
|
|
33
|
-
import {
|
|
32
|
+
import { SessionStarted } from "./testing/protocol.ts";
|
|
33
|
+
import { TestSession } from "./testing/session.ts";
|
|
34
|
+
import * as z from "zod";
|
|
35
|
+
import { fetchOrThrow, getBaseUrl } from "./utils.ts";
|
|
36
|
+
import { runWebrtcHelper } from "./webrtc-helper.ts";
|
|
37
|
+
|
|
38
|
+
export const _roleplayActionSchema = z.object({
|
|
39
|
+
action: z.enum(["speak", "hangup"]),
|
|
40
|
+
utterance: z.string().optional(),
|
|
41
|
+
});
|
|
34
42
|
|
|
35
43
|
export type { CallInfo } from "./socket/call-info.ts";
|
|
36
44
|
|
|
@@ -41,16 +49,21 @@ export interface SuggestedAction {
|
|
|
41
49
|
description?: string;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
export type InboundConnection =
|
|
52
|
+
export type InboundConnection =
|
|
53
|
+
| { agent_number: string }
|
|
54
|
+
| { webrtc_code: string }
|
|
55
|
+
| { sip_code: string };
|
|
45
56
|
|
|
46
57
|
@telemetryClient.trackClass()
|
|
47
58
|
export class Agent {
|
|
48
59
|
private _name?: string;
|
|
49
60
|
private _organization?: string;
|
|
50
61
|
private _purpose?: string;
|
|
62
|
+
private _voice?: string;
|
|
63
|
+
private _acceptDtmfForNumbers: boolean;
|
|
51
64
|
private _logger: Logger;
|
|
52
65
|
|
|
53
|
-
|
|
66
|
+
_client: Client = new Client();
|
|
54
67
|
|
|
55
68
|
private _onCallReceived: (callInfo: CallInfo) => Promise<IncomingCallAction> = async () => ({
|
|
56
69
|
action: "accept",
|
|
@@ -68,16 +81,29 @@ export class Agent {
|
|
|
68
81
|
private _onActionRequested?: (
|
|
69
82
|
call: Call,
|
|
70
83
|
intentSummary: string,
|
|
71
|
-
) => Promise<SuggestedAction | undefined>;
|
|
84
|
+
) => Promise<SuggestedAction | SuggestedAction[] | null | undefined>;
|
|
72
85
|
private _onActionGeneric?: (call: Call, actionKey: string) => Promise<void>;
|
|
73
86
|
private _onActionHandlers: Record<string, (call: Call) => Promise<void>> = {};
|
|
87
|
+
private _onValidateHandlers: Record<
|
|
88
|
+
string,
|
|
89
|
+
(call: Call, value: unknown) => Promise<true | [false, string]>
|
|
90
|
+
> = {};
|
|
74
91
|
private _onSessionEnd?: (call: Call, event: BotSessionEnded) => Promise<void>;
|
|
92
|
+
private _onEscalate?: (call: Call, event: EscalateEvent) => Promise<void>;
|
|
75
93
|
private _onDtmf?: (call: Call, event: DTMFPressedEvent) => Promise<void>;
|
|
76
94
|
|
|
77
|
-
constructor(args?: {
|
|
95
|
+
constructor(args?: {
|
|
96
|
+
name?: string;
|
|
97
|
+
organization?: string;
|
|
98
|
+
purpose?: string;
|
|
99
|
+
voice?: string;
|
|
100
|
+
acceptDtmf?: boolean;
|
|
101
|
+
}) {
|
|
78
102
|
this._name = args?.name;
|
|
79
103
|
this._organization = args?.organization;
|
|
80
104
|
this._purpose = args?.purpose;
|
|
105
|
+
this._voice = args?.voice;
|
|
106
|
+
this._acceptDtmfForNumbers = args?.acceptDtmf ?? true;
|
|
81
107
|
this._logger = getDefaultLogger();
|
|
82
108
|
}
|
|
83
109
|
|
|
@@ -117,6 +143,13 @@ export class Agent {
|
|
|
117
143
|
}
|
|
118
144
|
}
|
|
119
145
|
|
|
146
|
+
onValidate(
|
|
147
|
+
fieldKey: string,
|
|
148
|
+
callback: (call: Call, value: unknown) => Promise<true | [false, string]>,
|
|
149
|
+
): void {
|
|
150
|
+
this._onValidateHandlers[fieldKey] = callback;
|
|
151
|
+
}
|
|
152
|
+
|
|
120
153
|
onSearchQuery(
|
|
121
154
|
fieldKey: string,
|
|
122
155
|
callback: (call: Call, query: string) => Promise<[string[], string[]]>,
|
|
@@ -125,7 +158,10 @@ export class Agent {
|
|
|
125
158
|
}
|
|
126
159
|
|
|
127
160
|
onActionRequest(
|
|
128
|
-
callback: (
|
|
161
|
+
callback: (
|
|
162
|
+
call: Call,
|
|
163
|
+
intentSummary: string,
|
|
164
|
+
) => Promise<SuggestedAction | SuggestedAction[] | null | undefined>,
|
|
129
165
|
): void {
|
|
130
166
|
this._onActionRequested = callback;
|
|
131
167
|
}
|
|
@@ -150,6 +186,10 @@ export class Agent {
|
|
|
150
186
|
this._onSessionEnd = callback;
|
|
151
187
|
}
|
|
152
188
|
|
|
189
|
+
onEscalate(callback: (call: Call, event: EscalateEvent) => Promise<void>): void {
|
|
190
|
+
this._onEscalate = callback;
|
|
191
|
+
}
|
|
192
|
+
|
|
153
193
|
onDtmf(callback: (call: Call, event: DTMFPressedEvent) => Promise<void>): void {
|
|
154
194
|
this._onDtmf = callback;
|
|
155
195
|
}
|
|
@@ -208,7 +248,9 @@ export class Agent {
|
|
|
208
248
|
}
|
|
209
249
|
|
|
210
250
|
async listenPhone(phoneNumber: string): Promise<void> {
|
|
211
|
-
|
|
251
|
+
const healthCtx = new HealthContext();
|
|
252
|
+
await using _server = await getHealthServer(healthCtx);
|
|
253
|
+
return this._listenInbound(healthCtx, { agent_number: phoneNumber });
|
|
212
254
|
}
|
|
213
255
|
|
|
214
256
|
async listenWebrtc(webrtcCode?: string): Promise<void> {
|
|
@@ -216,12 +258,21 @@ export class Agent {
|
|
|
216
258
|
this._logger.info("No WebRTC code provided. Creating a temporary one.");
|
|
217
259
|
webrtcCode = await this._client.createWebrtcAgent(3600);
|
|
218
260
|
}
|
|
219
|
-
|
|
261
|
+
const healthCtx = new HealthContext();
|
|
262
|
+
await using _server = await getHealthServer(healthCtx);
|
|
263
|
+
return this._listenInbound(healthCtx, { webrtc_code: webrtcCode });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async listenSip(sipCode: string): Promise<void> {
|
|
267
|
+
const healthCtx = new HealthContext();
|
|
268
|
+
await using _server = await getHealthServer(healthCtx);
|
|
269
|
+
return this._listenInbound(healthCtx, { sip_code: sipCode });
|
|
220
270
|
}
|
|
221
271
|
|
|
222
272
|
async callLocal(): Promise<void> {
|
|
223
273
|
const webrtcCode = await this._client.createWebrtcAgent(300);
|
|
224
|
-
|
|
274
|
+
// No health-server for call_local.
|
|
275
|
+
this._listenInbound(new HealthContext(), { webrtc_code: webrtcCode }).catch((err) => {
|
|
225
276
|
this._logger.error("Listen loop error: %s", err);
|
|
226
277
|
});
|
|
227
278
|
await runWebrtcHelper(webrtcCode, getBaseUrl());
|
|
@@ -237,13 +288,29 @@ export class Agent {
|
|
|
237
288
|
await this._onAgentSpeech(call, event);
|
|
238
289
|
}
|
|
239
290
|
} else if (event.event_type === "task-done") {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
291
|
+
const errors: string[] = [];
|
|
292
|
+
for (const fieldKey of call._fieldKeysByTaskId[event.task_id] ?? []) {
|
|
293
|
+
const handler = this._onValidateHandlers[fieldKey];
|
|
294
|
+
if (handler) {
|
|
295
|
+
const result = await handler(call, await call.getField(fieldKey));
|
|
296
|
+
if (result !== true) {
|
|
297
|
+
const [, error] = result;
|
|
298
|
+
errors.push(error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (errors.length > 0) {
|
|
304
|
+
await call.retryTask(errors.join(" "));
|
|
245
305
|
} else {
|
|
246
|
-
this._logger.
|
|
306
|
+
this._logger.info(`Task ${event.task_id} completed.`);
|
|
307
|
+
if (this._onTaskCompleteGeneric !== undefined) {
|
|
308
|
+
await this._onTaskCompleteGeneric(call, event.task_id);
|
|
309
|
+
} else if (event.task_id in this._onTaskCompleteHandlers) {
|
|
310
|
+
await this._onTaskCompleteHandlers[event.task_id](call);
|
|
311
|
+
} else {
|
|
312
|
+
this._logger.warn(`No handler registered for completion of task '${event.task_id}'`);
|
|
313
|
+
}
|
|
247
314
|
}
|
|
248
315
|
} else if (event.event_type === "agent-question") {
|
|
249
316
|
if (this._onQuestion !== undefined) {
|
|
@@ -291,15 +358,19 @@ export class Agent {
|
|
|
291
358
|
}
|
|
292
359
|
} else if (event.event_type === "action-request") {
|
|
293
360
|
this._logger.info(`Received action request ${event.intent_id}: ${event.intent_summary}`);
|
|
294
|
-
let
|
|
361
|
+
let suggestions: SuggestedAction[] = [];
|
|
295
362
|
if (this._onActionRequested !== undefined) {
|
|
296
|
-
|
|
363
|
+
const result = await this._onActionRequested(call, event.intent_summary);
|
|
364
|
+
if (Array.isArray(result)) {
|
|
365
|
+
suggestions = result;
|
|
366
|
+
} else if (result !== null && result !== undefined) {
|
|
367
|
+
suggestions = [result];
|
|
368
|
+
}
|
|
297
369
|
}
|
|
298
370
|
await call.sendCommand(ActionSuggestionCommand, {
|
|
299
371
|
command_type: "action-suggestion",
|
|
300
372
|
intent_id: event.intent_id,
|
|
301
|
-
|
|
302
|
-
action_description: suggestion?.description ?? "",
|
|
373
|
+
actions: suggestions.map((s) => ({ key: s.key, description: s.description ?? "" })),
|
|
303
374
|
});
|
|
304
375
|
} else if (event.event_type === "execute-action") {
|
|
305
376
|
this._logger.info(`Executing action '${event.action_key}'`);
|
|
@@ -327,6 +398,18 @@ export class Agent {
|
|
|
327
398
|
if (this._onDtmf !== undefined) {
|
|
328
399
|
await this._onDtmf(call, event);
|
|
329
400
|
}
|
|
401
|
+
} else if (event.event_type === "escalate") {
|
|
402
|
+
if (this._onEscalate !== undefined) {
|
|
403
|
+
await this._onEscalate(call, event);
|
|
404
|
+
} else if (event.requested_by === "agent") {
|
|
405
|
+
await call.sendInstruction(
|
|
406
|
+
"No escalation target set. Apologize for not being able to help, ask them to try calling another time, and hang up the call immediately.",
|
|
407
|
+
);
|
|
408
|
+
} else {
|
|
409
|
+
await call.sendInstruction(
|
|
410
|
+
"Let them know there are no representatives available to take their call. Ask them if they would prefer to continue or to call another time.",
|
|
411
|
+
);
|
|
412
|
+
}
|
|
330
413
|
} else if (event.event_type === "error") {
|
|
331
414
|
this._logger.error(`The Guava agent reported an error: ${event.content}`);
|
|
332
415
|
} else if (event.event_type === "warning") {
|
|
@@ -334,18 +417,25 @@ export class Agent {
|
|
|
334
417
|
}
|
|
335
418
|
}
|
|
336
419
|
|
|
337
|
-
async _initCall(
|
|
338
|
-
|
|
420
|
+
async _initCall(
|
|
421
|
+
callId: string,
|
|
422
|
+
callInfo: CallInfo,
|
|
423
|
+
variables: Record<string, any> = {},
|
|
424
|
+
): Promise<Call> {
|
|
425
|
+
const call = new Call(callId, callInfo, variables);
|
|
339
426
|
await call.setPersona({
|
|
340
427
|
agentName: this._name,
|
|
341
428
|
agentPurpose: this._purpose,
|
|
342
429
|
organizationName: this._organization,
|
|
430
|
+
voice: this._voice,
|
|
343
431
|
});
|
|
344
432
|
await call.sendCommand(RegisteredHooksCommand, {
|
|
345
433
|
command_type: "registered-hooks",
|
|
346
434
|
has_on_question: this._onQuestion !== undefined,
|
|
347
435
|
has_on_intent: false,
|
|
348
436
|
has_on_action_requested: this._onActionRequested !== undefined,
|
|
437
|
+
has_on_escalate: this._onEscalate !== undefined,
|
|
438
|
+
accept_dtmf_for_numbers: this._acceptDtmfForNumbers,
|
|
349
439
|
});
|
|
350
440
|
if (this._onCallStart !== undefined) {
|
|
351
441
|
await this._onCallStart(call);
|
|
@@ -355,11 +445,12 @@ export class Agent {
|
|
|
355
445
|
|
|
356
446
|
async _attachToCall(
|
|
357
447
|
callId: string,
|
|
448
|
+
callInfo: CallInfo,
|
|
358
449
|
initialVariables: Record<string, any> = {},
|
|
359
450
|
testSession?: TestSession,
|
|
360
451
|
route: string = "v2/connect-call",
|
|
361
452
|
): Promise<void> {
|
|
362
|
-
const call = await this._initCall(initialVariables);
|
|
453
|
+
const call = await this._initCall(callId, callInfo, initialVariables);
|
|
363
454
|
|
|
364
455
|
const url = new URL(`${route}/${callId}`, this._client.getWebsocketBase());
|
|
365
456
|
await using socket = await new GuavaSocket<Command, GuavaEvent | null>(
|
|
@@ -393,63 +484,76 @@ export class Agent {
|
|
|
393
484
|
}
|
|
394
485
|
}
|
|
395
486
|
|
|
396
|
-
async _listenInbound(conn: InboundConnection): Promise<void> {
|
|
487
|
+
async _listenInbound(healthCtx: HealthContext, conn: InboundConnection): Promise<void> {
|
|
397
488
|
const url = new URL("v2/listen-inbound", this._client.getWebsocketBase());
|
|
398
489
|
if ("agent_number" in conn) {
|
|
399
490
|
url.searchParams.set("phone_number", conn.agent_number);
|
|
400
|
-
} else {
|
|
491
|
+
} else if ("webrtc_code" in conn) {
|
|
401
492
|
url.searchParams.set("webrtc_code", conn.webrtc_code);
|
|
493
|
+
} else {
|
|
494
|
+
url.searchParams.set("sip_code", conn.sip_code);
|
|
402
495
|
}
|
|
403
496
|
|
|
404
|
-
await using socket = await new GuavaSocket<
|
|
405
|
-
ListenInbound.ClientMessage,
|
|
406
|
-
ListenInbound.ServerMessage
|
|
407
|
-
>(
|
|
408
|
-
"listen-inbound",
|
|
409
|
-
url.toString(),
|
|
410
|
-
this._client,
|
|
411
|
-
(msg) => msg as unknown as Record<string, unknown>,
|
|
412
|
-
ListenInbound.decodeServerMessage,
|
|
413
|
-
).connect();
|
|
414
|
-
|
|
415
497
|
try {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
this.
|
|
434
|
-
|
|
435
|
-
|
|
498
|
+
await using socket = await new GuavaSocket<
|
|
499
|
+
ListenInbound.ClientMessage,
|
|
500
|
+
ListenInbound.ServerMessage
|
|
501
|
+
>(
|
|
502
|
+
"listen-inbound",
|
|
503
|
+
url.toString(),
|
|
504
|
+
this._client,
|
|
505
|
+
(msg) => msg as unknown as Record<string, unknown>,
|
|
506
|
+
ListenInbound.decodeServerMessage,
|
|
507
|
+
).connect();
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
for await (const msg of socket) {
|
|
511
|
+
switch (msg.message_type) {
|
|
512
|
+
case "listen-started":
|
|
513
|
+
healthCtx.ready();
|
|
514
|
+
if ("agent_number" in conn) {
|
|
515
|
+
this._logger.info(
|
|
516
|
+
"Listening on %s (%d other listeners).",
|
|
517
|
+
conn.agent_number,
|
|
518
|
+
msg.other_listeners,
|
|
519
|
+
);
|
|
520
|
+
} else if ("webrtc_code" in conn) {
|
|
521
|
+
this._logger.info(
|
|
522
|
+
"Listening on WebRTC code %s (%d other listeners).",
|
|
523
|
+
conn.webrtc_code,
|
|
524
|
+
msg.other_listeners,
|
|
525
|
+
);
|
|
526
|
+
const debugUrl = new URL(
|
|
527
|
+
`debug-webrtc?webrtc_code=${conn.webrtc_code}`,
|
|
528
|
+
this._client.getHttpBase(),
|
|
529
|
+
);
|
|
530
|
+
this._logger.info("Call your agent at: %s", debugUrl);
|
|
531
|
+
} else {
|
|
532
|
+
this._logger.info(
|
|
533
|
+
"Listening on SIP code %s (%d other listeners).",
|
|
534
|
+
conn.sip_code,
|
|
535
|
+
msg.other_listeners,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
case "incoming-call":
|
|
540
|
+
socket.send({ message_type: "claim-call", call_id: msg.call_id });
|
|
541
|
+
break;
|
|
542
|
+
case "assign-call": {
|
|
543
|
+
const { call_id, call_info } = msg;
|
|
544
|
+
this._logger.info("Received call (session ID: %s)", call_id);
|
|
545
|
+
this._handleAssignedCall(call_id, call_info, socket).catch((err) => {
|
|
546
|
+
this._logger.error("Error handling assigned call %s: %s", call_id, err);
|
|
547
|
+
});
|
|
548
|
+
break;
|
|
436
549
|
}
|
|
437
|
-
break;
|
|
438
|
-
case "incoming-call":
|
|
439
|
-
socket.send({ message_type: "claim-call", call_id: msg.call_id });
|
|
440
|
-
break;
|
|
441
|
-
case "assign-call": {
|
|
442
|
-
const { call_id, call_info } = msg;
|
|
443
|
-
this._logger.info("Received call (session ID: %s)", call_id);
|
|
444
|
-
this._handleAssignedCall(call_id, call_info, socket).catch((err) => {
|
|
445
|
-
this._logger.error("Error handling assigned call %s: %s", call_id, err);
|
|
446
|
-
});
|
|
447
|
-
break;
|
|
448
550
|
}
|
|
449
551
|
}
|
|
552
|
+
} catch (e) {
|
|
553
|
+
if (!(e instanceof GuavaSocketClosedError)) throw e;
|
|
450
554
|
}
|
|
451
|
-
}
|
|
452
|
-
|
|
555
|
+
} finally {
|
|
556
|
+
healthCtx.stopped();
|
|
453
557
|
}
|
|
454
558
|
}
|
|
455
559
|
|
|
@@ -466,7 +570,7 @@ export class Agent {
|
|
|
466
570
|
} else {
|
|
467
571
|
this._logger.info("Accepting call %s.", callId);
|
|
468
572
|
socket.send({ message_type: "answer-call", call_id: callId });
|
|
469
|
-
await this._attachToCall(callId, initialVariables);
|
|
573
|
+
await this._attachToCall(callId, callInfo, initialVariables);
|
|
470
574
|
}
|
|
471
575
|
}
|
|
472
576
|
|
|
@@ -474,12 +578,12 @@ export class Agent {
|
|
|
474
578
|
* @description use the Guava API to call out to a number
|
|
475
579
|
*/
|
|
476
580
|
async callPhone(
|
|
477
|
-
fromNumber: string
|
|
581
|
+
fromNumber: string,
|
|
478
582
|
toNumber: string,
|
|
479
583
|
variables: Record<string, any> = {},
|
|
480
584
|
): Promise<void> {
|
|
481
585
|
const url = new URL("v2/create-outbound", this._client.getHttpBase());
|
|
482
|
-
|
|
586
|
+
url.searchParams.set("from_number", fromNumber);
|
|
483
587
|
url.searchParams.set("to_number", toNumber);
|
|
484
588
|
|
|
485
589
|
const response = await fetchOrThrow(url, {
|
|
@@ -489,7 +593,13 @@ export class Agent {
|
|
|
489
593
|
const { call_id } = (await response.json()) as { call_id: string };
|
|
490
594
|
|
|
491
595
|
this._logger.info("Outbound call created with session ID: %s", call_id);
|
|
492
|
-
|
|
596
|
+
const callInfo: CallInfo = {
|
|
597
|
+
call_type: "pstn",
|
|
598
|
+
from_number: fromNumber ?? null,
|
|
599
|
+
to_number: toNumber,
|
|
600
|
+
caller_id: null,
|
|
601
|
+
};
|
|
602
|
+
await this._attachToCall(call_id, callInfo, variables);
|
|
493
603
|
}
|
|
494
604
|
|
|
495
605
|
/**
|
|
@@ -529,8 +639,15 @@ export class Agent {
|
|
|
529
639
|
const sessionStarted = SessionStarted.parse(JSON.parse(rawFirst));
|
|
530
640
|
const testSession = new TestSession(ws, this._client);
|
|
531
641
|
|
|
642
|
+
const testCallInfo: CallInfo = {
|
|
643
|
+
call_type: "pstn",
|
|
644
|
+
from_number: null,
|
|
645
|
+
to_number: "+15555555555",
|
|
646
|
+
caller_id: null,
|
|
647
|
+
};
|
|
532
648
|
const attachPromise = this._attachToCall(
|
|
533
649
|
sessionStarted.session_id,
|
|
650
|
+
testCallInfo,
|
|
534
651
|
variables,
|
|
535
652
|
testSession,
|
|
536
653
|
).catch((err: unknown) => {
|
|
@@ -570,16 +687,6 @@ export class Agent {
|
|
|
570
687
|
roleplayPrompt: string,
|
|
571
688
|
variables: Record<string, any> = {},
|
|
572
689
|
): Promise<TestSession> {
|
|
573
|
-
const roleplaySchema = {
|
|
574
|
-
type: "object",
|
|
575
|
-
properties: {
|
|
576
|
-
action: { type: "string", enum: ["speak", "hangup"] },
|
|
577
|
-
utterance: { type: "string" },
|
|
578
|
-
},
|
|
579
|
-
required: ["action"],
|
|
580
|
-
additionalProperties: false,
|
|
581
|
-
};
|
|
582
|
-
|
|
583
690
|
return this.test(async (session) => {
|
|
584
691
|
let snapshotLen = 0;
|
|
585
692
|
try {
|
|
@@ -603,8 +710,8 @@ ${transcript || "(The agent has not spoken yet)"}
|
|
|
603
710
|
|
|
604
711
|
Choose "speak" and provide your next utterance, or choose "hangup" if the conversation has naturally concluded.`;
|
|
605
712
|
|
|
606
|
-
const raw = await _generate(this._client, prompt,
|
|
607
|
-
const action = JSON.parse(raw)
|
|
713
|
+
const raw = await _generate(this._client, prompt, z.toJSONSchema(_roleplayActionSchema));
|
|
714
|
+
const action = _roleplayActionSchema.parse(JSON.parse(raw));
|
|
608
715
|
|
|
609
716
|
if (action.action === "hangup") {
|
|
610
717
|
this._logger.info("(Roleplay Session) [caller hangs up]");
|
|
@@ -671,6 +778,8 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
671
778
|
name: this._name,
|
|
672
779
|
organization: this._organization,
|
|
673
780
|
purpose: this._purpose,
|
|
781
|
+
voice: this._voice,
|
|
782
|
+
acceptDtmf: this._acceptDtmfForNumbers,
|
|
674
783
|
});
|
|
675
784
|
cloned._onCallReceived = this._onCallReceived;
|
|
676
785
|
cloned._onCallStart = this._onCallStart;
|
|
@@ -680,66 +789,85 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
680
789
|
cloned._onTaskCompleteGeneric = this._onTaskCompleteGeneric;
|
|
681
790
|
cloned._onTaskCompleteHandlers = { ...this._onTaskCompleteHandlers };
|
|
682
791
|
cloned._searchQueryHandlers = { ...this._searchQueryHandlers };
|
|
792
|
+
cloned._onValidateHandlers = { ...this._onValidateHandlers };
|
|
683
793
|
cloned._onActionRequested = this._onActionRequested;
|
|
684
794
|
cloned._onActionGeneric = this._onActionGeneric;
|
|
685
795
|
cloned._onActionHandlers = { ...this._onActionHandlers };
|
|
686
796
|
cloned._onSessionEnd = this._onSessionEnd;
|
|
797
|
+
cloned._onEscalate = this._onEscalate;
|
|
687
798
|
cloned._onDtmf = this._onDtmf;
|
|
688
799
|
return cloned;
|
|
689
800
|
}
|
|
690
801
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
802
|
+
async _serveCampaign(healthCtx: HealthContext, campaignCode: string): Promise<void> {
|
|
803
|
+
try {
|
|
804
|
+
const campaignUrl = new URL(`v1/campaigns/${campaignCode}`, this._client.getHttpBase());
|
|
805
|
+
const campaignResponse = await fetchOrThrow(campaignUrl, {
|
|
806
|
+
headers: await this._client.headers(),
|
|
807
|
+
});
|
|
808
|
+
const campaign = (await campaignResponse.json()) as { id: string; name: string };
|
|
697
809
|
|
|
698
|
-
|
|
699
|
-
|
|
810
|
+
const wsUrl = new URL(`v1/serve-campaign/${campaign.id}`, this._client.getWebsocketBase());
|
|
811
|
+
this._logger.info("Connecting to campaign '%s' (id: %s).", campaign.name, campaign.id);
|
|
700
812
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
813
|
+
await using socket = await new GuavaSocket<DialerClientMessage, DialerServerMessage>(
|
|
814
|
+
"serve-campaign",
|
|
815
|
+
wsUrl.toString(),
|
|
816
|
+
this._client,
|
|
817
|
+
(msg) => msg as unknown as Record<string, unknown>,
|
|
818
|
+
decodeDialerServerMessage,
|
|
819
|
+
).connect();
|
|
708
820
|
|
|
709
|
-
|
|
821
|
+
const activeCalls: Promise<void>[] = [];
|
|
710
822
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
(
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
823
|
+
try {
|
|
824
|
+
for await (const msg of socket) {
|
|
825
|
+
switch (msg.message_type) {
|
|
826
|
+
case "listen-started":
|
|
827
|
+
this._logger.info("Listening for calls on campaign '%s'. Ready.", campaign.name);
|
|
828
|
+
healthCtx.ready();
|
|
829
|
+
break;
|
|
830
|
+
case "initiate-and-assign-call": {
|
|
831
|
+
const { call_id, contact_data } = msg;
|
|
832
|
+
const data = contact_data as Record<string, unknown> | null;
|
|
833
|
+
const logPhone = (data?.phone_number as string | undefined) ?? "?";
|
|
834
|
+
this._logger.info(
|
|
835
|
+
"Ready to make call, id %s — initiating call setup and dispatch for contact %s.",
|
|
836
|
+
call_id,
|
|
837
|
+
logPhone,
|
|
838
|
+
);
|
|
839
|
+
activeCalls.push(
|
|
840
|
+
(async () => {
|
|
841
|
+
socket.send({ message_type: "controller-ready", call_id });
|
|
842
|
+
const variables = (data?.data as Record<string, unknown>) ?? {};
|
|
843
|
+
const campaignCallInfo: CallInfo = {
|
|
844
|
+
call_type: "pstn",
|
|
845
|
+
from_number: null,
|
|
846
|
+
to_number: (data?.phone_number as string) ?? "",
|
|
847
|
+
caller_id: null,
|
|
848
|
+
};
|
|
849
|
+
await this._attachToCall(
|
|
850
|
+
call_id,
|
|
851
|
+
campaignCallInfo,
|
|
852
|
+
variables,
|
|
853
|
+
undefined,
|
|
854
|
+
"v2/connect-campaign-call",
|
|
855
|
+
);
|
|
856
|
+
})(),
|
|
857
|
+
);
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
734
860
|
}
|
|
735
861
|
}
|
|
862
|
+
} catch (e) {
|
|
863
|
+
if (!(e instanceof GuavaSocketClosedError)) throw e;
|
|
864
|
+
this._logger.info("Campaign '%s' disconnected.", campaign.name);
|
|
736
865
|
}
|
|
737
|
-
} catch (e) {
|
|
738
|
-
if (!(e instanceof GuavaSocketClosedError)) throw e;
|
|
739
|
-
this._logger.info("Campaign '%s' disconnected.", campaign.name);
|
|
740
|
-
}
|
|
741
866
|
|
|
742
|
-
|
|
867
|
+
await Promise.all(activeCalls);
|
|
868
|
+
} finally {
|
|
869
|
+
healthCtx.stopped();
|
|
870
|
+
}
|
|
743
871
|
}
|
|
744
872
|
|
|
745
873
|
/**
|
|
@@ -754,7 +882,9 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
754
882
|
* await agent.attachCampaign("gcmp-abc123");
|
|
755
883
|
*/
|
|
756
884
|
async attachCampaign(campaignCode: string): Promise<void> {
|
|
757
|
-
|
|
885
|
+
const healthCtx = new HealthContext();
|
|
886
|
+
await using _server = await getHealthServer(healthCtx);
|
|
887
|
+
return this._serveCampaign(healthCtx, campaignCode);
|
|
758
888
|
}
|
|
759
889
|
|
|
760
890
|
/* ===== Aliases to be removed at some point. ===== */
|
|
@@ -764,11 +894,7 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
764
894
|
}
|
|
765
895
|
|
|
766
896
|
/** @deprecated Use {@link callPhone} instead. */
|
|
767
|
-
async outboundPhone(
|
|
768
|
-
fromNumber: string | undefined,
|
|
769
|
-
toNumber: string,
|
|
770
|
-
variables: Record<string, any> = {},
|
|
771
|
-
) {
|
|
897
|
+
async outboundPhone(fromNumber: string, toNumber: string, variables: Record<string, any> = {}) {
|
|
772
898
|
return this.callPhone(fromNumber, toNumber, variables);
|
|
773
899
|
}
|
|
774
900
|
}
|