@guava-ai/guava-sdk 0.23.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 +18 -6
- package/dist/src/agent.js +269 -121
- 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 +28 -1
- package/dist/src/events.js +10 -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 +261 -127
- package/src/call.ts +37 -1
- package/src/commands.ts +16 -2
- package/src/events.ts +11 -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) => {
|
|
@@ -671,6 +782,8 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
671
782
|
name: this._name,
|
|
672
783
|
organization: this._organization,
|
|
673
784
|
purpose: this._purpose,
|
|
785
|
+
voice: this._voice,
|
|
786
|
+
acceptDtmf: this._acceptDtmfForNumbers,
|
|
674
787
|
});
|
|
675
788
|
cloned._onCallReceived = this._onCallReceived;
|
|
676
789
|
cloned._onCallStart = this._onCallStart;
|
|
@@ -680,66 +793,85 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
680
793
|
cloned._onTaskCompleteGeneric = this._onTaskCompleteGeneric;
|
|
681
794
|
cloned._onTaskCompleteHandlers = { ...this._onTaskCompleteHandlers };
|
|
682
795
|
cloned._searchQueryHandlers = { ...this._searchQueryHandlers };
|
|
796
|
+
cloned._onValidateHandlers = { ...this._onValidateHandlers };
|
|
683
797
|
cloned._onActionRequested = this._onActionRequested;
|
|
684
798
|
cloned._onActionGeneric = this._onActionGeneric;
|
|
685
799
|
cloned._onActionHandlers = { ...this._onActionHandlers };
|
|
686
800
|
cloned._onSessionEnd = this._onSessionEnd;
|
|
801
|
+
cloned._onEscalate = this._onEscalate;
|
|
687
802
|
cloned._onDtmf = this._onDtmf;
|
|
688
803
|
return cloned;
|
|
689
804
|
}
|
|
690
805
|
|
|
691
|
-
private async _serveCampaign(campaignCode: string): Promise<void> {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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 };
|
|
697
813
|
|
|
698
|
-
|
|
699
|
-
|
|
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);
|
|
700
816
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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();
|
|
708
824
|
|
|
709
|
-
|
|
825
|
+
const activeCalls: Promise<void>[] = [];
|
|
710
826
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
(
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
+
}
|
|
734
864
|
}
|
|
735
865
|
}
|
|
866
|
+
} catch (e) {
|
|
867
|
+
if (!(e instanceof GuavaSocketClosedError)) throw e;
|
|
868
|
+
this._logger.info("Campaign '%s' disconnected.", campaign.name);
|
|
736
869
|
}
|
|
737
|
-
} catch (e) {
|
|
738
|
-
if (!(e instanceof GuavaSocketClosedError)) throw e;
|
|
739
|
-
this._logger.info("Campaign '%s' disconnected.", campaign.name);
|
|
740
|
-
}
|
|
741
870
|
|
|
742
|
-
|
|
871
|
+
await Promise.all(activeCalls);
|
|
872
|
+
} finally {
|
|
873
|
+
healthCtx.stopped();
|
|
874
|
+
}
|
|
743
875
|
}
|
|
744
876
|
|
|
745
877
|
/**
|
|
@@ -754,7 +886,9 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
754
886
|
* await agent.attachCampaign("gcmp-abc123");
|
|
755
887
|
*/
|
|
756
888
|
async attachCampaign(campaignCode: string): Promise<void> {
|
|
757
|
-
|
|
889
|
+
const healthCtx = new HealthContext();
|
|
890
|
+
await using _server = await getHealthServer(healthCtx);
|
|
891
|
+
return this._serveCampaign(healthCtx, campaignCode);
|
|
758
892
|
}
|
|
759
893
|
|
|
760
894
|
/* ===== Aliases to be removed at some point. ===== */
|