@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.
Files changed (91) hide show
  1. package/README.md +13 -3
  2. package/bin/example-runner.ts +138 -11
  3. package/dist/bin/example-runner.d.ts +23 -6
  4. package/dist/bin/example-runner.js +112 -9
  5. package/dist/bin/example-runner.js.map +1 -1
  6. package/dist/examples/{example.test.js → agent-testing.test.js} +2 -2
  7. package/dist/examples/agent-testing.test.js.map +1 -0
  8. package/dist/examples/help-desk.js +22 -8
  9. package/dist/examples/help-desk.js.map +1 -1
  10. package/dist/examples/multiple-agents.d.ts +1 -0
  11. package/dist/examples/multiple-agents.js +52 -0
  12. package/dist/examples/multiple-agents.js.map +1 -0
  13. package/dist/examples/property-insurance.js +14 -3
  14. package/dist/examples/property-insurance.js.map +1 -1
  15. package/dist/examples/restaurant-waitlist.js +9 -1
  16. package/dist/examples/restaurant-waitlist.js.map +1 -1
  17. package/dist/examples/scheduling-outbound.js +5 -6
  18. package/dist/examples/scheduling-outbound.js.map +1 -1
  19. package/dist/src/action-item.d.ts +4 -4
  20. package/dist/src/action-item.js +1 -1
  21. package/dist/src/action-item.js.map +1 -1
  22. package/dist/src/agent.d.ts +31 -11
  23. package/dist/src/agent.js +278 -135
  24. package/dist/src/agent.js.map +1 -1
  25. package/dist/src/call.d.ts +12 -1
  26. package/dist/src/call.js +87 -24
  27. package/dist/src/call.js.map +1 -1
  28. package/dist/src/client.d.ts +3 -0
  29. package/dist/src/client.js +6 -0
  30. package/dist/src/client.js.map +1 -1
  31. package/dist/src/commands.d.ts +37 -9
  32. package/dist/src/commands.js +13 -3
  33. package/dist/src/commands.js.map +1 -1
  34. package/dist/src/events.d.ts +28 -1
  35. package/dist/src/events.js +10 -2
  36. package/dist/src/events.js.map +1 -1
  37. package/dist/src/example-utils.d.ts +2 -0
  38. package/dist/src/example-utils.js +115 -0
  39. package/dist/src/example-utils.js.map +1 -0
  40. package/dist/src/health.d.ts +35 -0
  41. package/dist/src/health.js +99 -0
  42. package/dist/src/health.js.map +1 -0
  43. package/dist/src/helpers/index.d.ts +1 -0
  44. package/dist/src/helpers/index.js +9 -0
  45. package/dist/src/helpers/index.js.map +1 -0
  46. package/dist/src/helpers/llm.d.ts +43 -1
  47. package/dist/src/helpers/llm.js +247 -0
  48. package/dist/src/helpers/llm.js.map +1 -1
  49. package/dist/src/helpers/server-rag.d.ts +25 -0
  50. package/dist/src/helpers/server-rag.js +119 -0
  51. package/dist/src/helpers/server-rag.js.map +1 -0
  52. package/dist/src/index.d.ts +3 -1
  53. package/dist/src/index.js +4 -1
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/runner.d.ts +10 -0
  56. package/dist/src/runner.js +101 -0
  57. package/dist/src/runner.js.map +1 -0
  58. package/dist/src/socket/protocol.d.ts +4 -4
  59. package/dist/src/testing/mocks.d.ts +6 -0
  60. package/dist/src/testing/mocks.js +12 -0
  61. package/dist/src/testing/mocks.js.map +1 -1
  62. package/dist/src/testing/session.d.ts +7 -0
  63. package/dist/src/testing/session.js +3 -22
  64. package/dist/src/testing/session.js.map +1 -1
  65. package/dist/src/version.d.ts +1 -1
  66. package/dist/src/version.js +1 -1
  67. package/examples/{example.test.ts → agent-testing.test.ts} +1 -1
  68. package/examples/help-desk.ts +21 -12
  69. package/examples/multiple-agents.ts +18 -0
  70. package/examples/property-insurance.ts +15 -3
  71. package/examples/restaurant-waitlist.ts +10 -1
  72. package/examples/scheduling-outbound.ts +5 -6
  73. package/package.json +9 -1
  74. package/src/action-item.ts +1 -1
  75. package/src/agent.ts +273 -147
  76. package/src/call.ts +115 -24
  77. package/src/client.ts +7 -0
  78. package/src/commands.ts +16 -2
  79. package/src/events.ts +11 -1
  80. package/src/example-utils.ts +87 -0
  81. package/src/health.ts +120 -0
  82. package/src/helpers/index.ts +2 -0
  83. package/src/helpers/llm.ts +170 -1
  84. package/src/helpers/server-rag.ts +132 -0
  85. package/src/index.ts +3 -1
  86. package/src/runner.ts +39 -0
  87. package/src/testing/mocks.ts +17 -0
  88. package/src/testing/session.ts +2 -22
  89. package/src/version.ts +1 -1
  90. package/dist/examples/example.test.js.map +0 -1
  91. /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 * as ListenInbound from "./socket/listen-inbound.ts";
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 { TestSession } from "./testing/session.ts";
31
- import { SessionStarted } from "./testing/protocol.ts";
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 { _generate } from "./helpers/llm.ts";
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 = { agent_number: string } | { webrtc_code: string };
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
- private _client: Client = new Client();
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?: { name?: string; organization?: string; purpose?: string }) {
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: (call: Call, intentSummary: string) => Promise<SuggestedAction | undefined>,
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
- return this._listenInbound({ agent_number: phoneNumber });
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
- return this._listenInbound({ webrtc_code: webrtcCode });
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
- this._listenInbound({ webrtc_code: webrtcCode }).catch((err) => {
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
- this._logger.info(`Task ${event.task_id} completed.`);
241
- if (this._onTaskCompleteGeneric !== undefined) {
242
- await this._onTaskCompleteGeneric(call, event.task_id);
243
- } else if (event.task_id in this._onTaskCompleteHandlers) {
244
- await this._onTaskCompleteHandlers[event.task_id](call);
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.warn(`No handler registered for completion of task '${event.task_id}'`);
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 suggestion: SuggestedAction | undefined;
361
+ let suggestions: SuggestedAction[] = [];
295
362
  if (this._onActionRequested !== undefined) {
296
- suggestion = await this._onActionRequested(call, event.intent_summary);
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
- action_key: suggestion?.key ?? null,
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(variables: Record<string, any> = {}): Promise<Call> {
338
- const call = new Call(variables);
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
- for await (const msg of socket) {
417
- switch (msg.message_type) {
418
- case "listen-started":
419
- if ("agent_number" in conn) {
420
- this._logger.info(
421
- "Listening on %s (%d other listeners).",
422
- conn.agent_number,
423
- msg.other_listeners,
424
- );
425
- } else {
426
- this._logger.info(
427
- "Listening on WebRTC code %s (%d other listeners).",
428
- conn.webrtc_code,
429
- msg.other_listeners,
430
- );
431
- const debugUrl = new URL(
432
- `debug-webrtc?webrtc_code=${conn.webrtc_code}`,
433
- this._client.getHttpBase(),
434
- );
435
- this._logger.info("Call your agent at: %s", debugUrl);
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
- } catch (e) {
452
- if (!(e instanceof GuavaSocketClosedError)) throw e;
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 | undefined,
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
- if (fromNumber) url.searchParams.set("from_number", fromNumber);
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
- await this._attachToCall(call_id, variables);
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, roleplaySchema);
607
- const action = JSON.parse(raw) as { action: string; utterance?: string };
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
- private async _serveCampaign(campaignCode: string): Promise<void> {
692
- const campaignUrl = new URL(`v1/campaigns/${campaignCode}`, this._client.getHttpBase());
693
- const campaignResponse = await fetchOrThrow(campaignUrl, {
694
- headers: await this._client.headers(),
695
- });
696
- const campaign = (await campaignResponse.json()) as { id: string; name: string };
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
- const wsUrl = new URL(`v1/serve-campaign/${campaign.id}`, this._client.getWebsocketBase());
699
- this._logger.info("Connecting to campaign '%s' (id: %s).", campaign.name, campaign.id);
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
- await using socket = await new GuavaSocket<DialerClientMessage, DialerServerMessage>(
702
- "serve-campaign",
703
- wsUrl.toString(),
704
- this._client,
705
- (msg) => msg as unknown as Record<string, unknown>,
706
- decodeDialerServerMessage,
707
- ).connect();
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
- const activeCalls: Promise<void>[] = [];
821
+ const activeCalls: Promise<void>[] = [];
710
822
 
711
- try {
712
- for await (const msg of socket) {
713
- switch (msg.message_type) {
714
- case "listen-started":
715
- this._logger.info("Listening for calls on campaign '%s'. Ready.", campaign.name);
716
- break;
717
- case "initiate-and-assign-call": {
718
- const { call_id, contact_data } = msg;
719
- const data = contact_data as Record<string, unknown> | null;
720
- const logPhone = (data?.phone_number as string | undefined) ?? "?";
721
- this._logger.info(
722
- "Ready to make call, id %s — initiating call setup and dispatch for contact %s.",
723
- call_id,
724
- logPhone,
725
- );
726
- activeCalls.push(
727
- (async () => {
728
- socket.send({ message_type: "controller-ready", call_id });
729
- const variables = (data?.data as Record<string, unknown>) ?? {};
730
- await this._attachToCall(call_id, variables, undefined, "v2/connect-campaign-call");
731
- })(),
732
- );
733
- break;
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
- await Promise.all(activeCalls);
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
- return this._serveCampaign(campaignCode);
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
  }