@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.
- package/README.md +1 -1
- package/bin/example-runner.ts +67 -26
- package/dist/bin/example-runner.d.ts +1 -25
- package/dist/bin/example-runner.js +64 -24
- package/dist/bin/example-runner.js.map +1 -1
- package/dist/examples/help-desk.d.ts +1 -1
- package/dist/examples/help-desk.js +34 -23
- package/dist/examples/help-desk.js.map +1 -1
- package/dist/examples/multiple-agents.d.ts +1 -1
- package/dist/examples/multiple-agents.js +1 -1
- package/dist/examples/multiple-agents.js.map +1 -1
- package/dist/examples/polling-campaign.d.ts +1 -1
- package/dist/examples/polling-campaign.js +9 -7
- package/dist/examples/polling-campaign.js.map +1 -1
- package/dist/examples/property-insurance.d.ts +1 -1
- package/dist/examples/property-insurance.js +34 -23
- package/dist/examples/property-insurance.js.map +1 -1
- package/dist/examples/restaurant-waitlist.d.ts +1 -1
- package/dist/examples/restaurant-waitlist.js +34 -23
- package/dist/examples/restaurant-waitlist.js.map +1 -1
- package/dist/examples/scheduling-outbound.d.ts +1 -1
- package/dist/examples/scheduling-outbound.js +24 -14
- package/dist/examples/scheduling-outbound.js.map +1 -1
- package/dist/src/agent.d.ts +2 -2
- package/dist/src/agent.js +4 -4
- package/dist/src/agent.js.map +1 -1
- package/dist/src/call.d.ts +2 -0
- package/dist/src/call.js +18 -0
- package/dist/src/call.js.map +1 -1
- package/dist/src/client.d.ts +0 -22
- package/dist/src/client.js +0 -155
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands.d.ts +62 -0
- package/dist/src/commands.js +7 -1
- package/dist/src/commands.js.map +1 -1
- package/dist/src/events.d.ts +2 -1
- package/dist/src/events.js +20 -2
- package/dist/src/events.js.map +1 -1
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +1 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/socket/client.js +10 -0
- package/dist/src/socket/client.js.map +1 -1
- package/dist/src/telemetry.js +1 -1
- package/dist/src/telemetry.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/examples/help-desk.ts +42 -20
- package/examples/multiple-agents.ts +1 -1
- package/examples/polling-campaign.ts +12 -7
- package/examples/property-insurance.ts +42 -22
- package/examples/restaurant-waitlist.ts +42 -22
- package/examples/scheduling-outbound.ts +26 -15
- package/package.json +2 -1
- package/src/agent.ts +16 -8
- package/src/call.ts +25 -0
- package/src/client.ts +0 -197
- package/src/commands.ts +8 -0
- package/src/events.ts +20 -18
- package/src/index.ts +0 -1
- package/src/socket/client.ts +22 -0
- package/src/telemetry.ts +1 -1
- package/src/version.ts +1 -1
- package/dist/src/call-controller.d.ts +0 -137
- package/dist/src/call-controller.js +0 -436
- package/dist/src/call-controller.js.map +0 -1
- package/src/call-controller.ts +0 -451
package/examples/help-desk.ts
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
71
|
+
export async function run(prog: string, args: string[]) {
|
|
72
|
+
const program = new Command().name(prog).showHelpAfterError();
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
283
|
-
|
|
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(
|
|
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(
|
|
636
|
-
|
|
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
|
}
|