@guava-ai/guava-sdk 0.22.0 → 0.24.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/dist/examples/help-desk.js +22 -8
- package/dist/examples/help-desk.js.map +1 -1
- 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 +2 -4
- 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 +21 -7
- package/dist/src/agent.js +275 -123
- package/dist/src/agent.js.map +1 -1
- package/dist/src/call.d.ts +9 -1
- package/dist/src/call.js +24 -1
- package/dist/src/call.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 +31 -1
- package/dist/src/events.js +11 -2
- package/dist/src/events.js.map +1 -1
- 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 +33 -1
- package/dist/src/helpers/llm.js +220 -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/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/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/examples/help-desk.ts +21 -12
- package/examples/property-insurance.ts +15 -3
- package/examples/restaurant-waitlist.ts +10 -1
- package/examples/scheduling-outbound.ts +2 -4
- package/package.json +5 -1
- package/src/action-item.ts +1 -1
- package/src/agent.ts +271 -129
- package/src/call.ts +37 -1
- package/src/commands.ts +16 -2
- package/src/events.ts +12 -1
- package/src/health.ts +120 -0
- package/src/helpers/index.ts +2 -0
- package/src/helpers/llm.ts +178 -1
- package/src/helpers/server-rag.ts +132 -0
- package/src/testing/mocks.ts +17 -0
- package/src/version.ts +1 -1
package/src/agent.ts
CHANGED
|
@@ -1,36 +1,38 @@
|
|
|
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 { fetchOrThrow, getBaseUrl } from "./utils.ts";
|
|
35
|
+
import { runWebrtcHelper } from "./webrtc-helper.ts";
|
|
34
36
|
|
|
35
37
|
export type { CallInfo } from "./socket/call-info.ts";
|
|
36
38
|
|
|
@@ -41,13 +43,18 @@ export interface SuggestedAction {
|
|
|
41
43
|
description?: string;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
export type InboundConnection =
|
|
46
|
+
export type InboundConnection =
|
|
47
|
+
| { agent_number: string }
|
|
48
|
+
| { webrtc_code: string }
|
|
49
|
+
| { sip_code: string };
|
|
45
50
|
|
|
46
51
|
@telemetryClient.trackClass()
|
|
47
52
|
export class Agent {
|
|
48
53
|
private _name?: string;
|
|
49
54
|
private _organization?: string;
|
|
50
55
|
private _purpose?: string;
|
|
56
|
+
private _voice?: string;
|
|
57
|
+
private _acceptDtmfForNumbers: boolean;
|
|
51
58
|
private _logger: Logger;
|
|
52
59
|
|
|
53
60
|
private _client: Client = new Client();
|
|
@@ -68,16 +75,29 @@ export class Agent {
|
|
|
68
75
|
private _onActionRequested?: (
|
|
69
76
|
call: Call,
|
|
70
77
|
intentSummary: string,
|
|
71
|
-
) => Promise<SuggestedAction | undefined>;
|
|
78
|
+
) => Promise<SuggestedAction | SuggestedAction[] | null | undefined>;
|
|
72
79
|
private _onActionGeneric?: (call: Call, actionKey: string) => Promise<void>;
|
|
73
80
|
private _onActionHandlers: Record<string, (call: Call) => Promise<void>> = {};
|
|
81
|
+
private _onValidateHandlers: Record<
|
|
82
|
+
string,
|
|
83
|
+
(call: Call, value: unknown) => Promise<true | [false, string]>
|
|
84
|
+
> = {};
|
|
74
85
|
private _onSessionEnd?: (call: Call, event: BotSessionEnded) => Promise<void>;
|
|
86
|
+
private _onEscalate?: (call: Call, event: EscalateEvent) => Promise<void>;
|
|
75
87
|
private _onDtmf?: (call: Call, event: DTMFPressedEvent) => Promise<void>;
|
|
76
88
|
|
|
77
|
-
constructor(args?: {
|
|
89
|
+
constructor(args?: {
|
|
90
|
+
name?: string;
|
|
91
|
+
organization?: string;
|
|
92
|
+
purpose?: string;
|
|
93
|
+
voice?: string;
|
|
94
|
+
acceptDtmf?: boolean;
|
|
95
|
+
}) {
|
|
78
96
|
this._name = args?.name;
|
|
79
97
|
this._organization = args?.organization;
|
|
80
98
|
this._purpose = args?.purpose;
|
|
99
|
+
this._voice = args?.voice;
|
|
100
|
+
this._acceptDtmfForNumbers = args?.acceptDtmf ?? true;
|
|
81
101
|
this._logger = getDefaultLogger();
|
|
82
102
|
}
|
|
83
103
|
|
|
@@ -117,6 +137,13 @@ export class Agent {
|
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
139
|
|
|
140
|
+
onValidate(
|
|
141
|
+
fieldKey: string,
|
|
142
|
+
callback: (call: Call, value: unknown) => Promise<true | [false, string]>,
|
|
143
|
+
): void {
|
|
144
|
+
this._onValidateHandlers[fieldKey] = callback;
|
|
145
|
+
}
|
|
146
|
+
|
|
120
147
|
onSearchQuery(
|
|
121
148
|
fieldKey: string,
|
|
122
149
|
callback: (call: Call, query: string) => Promise<[string[], string[]]>,
|
|
@@ -125,7 +152,10 @@ export class Agent {
|
|
|
125
152
|
}
|
|
126
153
|
|
|
127
154
|
onActionRequest(
|
|
128
|
-
callback: (
|
|
155
|
+
callback: (
|
|
156
|
+
call: Call,
|
|
157
|
+
intentSummary: string,
|
|
158
|
+
) => Promise<SuggestedAction | SuggestedAction[] | null | undefined>,
|
|
129
159
|
): void {
|
|
130
160
|
this._onActionRequested = callback;
|
|
131
161
|
}
|
|
@@ -150,6 +180,10 @@ export class Agent {
|
|
|
150
180
|
this._onSessionEnd = callback;
|
|
151
181
|
}
|
|
152
182
|
|
|
183
|
+
onEscalate(callback: (call: Call, event: EscalateEvent) => Promise<void>): void {
|
|
184
|
+
this._onEscalate = callback;
|
|
185
|
+
}
|
|
186
|
+
|
|
153
187
|
onDtmf(callback: (call: Call, event: DTMFPressedEvent) => Promise<void>): void {
|
|
154
188
|
this._onDtmf = callback;
|
|
155
189
|
}
|
|
@@ -208,7 +242,9 @@ export class Agent {
|
|
|
208
242
|
}
|
|
209
243
|
|
|
210
244
|
async listenPhone(phoneNumber: string): Promise<void> {
|
|
211
|
-
|
|
245
|
+
const healthCtx = new HealthContext();
|
|
246
|
+
await using _server = await getHealthServer(healthCtx);
|
|
247
|
+
return this._listenInbound(healthCtx, { agent_number: phoneNumber });
|
|
212
248
|
}
|
|
213
249
|
|
|
214
250
|
async listenWebrtc(webrtcCode?: string): Promise<void> {
|
|
@@ -216,12 +252,21 @@ export class Agent {
|
|
|
216
252
|
this._logger.info("No WebRTC code provided. Creating a temporary one.");
|
|
217
253
|
webrtcCode = await this._client.createWebrtcAgent(3600);
|
|
218
254
|
}
|
|
219
|
-
|
|
255
|
+
const healthCtx = new HealthContext();
|
|
256
|
+
await using _server = await getHealthServer(healthCtx);
|
|
257
|
+
return this._listenInbound(healthCtx, { webrtc_code: webrtcCode });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async listenSip(sipCode: string): Promise<void> {
|
|
261
|
+
const healthCtx = new HealthContext();
|
|
262
|
+
await using _server = await getHealthServer(healthCtx);
|
|
263
|
+
return this._listenInbound(healthCtx, { sip_code: sipCode });
|
|
220
264
|
}
|
|
221
265
|
|
|
222
266
|
async callLocal(): Promise<void> {
|
|
223
267
|
const webrtcCode = await this._client.createWebrtcAgent(300);
|
|
224
|
-
|
|
268
|
+
// No health-server for call_local.
|
|
269
|
+
this._listenInbound(new HealthContext(), { webrtc_code: webrtcCode }).catch((err) => {
|
|
225
270
|
this._logger.error("Listen loop error: %s", err);
|
|
226
271
|
});
|
|
227
272
|
await runWebrtcHelper(webrtcCode, getBaseUrl());
|
|
@@ -237,13 +282,29 @@ export class Agent {
|
|
|
237
282
|
await this._onAgentSpeech(call, event);
|
|
238
283
|
}
|
|
239
284
|
} else if (event.event_type === "task-done") {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
285
|
+
const errors: string[] = [];
|
|
286
|
+
for (const fieldKey of call._fieldKeysByTaskId[event.task_id] ?? []) {
|
|
287
|
+
const handler = this._onValidateHandlers[fieldKey];
|
|
288
|
+
if (handler) {
|
|
289
|
+
const result = await handler(call, await call.getField(fieldKey));
|
|
290
|
+
if (result !== true) {
|
|
291
|
+
const [, error] = result;
|
|
292
|
+
errors.push(error);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (errors.length > 0) {
|
|
298
|
+
await call.retryTask(errors.join(" "));
|
|
245
299
|
} else {
|
|
246
|
-
this._logger.
|
|
300
|
+
this._logger.info(`Task ${event.task_id} completed.`);
|
|
301
|
+
if (this._onTaskCompleteGeneric !== undefined) {
|
|
302
|
+
await this._onTaskCompleteGeneric(call, event.task_id);
|
|
303
|
+
} else if (event.task_id in this._onTaskCompleteHandlers) {
|
|
304
|
+
await this._onTaskCompleteHandlers[event.task_id](call);
|
|
305
|
+
} else {
|
|
306
|
+
this._logger.warn(`No handler registered for completion of task '${event.task_id}'`);
|
|
307
|
+
}
|
|
247
308
|
}
|
|
248
309
|
} else if (event.event_type === "agent-question") {
|
|
249
310
|
if (this._onQuestion !== undefined) {
|
|
@@ -291,15 +352,19 @@ export class Agent {
|
|
|
291
352
|
}
|
|
292
353
|
} else if (event.event_type === "action-request") {
|
|
293
354
|
this._logger.info(`Received action request ${event.intent_id}: ${event.intent_summary}`);
|
|
294
|
-
let
|
|
355
|
+
let suggestions: SuggestedAction[] = [];
|
|
295
356
|
if (this._onActionRequested !== undefined) {
|
|
296
|
-
|
|
357
|
+
const result = await this._onActionRequested(call, event.intent_summary);
|
|
358
|
+
if (Array.isArray(result)) {
|
|
359
|
+
suggestions = result;
|
|
360
|
+
} else if (result !== null && result !== undefined) {
|
|
361
|
+
suggestions = [result];
|
|
362
|
+
}
|
|
297
363
|
}
|
|
298
364
|
await call.sendCommand(ActionSuggestionCommand, {
|
|
299
365
|
command_type: "action-suggestion",
|
|
300
366
|
intent_id: event.intent_id,
|
|
301
|
-
|
|
302
|
-
action_description: suggestion?.description ?? "",
|
|
367
|
+
actions: suggestions.map((s) => ({ key: s.key, description: s.description ?? "" })),
|
|
303
368
|
});
|
|
304
369
|
} else if (event.event_type === "execute-action") {
|
|
305
370
|
this._logger.info(`Executing action '${event.action_key}'`);
|
|
@@ -327,6 +392,18 @@ export class Agent {
|
|
|
327
392
|
if (this._onDtmf !== undefined) {
|
|
328
393
|
await this._onDtmf(call, event);
|
|
329
394
|
}
|
|
395
|
+
} else if (event.event_type === "escalate") {
|
|
396
|
+
if (this._onEscalate !== undefined) {
|
|
397
|
+
await this._onEscalate(call, event);
|
|
398
|
+
} else if (event.requested_by === "agent") {
|
|
399
|
+
await call.sendInstruction(
|
|
400
|
+
"No escalation target set. Apologize for not being able to help, ask them to try calling another time, and hang up the call immediately.",
|
|
401
|
+
);
|
|
402
|
+
} else {
|
|
403
|
+
await call.sendInstruction(
|
|
404
|
+
"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.",
|
|
405
|
+
);
|
|
406
|
+
}
|
|
330
407
|
} else if (event.event_type === "error") {
|
|
331
408
|
this._logger.error(`The Guava agent reported an error: ${event.content}`);
|
|
332
409
|
} else if (event.event_type === "warning") {
|
|
@@ -334,18 +411,25 @@ export class Agent {
|
|
|
334
411
|
}
|
|
335
412
|
}
|
|
336
413
|
|
|
337
|
-
async _initCall(
|
|
338
|
-
|
|
414
|
+
async _initCall(
|
|
415
|
+
callId: string,
|
|
416
|
+
callInfo: CallInfo,
|
|
417
|
+
variables: Record<string, any> = {},
|
|
418
|
+
): Promise<Call> {
|
|
419
|
+
const call = new Call(callId, callInfo, variables);
|
|
339
420
|
await call.setPersona({
|
|
340
421
|
agentName: this._name,
|
|
341
422
|
agentPurpose: this._purpose,
|
|
342
423
|
organizationName: this._organization,
|
|
424
|
+
voice: this._voice,
|
|
343
425
|
});
|
|
344
426
|
await call.sendCommand(RegisteredHooksCommand, {
|
|
345
427
|
command_type: "registered-hooks",
|
|
346
428
|
has_on_question: this._onQuestion !== undefined,
|
|
347
429
|
has_on_intent: false,
|
|
348
430
|
has_on_action_requested: this._onActionRequested !== undefined,
|
|
431
|
+
has_on_escalate: this._onEscalate !== undefined,
|
|
432
|
+
accept_dtmf_for_numbers: this._acceptDtmfForNumbers,
|
|
349
433
|
});
|
|
350
434
|
if (this._onCallStart !== undefined) {
|
|
351
435
|
await this._onCallStart(call);
|
|
@@ -355,11 +439,12 @@ export class Agent {
|
|
|
355
439
|
|
|
356
440
|
async _attachToCall(
|
|
357
441
|
callId: string,
|
|
442
|
+
callInfo: CallInfo,
|
|
358
443
|
initialVariables: Record<string, any> = {},
|
|
359
444
|
testSession?: TestSession,
|
|
360
445
|
route: string = "v2/connect-call",
|
|
361
446
|
): Promise<void> {
|
|
362
|
-
const call = await this._initCall(initialVariables);
|
|
447
|
+
const call = await this._initCall(callId, callInfo, initialVariables);
|
|
363
448
|
|
|
364
449
|
const url = new URL(`${route}/${callId}`, this._client.getWebsocketBase());
|
|
365
450
|
await using socket = await new GuavaSocket<Command, GuavaEvent | null>(
|
|
@@ -393,63 +478,76 @@ export class Agent {
|
|
|
393
478
|
}
|
|
394
479
|
}
|
|
395
480
|
|
|
396
|
-
async _listenInbound(conn: InboundConnection): Promise<void> {
|
|
481
|
+
async _listenInbound(healthCtx: HealthContext, conn: InboundConnection): Promise<void> {
|
|
397
482
|
const url = new URL("v2/listen-inbound", this._client.getWebsocketBase());
|
|
398
483
|
if ("agent_number" in conn) {
|
|
399
484
|
url.searchParams.set("phone_number", conn.agent_number);
|
|
400
|
-
} else {
|
|
485
|
+
} else if ("webrtc_code" in conn) {
|
|
401
486
|
url.searchParams.set("webrtc_code", conn.webrtc_code);
|
|
487
|
+
} else {
|
|
488
|
+
url.searchParams.set("sip_code", conn.sip_code);
|
|
402
489
|
}
|
|
403
490
|
|
|
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
491
|
try {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
this.
|
|
434
|
-
|
|
435
|
-
|
|
492
|
+
await using socket = await new GuavaSocket<
|
|
493
|
+
ListenInbound.ClientMessage,
|
|
494
|
+
ListenInbound.ServerMessage
|
|
495
|
+
>(
|
|
496
|
+
"listen-inbound",
|
|
497
|
+
url.toString(),
|
|
498
|
+
this._client,
|
|
499
|
+
(msg) => msg as unknown as Record<string, unknown>,
|
|
500
|
+
ListenInbound.decodeServerMessage,
|
|
501
|
+
).connect();
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
for await (const msg of socket) {
|
|
505
|
+
switch (msg.message_type) {
|
|
506
|
+
case "listen-started":
|
|
507
|
+
healthCtx.ready();
|
|
508
|
+
if ("agent_number" in conn) {
|
|
509
|
+
this._logger.info(
|
|
510
|
+
"Listening on %s (%d other listeners).",
|
|
511
|
+
conn.agent_number,
|
|
512
|
+
msg.other_listeners,
|
|
513
|
+
);
|
|
514
|
+
} else if ("webrtc_code" in conn) {
|
|
515
|
+
this._logger.info(
|
|
516
|
+
"Listening on WebRTC code %s (%d other listeners).",
|
|
517
|
+
conn.webrtc_code,
|
|
518
|
+
msg.other_listeners,
|
|
519
|
+
);
|
|
520
|
+
const debugUrl = new URL(
|
|
521
|
+
`debug-webrtc?webrtc_code=${conn.webrtc_code}`,
|
|
522
|
+
this._client.getHttpBase(),
|
|
523
|
+
);
|
|
524
|
+
this._logger.info("Call your agent at: %s", debugUrl);
|
|
525
|
+
} else {
|
|
526
|
+
this._logger.info(
|
|
527
|
+
"Listening on SIP code %s (%d other listeners).",
|
|
528
|
+
conn.sip_code,
|
|
529
|
+
msg.other_listeners,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
break;
|
|
533
|
+
case "incoming-call":
|
|
534
|
+
socket.send({ message_type: "claim-call", call_id: msg.call_id });
|
|
535
|
+
break;
|
|
536
|
+
case "assign-call": {
|
|
537
|
+
const { call_id, call_info } = msg;
|
|
538
|
+
this._logger.info("Received call (session ID: %s)", call_id);
|
|
539
|
+
this._handleAssignedCall(call_id, call_info, socket).catch((err) => {
|
|
540
|
+
this._logger.error("Error handling assigned call %s: %s", call_id, err);
|
|
541
|
+
});
|
|
542
|
+
break;
|
|
436
543
|
}
|
|
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
544
|
}
|
|
449
545
|
}
|
|
546
|
+
} catch (e) {
|
|
547
|
+
if (!(e instanceof GuavaSocketClosedError)) throw e;
|
|
450
548
|
}
|
|
451
|
-
}
|
|
452
|
-
|
|
549
|
+
} finally {
|
|
550
|
+
healthCtx.stopped();
|
|
453
551
|
}
|
|
454
552
|
}
|
|
455
553
|
|
|
@@ -466,7 +564,7 @@ export class Agent {
|
|
|
466
564
|
} else {
|
|
467
565
|
this._logger.info("Accepting call %s.", callId);
|
|
468
566
|
socket.send({ message_type: "answer-call", call_id: callId });
|
|
469
|
-
await this._attachToCall(callId, initialVariables);
|
|
567
|
+
await this._attachToCall(callId, callInfo, initialVariables);
|
|
470
568
|
}
|
|
471
569
|
}
|
|
472
570
|
|
|
@@ -489,7 +587,13 @@ export class Agent {
|
|
|
489
587
|
const { call_id } = (await response.json()) as { call_id: string };
|
|
490
588
|
|
|
491
589
|
this._logger.info("Outbound call created with session ID: %s", call_id);
|
|
492
|
-
|
|
590
|
+
const callInfo: CallInfo = {
|
|
591
|
+
call_type: "pstn",
|
|
592
|
+
from_number: fromNumber ?? null,
|
|
593
|
+
to_number: toNumber,
|
|
594
|
+
caller_id: null,
|
|
595
|
+
};
|
|
596
|
+
await this._attachToCall(call_id, callInfo, variables);
|
|
493
597
|
}
|
|
494
598
|
|
|
495
599
|
/**
|
|
@@ -529,8 +633,15 @@ export class Agent {
|
|
|
529
633
|
const sessionStarted = SessionStarted.parse(JSON.parse(rawFirst));
|
|
530
634
|
const testSession = new TestSession(ws, this._client);
|
|
531
635
|
|
|
636
|
+
const testCallInfo: CallInfo = {
|
|
637
|
+
call_type: "pstn",
|
|
638
|
+
from_number: null,
|
|
639
|
+
to_number: "+15555555555",
|
|
640
|
+
caller_id: null,
|
|
641
|
+
};
|
|
532
642
|
const attachPromise = this._attachToCall(
|
|
533
643
|
sessionStarted.session_id,
|
|
644
|
+
testCallInfo,
|
|
534
645
|
variables,
|
|
535
646
|
testSession,
|
|
536
647
|
).catch((err: unknown) => {
|
|
@@ -561,12 +672,12 @@ export class Agent {
|
|
|
561
672
|
* pass/fail criteria, or `session.getTranscript()` to inspect the conversation.
|
|
562
673
|
*
|
|
563
674
|
* @example
|
|
564
|
-
* const session = await agent.
|
|
675
|
+
* const session = await agent.roleplay(
|
|
565
676
|
* "You are a caller trying to buy a new table.",
|
|
566
677
|
* );
|
|
567
678
|
* assert(session.executedActions.includes("sales"));
|
|
568
679
|
*/
|
|
569
|
-
async
|
|
680
|
+
async roleplay(
|
|
570
681
|
roleplayPrompt: string,
|
|
571
682
|
variables: Record<string, any> = {},
|
|
572
683
|
): Promise<TestSession> {
|
|
@@ -632,6 +743,14 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
632
743
|
}, variables);
|
|
633
744
|
}
|
|
634
745
|
|
|
746
|
+
/** @deprecated Use {@link roleplay} instead. */
|
|
747
|
+
async testRoleplay(
|
|
748
|
+
roleplayPrompt: string,
|
|
749
|
+
variables: Record<string, any> = {},
|
|
750
|
+
): Promise<TestSession> {
|
|
751
|
+
return this.roleplay(roleplayPrompt, variables);
|
|
752
|
+
}
|
|
753
|
+
|
|
635
754
|
/**
|
|
636
755
|
* Start an interactive terminal chat session with the agent.
|
|
637
756
|
*
|
|
@@ -663,6 +782,8 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
663
782
|
name: this._name,
|
|
664
783
|
organization: this._organization,
|
|
665
784
|
purpose: this._purpose,
|
|
785
|
+
voice: this._voice,
|
|
786
|
+
acceptDtmf: this._acceptDtmfForNumbers,
|
|
666
787
|
});
|
|
667
788
|
cloned._onCallReceived = this._onCallReceived;
|
|
668
789
|
cloned._onCallStart = this._onCallStart;
|
|
@@ -672,66 +793,85 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
672
793
|
cloned._onTaskCompleteGeneric = this._onTaskCompleteGeneric;
|
|
673
794
|
cloned._onTaskCompleteHandlers = { ...this._onTaskCompleteHandlers };
|
|
674
795
|
cloned._searchQueryHandlers = { ...this._searchQueryHandlers };
|
|
796
|
+
cloned._onValidateHandlers = { ...this._onValidateHandlers };
|
|
675
797
|
cloned._onActionRequested = this._onActionRequested;
|
|
676
798
|
cloned._onActionGeneric = this._onActionGeneric;
|
|
677
799
|
cloned._onActionHandlers = { ...this._onActionHandlers };
|
|
678
800
|
cloned._onSessionEnd = this._onSessionEnd;
|
|
801
|
+
cloned._onEscalate = this._onEscalate;
|
|
679
802
|
cloned._onDtmf = this._onDtmf;
|
|
680
803
|
return cloned;
|
|
681
804
|
}
|
|
682
805
|
|
|
683
|
-
private async _serveCampaign(campaignCode: string): Promise<void> {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
806
|
+
private async _serveCampaign(healthCtx: HealthContext, campaignCode: string): Promise<void> {
|
|
807
|
+
try {
|
|
808
|
+
const campaignUrl = new URL(`v1/campaigns/${campaignCode}`, this._client.getHttpBase());
|
|
809
|
+
const campaignResponse = await fetchOrThrow(campaignUrl, {
|
|
810
|
+
headers: await this._client.headers(),
|
|
811
|
+
});
|
|
812
|
+
const campaign = (await campaignResponse.json()) as { id: string; name: string };
|
|
689
813
|
|
|
690
|
-
|
|
691
|
-
|
|
814
|
+
const wsUrl = new URL(`v1/serve-campaign/${campaign.id}`, this._client.getWebsocketBase());
|
|
815
|
+
this._logger.info("Connecting to campaign '%s' (id: %s).", campaign.name, campaign.id);
|
|
692
816
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
817
|
+
await using socket = await new GuavaSocket<DialerClientMessage, DialerServerMessage>(
|
|
818
|
+
"serve-campaign",
|
|
819
|
+
wsUrl.toString(),
|
|
820
|
+
this._client,
|
|
821
|
+
(msg) => msg as unknown as Record<string, unknown>,
|
|
822
|
+
decodeDialerServerMessage,
|
|
823
|
+
).connect();
|
|
700
824
|
|
|
701
|
-
|
|
825
|
+
const activeCalls: Promise<void>[] = [];
|
|
702
826
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
(
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
827
|
+
try {
|
|
828
|
+
for await (const msg of socket) {
|
|
829
|
+
switch (msg.message_type) {
|
|
830
|
+
case "listen-started":
|
|
831
|
+
this._logger.info("Listening for calls on campaign '%s'. Ready.", campaign.name);
|
|
832
|
+
healthCtx.ready();
|
|
833
|
+
break;
|
|
834
|
+
case "initiate-and-assign-call": {
|
|
835
|
+
const { call_id, contact_data } = msg;
|
|
836
|
+
const data = contact_data as Record<string, unknown> | null;
|
|
837
|
+
const logPhone = (data?.phone_number as string | undefined) ?? "?";
|
|
838
|
+
this._logger.info(
|
|
839
|
+
"Ready to make call, id %s — initiating call setup and dispatch for contact %s.",
|
|
840
|
+
call_id,
|
|
841
|
+
logPhone,
|
|
842
|
+
);
|
|
843
|
+
activeCalls.push(
|
|
844
|
+
(async () => {
|
|
845
|
+
socket.send({ message_type: "controller-ready", call_id });
|
|
846
|
+
const variables = (data?.data as Record<string, unknown>) ?? {};
|
|
847
|
+
const campaignCallInfo: CallInfo = {
|
|
848
|
+
call_type: "pstn",
|
|
849
|
+
from_number: null,
|
|
850
|
+
to_number: (data?.phone_number as string) ?? "",
|
|
851
|
+
caller_id: null,
|
|
852
|
+
};
|
|
853
|
+
await this._attachToCall(
|
|
854
|
+
call_id,
|
|
855
|
+
campaignCallInfo,
|
|
856
|
+
variables,
|
|
857
|
+
undefined,
|
|
858
|
+
"v2/connect-campaign-call",
|
|
859
|
+
);
|
|
860
|
+
})(),
|
|
861
|
+
);
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
726
864
|
}
|
|
727
865
|
}
|
|
866
|
+
} catch (e) {
|
|
867
|
+
if (!(e instanceof GuavaSocketClosedError)) throw e;
|
|
868
|
+
this._logger.info("Campaign '%s' disconnected.", campaign.name);
|
|
728
869
|
}
|
|
729
|
-
} catch (e) {
|
|
730
|
-
if (!(e instanceof GuavaSocketClosedError)) throw e;
|
|
731
|
-
this._logger.info("Campaign '%s' disconnected.", campaign.name);
|
|
732
|
-
}
|
|
733
870
|
|
|
734
|
-
|
|
871
|
+
await Promise.all(activeCalls);
|
|
872
|
+
} finally {
|
|
873
|
+
healthCtx.stopped();
|
|
874
|
+
}
|
|
735
875
|
}
|
|
736
876
|
|
|
737
877
|
/**
|
|
@@ -746,7 +886,9 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
746
886
|
* await agent.attachCampaign("gcmp-abc123");
|
|
747
887
|
*/
|
|
748
888
|
async attachCampaign(campaignCode: string): Promise<void> {
|
|
749
|
-
|
|
889
|
+
const healthCtx = new HealthContext();
|
|
890
|
+
await using _server = await getHealthServer(healthCtx);
|
|
891
|
+
return this._serveCampaign(healthCtx, campaignCode);
|
|
750
892
|
}
|
|
751
893
|
|
|
752
894
|
/* ===== Aliases to be removed at some point. ===== */
|