@guava-ai/guava-sdk 0.26.0 → 0.27.1

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 (67) hide show
  1. package/README.md +1 -1
  2. package/bin/example-runner.ts +67 -26
  3. package/dist/bin/example-runner.d.ts +1 -25
  4. package/dist/bin/example-runner.js +64 -24
  5. package/dist/bin/example-runner.js.map +1 -1
  6. package/dist/examples/help-desk.d.ts +1 -1
  7. package/dist/examples/help-desk.js +34 -23
  8. package/dist/examples/help-desk.js.map +1 -1
  9. package/dist/examples/multiple-agents.d.ts +1 -1
  10. package/dist/examples/multiple-agents.js +1 -1
  11. package/dist/examples/multiple-agents.js.map +1 -1
  12. package/dist/examples/polling-campaign.d.ts +1 -1
  13. package/dist/examples/polling-campaign.js +9 -7
  14. package/dist/examples/polling-campaign.js.map +1 -1
  15. package/dist/examples/property-insurance.d.ts +1 -1
  16. package/dist/examples/property-insurance.js +34 -23
  17. package/dist/examples/property-insurance.js.map +1 -1
  18. package/dist/examples/restaurant-waitlist.d.ts +1 -1
  19. package/dist/examples/restaurant-waitlist.js +34 -23
  20. package/dist/examples/restaurant-waitlist.js.map +1 -1
  21. package/dist/examples/scheduling-outbound.d.ts +1 -1
  22. package/dist/examples/scheduling-outbound.js +24 -14
  23. package/dist/examples/scheduling-outbound.js.map +1 -1
  24. package/dist/src/agent.d.ts +2 -2
  25. package/dist/src/agent.js +4 -4
  26. package/dist/src/agent.js.map +1 -1
  27. package/dist/src/call.d.ts +2 -0
  28. package/dist/src/call.js +18 -0
  29. package/dist/src/call.js.map +1 -1
  30. package/dist/src/client.d.ts +0 -22
  31. package/dist/src/client.js +0 -155
  32. package/dist/src/client.js.map +1 -1
  33. package/dist/src/commands.d.ts +62 -0
  34. package/dist/src/commands.js +7 -1
  35. package/dist/src/commands.js.map +1 -1
  36. package/dist/src/events.d.ts +2 -1
  37. package/dist/src/events.js +20 -2
  38. package/dist/src/events.js.map +1 -1
  39. package/dist/src/index.d.ts +0 -1
  40. package/dist/src/index.js +1 -3
  41. package/dist/src/index.js.map +1 -1
  42. package/dist/src/socket/client.js +10 -0
  43. package/dist/src/socket/client.js.map +1 -1
  44. package/dist/src/telemetry.js +1 -1
  45. package/dist/src/telemetry.js.map +1 -1
  46. package/dist/src/version.d.ts +1 -1
  47. package/dist/src/version.js +1 -1
  48. package/examples/help-desk.ts +42 -20
  49. package/examples/multiple-agents.ts +1 -1
  50. package/examples/polling-campaign.ts +12 -7
  51. package/examples/property-insurance.ts +42 -22
  52. package/examples/restaurant-waitlist.ts +42 -22
  53. package/examples/scheduling-outbound.ts +26 -15
  54. package/package.json +2 -1
  55. package/src/agent.ts +16 -8
  56. package/src/call.ts +25 -0
  57. package/src/client.ts +0 -197
  58. package/src/commands.ts +8 -0
  59. package/src/events.ts +20 -18
  60. package/src/index.ts +0 -1
  61. package/src/socket/client.ts +22 -0
  62. package/src/telemetry.ts +1 -1
  63. package/src/version.ts +1 -1
  64. package/dist/src/call-controller.d.ts +0 -137
  65. package/dist/src/call-controller.js +0 -436
  66. package/dist/src/call-controller.js.map +0 -1
  67. package/src/call-controller.ts +0 -451
@@ -1,7 +1,9 @@
1
+ import { Command } from "commander";
1
2
  import * as guava from "@guava-ai/guava-sdk";
2
3
  import { DocumentQA } from "@guava-ai/guava-sdk/helpers";
3
4
  import { IntentRecognizer } from "@guava-ai/guava-sdk/helpers";
4
5
  import { FURNITURE_RETAILER_QA } from "@guava-ai/guava-sdk/example-data";
6
+ import { getAgentNumber } from "@guava-ai/guava-sdk/example-utils";
5
7
 
6
8
  export const agent = new guava.Agent({
7
9
  name: "Nova",
@@ -57,24 +59,44 @@ agent.onAction("other", async (call: guava.Call) => {
57
59
  );
58
60
  });
59
61
 
60
- export async function run(args: string[]) {
61
- if (args.includes("--webrtc")) {
62
- await agent.listenWebrtc();
63
- } else if (args.includes("--phone")) {
64
- agent.listenPhone(process.env.GUAVA_AGENT_NUMBER!);
65
- } else if (args.includes("--sip")) {
66
- const sipCode = args[args.indexOf("--sip") + 1];
67
- if (!sipCode) {
68
- console.error("Error: --sip requires a SIP code argument.");
69
- process.exit(1);
70
- }
71
- await agent.listenSip(sipCode);
72
- } else if (args.includes("--local")) {
73
- await agent.callLocal();
74
- } else if (args.includes("--chat")) {
75
- await agent.chat();
76
- } else {
77
- console.error("Usage: guava-example help-desk --phone | --webrtc | --sip | --local | --chat");
78
- process.exit(1);
79
- }
62
+ export async function run(prog: string, args: string[]) {
63
+ const program = new Command().name(prog).showHelpAfterError();
64
+
65
+ // Every Agent can be attached to one of many different channels.
66
+ program
67
+ .command("phone [number]")
68
+ .description("Listen for phone calls.")
69
+ .action(async (number?: string) => {
70
+ await agent.listenPhone(number ?? (await getAgentNumber()));
71
+ });
72
+
73
+ program
74
+ .command("webrtc [code]")
75
+ .description("Listen on a WebRTC code.")
76
+ .action(async (code?: string) => {
77
+ await agent.listenWebrtc(code);
78
+ });
79
+
80
+ program
81
+ .command("sip <code>")
82
+ .description("Listen on a SIP code 'guavasip-...'.")
83
+ .action(async (code: string) => {
84
+ await agent.listenSip(code);
85
+ });
86
+
87
+ program
88
+ .command("local")
89
+ .description("Start a local call.")
90
+ .action(async () => {
91
+ await agent.callLocal();
92
+ });
93
+
94
+ program
95
+ .command("chat")
96
+ .description("Start a local chat session (for testing).")
97
+ .action(async () => {
98
+ await agent.chat();
99
+ });
100
+
101
+ await program.parseAsync(args, { from: "user" });
80
102
  }
@@ -10,7 +10,7 @@ const agentB = new guava.Agent({
10
10
  purpose: "You are a helpful voice agent.",
11
11
  });
12
12
 
13
- export async function run(_args: string[]) {
13
+ export async function run(_prog: string, _args: string[]) {
14
14
  const runner = new guava.Runner();
15
15
  runner.listenPhone(agentA, process.env.GUAVA_AGENT_NUMBER!);
16
16
  runner.listenWebrtc(agentB);
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * Usage: guava-example polling-campaign <campaign-code>
9
9
  */
10
+ import { Command } from "commander";
10
11
  import * as guava from "@guava-ai/guava-sdk";
11
12
 
12
13
  const agent = new guava.Agent({
@@ -67,13 +68,17 @@ agent.onTaskComplete("political_poll", async (call) => {
67
68
  );
68
69
  });
69
70
 
70
- export async function run(args: string[]) {
71
- const [campaignCode] = args;
71
+ export async function run(prog: string, args: string[]) {
72
+ const program = new Command().name(prog).showHelpAfterError();
72
73
 
73
- if (!campaignCode) {
74
- console.error("Usage: guava-example polling-campaign <campaign-code>");
75
- process.exit(1);
76
- }
74
+ program
75
+ .argument(
76
+ "<campaign_code>",
77
+ "The campaign code to attach to (e.g. gcp-...). Use the CLI or dashboard to create a campaign.",
78
+ )
79
+ .action(async (campaignCode: string) => {
80
+ await agent.attachCampaign(campaignCode);
81
+ });
77
82
 
78
- await agent.attachCampaign(campaignCode);
83
+ await program.parseAsync(args, { from: "user" });
79
84
  }
@@ -1,6 +1,8 @@
1
+ import { Command } from "commander";
1
2
  import * as guava from "@guava-ai/guava-sdk";
2
3
  import { DocumentQA } from "@guava-ai/guava-sdk/helpers";
3
4
  import { PROPERTY_INSURANCE_POLICY } from "@guava-ai/guava-sdk/example-data";
5
+ import { getAgentNumber } from "@guava-ai/guava-sdk/example-utils";
4
6
 
5
7
  const agent = new guava.Agent({
6
8
  organization: "Harper Valley Property Insurance",
@@ -21,26 +23,44 @@ agent.onQuestion(async (call: guava.Call, question: string) => {
21
23
  return await documentQA.ask(question);
22
24
  });
23
25
 
24
- export async function run(args: string[]) {
25
- if (args.includes("--webrtc")) {
26
- await agent.listenWebrtc();
27
- } else if (args.includes("--phone")) {
28
- agent.listenPhone(process.env.GUAVA_AGENT_NUMBER!);
29
- } else if (args.includes("--sip")) {
30
- const sipCode = args[args.indexOf("--sip") + 1];
31
- if (!sipCode) {
32
- console.error("Error: --sip requires a SIP code argument.");
33
- process.exit(1);
34
- }
35
- await agent.listenSip(sipCode);
36
- } else if (args.includes("--local")) {
37
- await agent.callLocal();
38
- } else if (args.includes("--chat")) {
39
- await agent.chat();
40
- } else {
41
- console.error(
42
- "Usage: guava-example property-insurance --phone | --webrtc | --sip | --local | --chat",
43
- );
44
- process.exit(1);
45
- }
26
+ export async function run(prog: string, args: string[]) {
27
+ const program = new Command().name(prog).showHelpAfterError();
28
+
29
+ // Every Agent can be attached to one of many different channels.
30
+ program
31
+ .command("phone [number]")
32
+ .description("Listen for phone calls.")
33
+ .action(async (number?: string) => {
34
+ await agent.listenPhone(number ?? (await getAgentNumber()));
35
+ });
36
+
37
+ program
38
+ .command("webrtc [code]")
39
+ .description("Listen on a WebRTC code.")
40
+ .action(async (code?: string) => {
41
+ await agent.listenWebrtc(code);
42
+ });
43
+
44
+ program
45
+ .command("sip <code>")
46
+ .description("Listen on a SIP code 'guavasip-...'.")
47
+ .action(async (code: string) => {
48
+ await agent.listenSip(code);
49
+ });
50
+
51
+ program
52
+ .command("local")
53
+ .description("Start a local call.")
54
+ .action(async () => {
55
+ await agent.callLocal();
56
+ });
57
+
58
+ program
59
+ .command("chat")
60
+ .description("Start an interactive terminal chat.")
61
+ .action(async () => {
62
+ await agent.chat();
63
+ });
64
+
65
+ await program.parseAsync(args, { from: "user" });
46
66
  }
@@ -1,5 +1,7 @@
1
+ import { Command } from "commander";
1
2
  import * as guava from "@guava-ai/guava-sdk";
2
3
  import { getDefaultLogger } from "@guava-ai/guava-sdk";
4
+ import { getAgentNumber } from "@guava-ai/guava-sdk/example-utils";
3
5
 
4
6
  const logger = getDefaultLogger();
5
7
 
@@ -42,26 +44,44 @@ agent.onTaskComplete("waitlist", async (call: guava.Call) => {
42
44
  await call.hangup("Thank the caller and let them know we'll text when their table is ready.");
43
45
  });
44
46
 
45
- export async function run(args: string[]) {
46
- if (args.includes("--webrtc")) {
47
- await agent.listenWebrtc();
48
- } else if (args.includes("--phone")) {
49
- agent.listenPhone(process.env.GUAVA_AGENT_NUMBER!);
50
- } else if (args.includes("--sip")) {
51
- const sipCode = args[args.indexOf("--sip") + 1];
52
- if (!sipCode) {
53
- console.error("Error: --sip requires a SIP code argument.");
54
- process.exit(1);
55
- }
56
- await agent.listenSip(sipCode);
57
- } else if (args.includes("--local")) {
58
- await agent.callLocal();
59
- } else if (args.includes("--chat")) {
60
- await agent.chat();
61
- } else {
62
- console.error(
63
- "Usage: guava-example restaurant-waitlist --phone | --webrtc | --sip | --local | --chat",
64
- );
65
- process.exit(1);
66
- }
47
+ export async function run(prog: string, args: string[]) {
48
+ const program = new Command().name(prog).showHelpAfterError();
49
+
50
+ // Every Agent can be attached to one of many different channels.
51
+ program
52
+ .command("phone [number]")
53
+ .description("Listen for phone calls.")
54
+ .action(async (number?: string) => {
55
+ await agent.listenPhone(number ?? (await getAgentNumber()));
56
+ });
57
+
58
+ program
59
+ .command("webrtc [code]")
60
+ .description("Listen on a WebRTC code.")
61
+ .action(async (code?: string) => {
62
+ await agent.listenWebrtc(code);
63
+ });
64
+
65
+ program
66
+ .command("sip <code>")
67
+ .description("Listen on a SIP code 'guavasip-...'.")
68
+ .action(async (code: string) => {
69
+ await agent.listenSip(code);
70
+ });
71
+
72
+ program
73
+ .command("local")
74
+ .description("Start a local call.")
75
+ .action(async () => {
76
+ await agent.callLocal();
77
+ });
78
+
79
+ program
80
+ .command("chat")
81
+ .description("Start an interactive terminal chat.")
82
+ .action(async () => {
83
+ await agent.chat();
84
+ });
85
+
86
+ await program.parseAsync(args, { from: "user" });
67
87
  }
@@ -1,3 +1,4 @@
1
+ import { Command } from "commander";
1
2
  import * as guava from "@guava-ai/guava-sdk";
2
3
  import { DatetimeFilter } from "@guava-ai/guava-sdk/helpers";
3
4
  import { mockAppointmentsForFuture } from "@guava-ai/guava-sdk/example-data";
@@ -42,22 +43,32 @@ agent.onTaskComplete("schedule_appointment", async (call) => {
42
43
  await call.hangup("Thank them for their time and hang up the call.");
43
44
  });
44
45
 
45
- export async function run(args: string[]) {
46
- if (args.includes("--chat")) {
47
- const patientName = args[args.indexOf("--chat") + 1] ?? "Benjamin Buttons";
48
- await agent.chat({ patientName });
49
- return;
50
- }
46
+ export async function run(prog: string, args: string[]) {
47
+ const program = new Command().name(prog).showHelpAfterError();
51
48
 
52
- const [toNumber, patientName = "Benjamin Buttons"] = args;
49
+ program
50
+ .command("phone <number>")
51
+ .description("Call a phone number.")
52
+ .option("-n, --name <name>", "Name of the patient.", "Benjamin Buttons")
53
+ .action(async (number: string, options: { name: string }) => {
54
+ await agent.callPhone(await getAgentNumber(), number, { patientName: options.name });
55
+ });
53
56
 
54
- if (!toNumber) {
55
- console.error("Usage: guava-example scheduling-outbound <phone> [name]");
56
- console.error(" guava-example scheduling-outbound --chat [name]");
57
- process.exit(1);
58
- }
57
+ program
58
+ .command("local")
59
+ .description("Start a local call (for testing).")
60
+ .option("-n, --name <name>", "Name of the patient.", "Benjamin Buttons")
61
+ .action(async (options: { name: string }) => {
62
+ await agent.callLocal({ patientName: options.name });
63
+ });
64
+
65
+ program
66
+ .command("chat")
67
+ .description("Start a local chat session (for testing).")
68
+ .option("-n, --name <name>", "Name of the patient.", "Benjamin Buttons")
69
+ .action(async (options: { name: string }) => {
70
+ await agent.chat({ patientName: options.name });
71
+ });
59
72
 
60
- agent.callPhone(await getAgentNumber(), toNumber, {
61
- patientName: patientName,
62
- });
73
+ await program.parseAsync(args, { from: "user" });
63
74
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@guava-ai/guava-sdk",
3
3
  "description": "The official TypeScript SDK for building Guava voice agents.",
4
4
  "homepage": "https://docs.goguava.ai/typescript-sdk/",
5
- "version": "0.26.0",
5
+ "version": "0.27.1",
6
6
  "type": "commonjs",
7
7
  "main": "dist/src/index.js",
8
8
  "types": "dist/src/index.d.ts",
@@ -46,6 +46,7 @@
46
46
  "guava-example": "dist/bin/example-runner.js"
47
47
  },
48
48
  "dependencies": {
49
+ "commander": "^15.0.0",
49
50
  "openai": "^6.16.0",
50
51
  "ws": "^8.19.0",
51
52
  "zod": "^4.3.5"
package/src/agent.ts CHANGED
@@ -276,12 +276,14 @@ export class Agent {
276
276
  return this._listenInbound(healthCtx, { sip_code: sipCode });
277
277
  }
278
278
 
279
- async callLocal(): Promise<void> {
279
+ async callLocal(variables: Record<string, any> = {}): Promise<void> {
280
280
  const webrtcCode = await this._client.createWebrtcAgent(300);
281
281
  // No health-server for call_local.
282
- this._listenInbound(new HealthContext(), { webrtc_code: webrtcCode }).catch((err) => {
283
- this._logger.error("Listen loop error: %s", err);
284
- });
282
+ this._listenInbound(new HealthContext(), { webrtc_code: webrtcCode }, variables).catch(
283
+ (err) => {
284
+ this._logger.error("Listen loop error: %s", err);
285
+ },
286
+ );
285
287
  await runWebrtcHelper(webrtcCode, getBaseUrl());
286
288
  }
287
289
 
@@ -574,7 +576,11 @@ export class Agent {
574
576
  }
575
577
  }
576
578
 
577
- async _listenInbound(healthCtx: HealthContext, conn: InboundConnection): Promise<void> {
579
+ async _listenInbound(
580
+ healthCtx: HealthContext,
581
+ conn: InboundConnection,
582
+ initialVariables: Record<string, any> = {},
583
+ ): Promise<void> {
578
584
  const url = new URL("v2/listen-inbound", this._client.getWebsocketBase());
579
585
  if ("agent_number" in conn) {
580
586
  url.searchParams.set("phone_number", conn.agent_number);
@@ -632,9 +638,11 @@ export class Agent {
632
638
  case "assign-call": {
633
639
  const { call_id, call_info } = msg;
634
640
  this._logger.info("Received call (session ID: %s)", call_id);
635
- this._handleAssignedCall(call_id, call_info, socket).catch((err) => {
636
- this._logger.error("Error handling assigned call %s: %s", call_id, err);
637
- });
641
+ this._handleAssignedCall(call_id, call_info, socket, initialVariables).catch(
642
+ (err) => {
643
+ this._logger.error("Error handling assigned call %s: %s", call_id, err);
644
+ },
645
+ );
638
646
  break;
639
647
  }
640
648
  }
package/src/call.ts CHANGED
@@ -10,7 +10,9 @@ import {
10
10
  ReadScriptCommand,
11
11
  RetryTaskCommand,
12
12
  SetAgentDTMFCommand,
13
+ SendAgentDTMFCommand,
13
14
  } from "./commands.ts";
15
+ import { DTMF_DIGITS, type DTMFDigit } from "./events.ts";
14
16
  import type * as z from "zod";
15
17
  import type {
16
18
  ActionItem,
@@ -133,12 +135,35 @@ export class Call {
133
135
  }
134
136
 
135
137
  async setAgentDtmf(enabled: boolean) {
138
+ if (this._callInfo.call_type === "webrtc") {
139
+ throw new Error("WebRTC calls do not support sending DTMF.");
140
+ }
136
141
  await this.sendCommand(SetAgentDTMFCommand, {
137
142
  command_type: "set-agent-dtmf",
138
143
  enabled,
139
144
  });
140
145
  }
141
146
 
147
+ async sendDtmf(digits: DTMFDigit[] | string) {
148
+ if (this._callInfo.call_type === "webrtc") {
149
+ throw new Error("WebRTC calls do not support sending DTMF.");
150
+ }
151
+
152
+ const digitsList = (typeof digits === "string" ? [...digits] : digits) as DTMFDigit[];
153
+
154
+ const validSet = new Set<string>(DTMF_DIGITS);
155
+ if (!digitsList.every((d) => validSet.has(d))) {
156
+ throw new Error(
157
+ `Please input a valid set of DTMF digits. The valid DTMF digits are: ${JSON.stringify(DTMF_DIGITS)}.`,
158
+ );
159
+ }
160
+
161
+ await this.sendCommand(SendAgentDTMFCommand, {
162
+ command_type: "send-agent-dtmf",
163
+ digits: digitsList,
164
+ });
165
+ }
166
+
142
167
  async setLanguageMode(args: { primary?: Language; secondary?: Language[] }) {
143
168
  await this.sendCommand(SetLanguageModeCommand, {
144
169
  command_type: "set-language-mode",
package/src/client.ts CHANGED
@@ -1,19 +1,10 @@
1
- import WebSocket from "ws";
2
1
  import { type Logger, getDefaultLogger } from "./logging.ts";
3
- import {
4
- StartOutboundCallCommand,
5
- ListenInboundCommand,
6
- InboundTunnelCommand,
7
- } from "./commands.ts";
8
- import * as z from "zod";
9
- import { ErrorEvent, SessionStartedEvent, decodeEvent, InboundTunnelEvent } from "./events.ts";
10
2
  import { SDK_VERSION } from "./version.ts";
11
3
  import os from "node:os";
12
4
  import * as fs from "node:fs";
13
5
  import { getBaseUrl, fetchOrThrow, sleep } from "./utils.ts";
14
6
  import { SmsMessage } from "./sms.ts";
15
7
  import { telemetryClient } from "./telemetry.ts";
16
- import type { CallController } from "./call-controller.ts";
17
8
  import {
18
9
  type AuthStrategy,
19
10
  APIKeyAuth,
@@ -35,10 +26,6 @@ export interface ClientOptions {
35
26
 
36
27
  let firstClient = false;
37
28
 
38
- function stringifyZod<Schema extends z.ZodType>(schema: Schema, data: z.input<Schema>): string {
39
- return JSON.stringify(schema.parse(data));
40
- }
41
-
42
29
  export type InboundConnection = { agent_number: string } | { webrtc_code: string };
43
30
 
44
31
  const http_start = /^http:\/\//;
@@ -49,9 +36,6 @@ export class Client {
49
36
  private _auth: AuthStrategy;
50
37
  private _baseUrl: string;
51
38
  private _logger: Logger;
52
- private _ws?: WebSocket;
53
- private _controller?: CallController;
54
- private messageHandler?: (_: WebSocket.MessageEvent) => void;
55
39
 
56
40
  constructor({
57
41
  apiKey,
@@ -245,185 +229,4 @@ export class Client {
245
229
  await sleep(Math.min(pollIntervalMs, remaining));
246
230
  }
247
231
  }
248
-
249
- /**
250
- * @description use the Guava API to call out to a number
251
- */
252
- async createOutbound(
253
- fromNumber: string | undefined,
254
- toNumber: string,
255
- callController: CallController,
256
- ) {
257
- const url = new URL("v1/create-outbound", this.getWebsocketBase());
258
- const ws = new WebSocket(url, {
259
- headers: await this.headers(),
260
- });
261
-
262
- ws.addEventListener("open", async (_ev) => {
263
- ws.send(
264
- stringifyZod(StartOutboundCallCommand, {
265
- command_type: "start-outbound",
266
- to_number: toNumber,
267
- from_number: fromNumber,
268
- }),
269
- );
270
-
271
- // set the callController drain function to send all commands
272
- // through the now open websocket
273
- callController.setDrain(async (commands) => {
274
- for (const command of commands.splice(0)) {
275
- this._logger.debug(`Sending command ${JSON.stringify(command)}`);
276
- ws.send(JSON.stringify(command));
277
- }
278
- });
279
-
280
- await callController.onCallStart();
281
- });
282
-
283
- ws.addEventListener("close", (_ev) => {
284
- // we are closing the socket, so don't trigger any other listeners
285
- ws.removeAllListeners();
286
- this._ws = undefined;
287
- this._controller = undefined;
288
- });
289
-
290
- this._ws = ws;
291
- this._controller = callController;
292
- this.replaceHandler(this.uninitializedOutbound.bind(this));
293
- }
294
-
295
- private replaceHandler(newHandler?: (_: WebSocket.MessageEvent) => void) {
296
- if (this.messageHandler) {
297
- this._ws?.removeEventListener("message", this.messageHandler);
298
- }
299
- if (newHandler) {
300
- this._ws?.addEventListener("message", newHandler);
301
- }
302
- this.messageHandler = newHandler;
303
- }
304
-
305
- // eventlistener handlers for server events
306
- // (a state machine in functions)
307
- private uninitializedOutbound(ev: WebSocket.MessageEvent) {
308
- // for correctness (and type correctness)
309
- if (!this._ws) {
310
- throw new Error("[internal] Uninitialized WebSocket");
311
- }
312
-
313
- const session_started = z
314
- .union([SessionStartedEvent, ErrorEvent])
315
- .parse(JSON.parse(ev.data.toString("utf8")));
316
- if (session_started.event_type === "error") {
317
- throw new Error(`Outbound call failed: ${session_started.content}`);
318
- } else {
319
- this._logger.info(`Started session with ID: ${session_started.session_id}`);
320
- // move to next state
321
- this.replaceHandler(this.initializedOutbound.bind(this));
322
- }
323
- }
324
-
325
- private async initializedOutbound(ev: WebSocket.MessageEvent) {
326
- // for correctness (and type correctness)
327
- if (!this._ws) {
328
- throw new Error("[internal] Uninitialized WebSocket");
329
- }
330
-
331
- // handle the received event
332
- const event = decodeEvent(ev.data);
333
- if (event) {
334
- if (this._controller) {
335
- await this._controller.onEvent(event);
336
- }
337
- if (event.event_type === "outbound-call-failed" || event.event_type === "bot-session-ended") {
338
- // shutdown the websocket
339
- this._ws.close();
340
- }
341
- }
342
- }
343
-
344
- /**
345
- * @description use the Guava API to receive calls at a given number
346
- */
347
- async listenInbound<U extends CallController>(
348
- conn: InboundConnection,
349
- controllerClassFactory: (logger: Logger) => U,
350
- ): Promise<InboundListener> {
351
- const callControllers: Record<string, U> = {};
352
-
353
- // return a way to *stop* listening
354
- const url = new URL("v1/listen-inbound", this.getWebsocketBase());
355
- const ws = new WebSocket(url, {
356
- headers: await this.headers(),
357
- });
358
- let agent_number: string | undefined;
359
- let webrtc_code: string | undefined;
360
- if ("agent_number" in conn) {
361
- agent_number = conn.agent_number;
362
- } else {
363
- webrtc_code = conn.webrtc_code;
364
- }
365
-
366
- this._logger.info(`Listening for calls to ${agent_number ?? webrtc_code}`);
367
-
368
- if (webrtc_code) {
369
- const debugurl = new URL(`debug-webrtc?webrtc_code=${webrtc_code}`, this.getHttpBase());
370
- this._logger.debug(`WebRTC DebugURL: ${debugurl}`);
371
- }
372
-
373
- ws.addEventListener("open", (_ev) => {
374
- ws.send(
375
- stringifyZod(ListenInboundCommand, {
376
- command_type: "listen-inbound",
377
- agent_number: agent_number,
378
- webrtc_code: webrtc_code,
379
- }),
380
- );
381
- });
382
-
383
- ws.addEventListener("close", (_ev) => {
384
- ws.removeAllListeners();
385
- });
386
-
387
- ws.addEventListener("message", (ev) => {
388
- const tunnel_event = InboundTunnelEvent.parse(JSON.parse(ev.data.toString("utf8")));
389
- if (!(tunnel_event.call_id in callControllers)) {
390
- this._logger.info(
391
- `Received tunnel event for new call ID: ${tunnel_event.call_id}. Creating call controller.`,
392
- );
393
-
394
- const newController = controllerClassFactory(this._logger);
395
- newController.setDrain(async (commands) => {
396
- for (const command of commands.splice(0)) {
397
- this._logger.debug(
398
- `Sending command: ${JSON.stringify(command)} for call ID: ${tunnel_event.call_id}`,
399
- );
400
- ws.send(
401
- stringifyZod(InboundTunnelCommand, {
402
- call_id: tunnel_event.call_id,
403
- command,
404
- }),
405
- );
406
- }
407
- });
408
- callControllers[tunnel_event.call_id] = newController;
409
- newController.onEvent(tunnel_event.event);
410
- } else {
411
- // no threading, so manually forward to onEvent!
412
- callControllers[tunnel_event.call_id].onEvent(tunnel_event.event);
413
- }
414
- });
415
-
416
- return new InboundListener(ws);
417
- }
418
- }
419
-
420
- class InboundListener {
421
- private ws: WebSocket;
422
- constructor(ws: WebSocket) {
423
- this.ws = ws;
424
- }
425
-
426
- close() {
427
- this.ws.close();
428
- }
429
232
  }