@guava-ai/guava-sdk 0.12.0 → 0.13.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 +1 -1
- package/dist/examples/help-desk.js.map +1 -1
- package/dist/examples/property-insurance.js +1 -1
- package/dist/examples/property-insurance.js.map +1 -1
- package/dist/examples/restaurant-waitlist.js +1 -1
- package/dist/examples/restaurant-waitlist.js.map +1 -1
- package/dist/examples/scheduling-outbound.js +1 -1
- package/dist/examples/scheduling-outbound.js.map +1 -1
- package/dist/src/agent.d.ts +5 -1
- package/dist/src/agent.js +11 -2
- package/dist/src/agent.js.map +1 -1
- package/dist/src/client.d.ts +45 -0
- package/dist/src/client.js +344 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/helpers/openai.js +1 -1
- package/dist/src/helpers/openai.js.map +1 -1
- package/dist/src/index.d.ts +1 -44
- package/dist/src/index.js +6 -347
- package/dist/src/index.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/examples/help-desk.ts +1 -1
- package/examples/property-insurance.ts +1 -1
- package/examples/restaurant-waitlist.ts +1 -1
- package/examples/scheduling-outbound.ts +1 -1
- package/package.json +1 -1
- package/src/agent.ts +17 -2
- package/src/client.ts +304 -0
- package/src/helpers/openai.ts +1 -1
- package/src/index.ts +1 -307
- package/src/version.ts +1 -1
package/src/client.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
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
|
+
import { SDK_VERSION } from "./version.ts";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import { getBaseUrl, fetchOrThrow } from "./utils.ts";
|
|
13
|
+
import { telemetryClient } from "./telemetry.ts";
|
|
14
|
+
import type { CallController } from "./call-controller.ts";
|
|
15
|
+
|
|
16
|
+
const SDK_NAME = "typescript-sdk";
|
|
17
|
+
|
|
18
|
+
let firstClient = false;
|
|
19
|
+
|
|
20
|
+
function stringifyZod<Schema extends z.ZodType>(schema: Schema, data: z.input<Schema>): string {
|
|
21
|
+
return JSON.stringify(schema.parse(data));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type InboundConnection = { agent_number: string } | { webrtc_code: string };
|
|
25
|
+
|
|
26
|
+
const http_start = /^http:\/\//;
|
|
27
|
+
const https_start = /^https:\/\//;
|
|
28
|
+
|
|
29
|
+
@telemetryClient.trackClass()
|
|
30
|
+
export class Client {
|
|
31
|
+
private _apiKey: string;
|
|
32
|
+
private _baseUrl: string;
|
|
33
|
+
private _logger: Logger;
|
|
34
|
+
private _ws?: WebSocket;
|
|
35
|
+
private _controller?: CallController;
|
|
36
|
+
private messageHandler?: (_: WebSocket.MessageEvent) => void;
|
|
37
|
+
|
|
38
|
+
constructor(apiKey?: string, baseUrl?: string, logger?: Logger, captureWarnings: boolean = true) {
|
|
39
|
+
// Set up the default logger.
|
|
40
|
+
if (logger) {
|
|
41
|
+
this._logger = logger;
|
|
42
|
+
} else {
|
|
43
|
+
this._logger = getDefaultLogger();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Resolve the API base URL.
|
|
47
|
+
if (baseUrl) {
|
|
48
|
+
this._baseUrl = baseUrl;
|
|
49
|
+
} else {
|
|
50
|
+
this._baseUrl = getBaseUrl();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Resolve the API key.
|
|
54
|
+
if (apiKey) {
|
|
55
|
+
this._apiKey = apiKey;
|
|
56
|
+
} else if (process.env.GUAVA_API_KEY) {
|
|
57
|
+
this._apiKey = process.env.GUAVA_API_KEY;
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Guava API key must be provided either as argument to client constructor, or in environment variable GUAVA_API_KEY.",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!firstClient) {
|
|
65
|
+
firstClient = true;
|
|
66
|
+
|
|
67
|
+
if (captureWarnings) {
|
|
68
|
+
process.on("warning", (warning) => {
|
|
69
|
+
this._logger.warn(warning.toString());
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
telemetryClient.setSdkHeaders(this.headers());
|
|
74
|
+
this._checkSdkDeprecation();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getWebsocketBase() {
|
|
79
|
+
if (http_start.test(this._baseUrl)) {
|
|
80
|
+
return `ws://${this._baseUrl.substring("ws://".length)}`;
|
|
81
|
+
} else if (https_start.test(this._baseUrl)) {
|
|
82
|
+
return `wss://${this._baseUrl.substring("wss://".length)}`;
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error(`Invalid base URL: ${this._baseUrl}}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getHttpBase() {
|
|
89
|
+
return this._baseUrl;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
headers() {
|
|
93
|
+
return {
|
|
94
|
+
Authorization: `Bearer ${this._apiKey}`,
|
|
95
|
+
"x-guava-platform": os.platform(),
|
|
96
|
+
"x-guava-runtime": process.release.name,
|
|
97
|
+
"x-guava-runtime-version": process.version,
|
|
98
|
+
"x-guava-sdk": SDK_NAME,
|
|
99
|
+
"x-guava-sdk-version": SDK_VERSION,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async _checkSdkDeprecation() {
|
|
104
|
+
this._logger.debug(`Checking deprecation for SDK ${SDK_NAME}, ${SDK_VERSION}.`);
|
|
105
|
+
try {
|
|
106
|
+
const url = new URL("v1/check-sdk-deprecation", this.getHttpBase());
|
|
107
|
+
url.searchParams.set("sdk_name", SDK_NAME);
|
|
108
|
+
url.searchParams.set("sdk_version", SDK_VERSION);
|
|
109
|
+
const response = await fetchOrThrow(url, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: this.headers(),
|
|
112
|
+
});
|
|
113
|
+
const body = (await response.json()) as { deprecation_status: string };
|
|
114
|
+
if (body.deprecation_status === "supported") {
|
|
115
|
+
this._logger.info("SDK version still supported.");
|
|
116
|
+
} else if (body.deprecation_status === "deprecated") {
|
|
117
|
+
process.emitWarning(
|
|
118
|
+
"This SDK version is deprecated. Please update to a newer version of the SDK.",
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
this._logger.warn("SDK deprecation status unknown.");
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
this._logger.error("Encountered issue while checking for deprecation.");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @description use the Guava API to call out to a number
|
|
130
|
+
*/
|
|
131
|
+
createOutbound(fromNumber: string | undefined, toNumber: string, callController: CallController) {
|
|
132
|
+
const url = new URL("v1/create-outbound", this.getWebsocketBase());
|
|
133
|
+
const ws = new WebSocket(url, {
|
|
134
|
+
headers: this.headers(),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
ws.addEventListener("open", async (_ev) => {
|
|
138
|
+
ws.send(
|
|
139
|
+
stringifyZod(StartOutboundCallCommand, {
|
|
140
|
+
command_type: "start-outbound",
|
|
141
|
+
to_number: toNumber,
|
|
142
|
+
from_number: fromNumber,
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// set the callController drain function to send all commands
|
|
147
|
+
// through the now open websocket
|
|
148
|
+
callController.setDrain(async (commands) => {
|
|
149
|
+
for (const command of commands.splice(0)) {
|
|
150
|
+
this._logger.debug(`Sending command ${JSON.stringify(command)}`);
|
|
151
|
+
ws.send(JSON.stringify(command));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await callController.onCallStart();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
ws.addEventListener("close", (_ev) => {
|
|
159
|
+
// we are closing the socket, so don't trigger any other listeners
|
|
160
|
+
ws.removeAllListeners();
|
|
161
|
+
this._ws = undefined;
|
|
162
|
+
this._controller = undefined;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
this._ws = ws;
|
|
166
|
+
this._controller = callController;
|
|
167
|
+
this.replaceHandler(this.uninitializedOutbound.bind(this));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private replaceHandler(newHandler?: (_: WebSocket.MessageEvent) => void) {
|
|
171
|
+
if (this.messageHandler) {
|
|
172
|
+
this._ws?.removeEventListener("message", this.messageHandler);
|
|
173
|
+
}
|
|
174
|
+
if (newHandler) {
|
|
175
|
+
this._ws?.addEventListener("message", newHandler);
|
|
176
|
+
}
|
|
177
|
+
this.messageHandler = newHandler;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// eventlistener handlers for server events
|
|
181
|
+
// (a state machine in functions)
|
|
182
|
+
private uninitializedOutbound(ev: WebSocket.MessageEvent) {
|
|
183
|
+
// for correctness (and type correctness)
|
|
184
|
+
if (!this._ws) {
|
|
185
|
+
throw new Error("[internal] Uninitialized WebSocket");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const session_started = z
|
|
189
|
+
.union([SessionStartedEvent, ErrorEvent])
|
|
190
|
+
.parse(JSON.parse(ev.data.toString("utf8")));
|
|
191
|
+
if (session_started.event_type === "error") {
|
|
192
|
+
throw new Error(`Outbound call failed: ${session_started.content}`);
|
|
193
|
+
} else {
|
|
194
|
+
this._logger.info(`Started session with ID: ${session_started.session_id}`);
|
|
195
|
+
// move to next state
|
|
196
|
+
this.replaceHandler(this.initializedOutbound.bind(this));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async initializedOutbound(ev: WebSocket.MessageEvent) {
|
|
201
|
+
// for correctness (and type correctness)
|
|
202
|
+
if (!this._ws) {
|
|
203
|
+
throw new Error("[internal] Uninitialized WebSocket");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// handle the received event
|
|
207
|
+
const event = decodeEvent(ev.data);
|
|
208
|
+
if (event) {
|
|
209
|
+
if (this._controller) {
|
|
210
|
+
await this._controller.onEvent(event);
|
|
211
|
+
}
|
|
212
|
+
if (event.event_type === "outbound-call-failed" || event.event_type === "bot-session-ended") {
|
|
213
|
+
// shutdown the websocket
|
|
214
|
+
this._ws.close();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @description use the Guava API to receive calls at a given number
|
|
221
|
+
*/
|
|
222
|
+
listenInbound<U extends CallController>(
|
|
223
|
+
conn: InboundConnection,
|
|
224
|
+
controllerClassFactory: (logger: Logger) => U,
|
|
225
|
+
) {
|
|
226
|
+
const callControllers: Record<string, U> = {};
|
|
227
|
+
|
|
228
|
+
// return a way to *stop* listening
|
|
229
|
+
const url = new URL("v1/listen-inbound", this.getWebsocketBase());
|
|
230
|
+
const ws = new WebSocket(url, {
|
|
231
|
+
headers: this.headers(),
|
|
232
|
+
});
|
|
233
|
+
let agent_number: string | undefined;
|
|
234
|
+
let webrtc_code: string | undefined;
|
|
235
|
+
if ("agent_number" in conn) {
|
|
236
|
+
agent_number = conn.agent_number;
|
|
237
|
+
} else {
|
|
238
|
+
webrtc_code = conn.webrtc_code;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this._logger.info(`Listening for calls to ${agent_number ?? webrtc_code}`);
|
|
242
|
+
|
|
243
|
+
if (webrtc_code) {
|
|
244
|
+
const debugurl = new URL(`debug-webrtc?webrtc_code=${webrtc_code}`, this.getHttpBase());
|
|
245
|
+
this._logger.debug(`WebRTC DebugURL: ${debugurl}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
ws.addEventListener("open", (_ev) => {
|
|
249
|
+
ws.send(
|
|
250
|
+
stringifyZod(ListenInboundCommand, {
|
|
251
|
+
command_type: "listen-inbound",
|
|
252
|
+
agent_number: agent_number,
|
|
253
|
+
webrtc_code: webrtc_code,
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
ws.addEventListener("close", (_ev) => {
|
|
259
|
+
ws.removeAllListeners();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
ws.addEventListener("message", (ev) => {
|
|
263
|
+
const tunnel_event = InboundTunnelEvent.parse(JSON.parse(ev.data.toString("utf8")));
|
|
264
|
+
if (!(tunnel_event.call_id in callControllers)) {
|
|
265
|
+
this._logger.info(
|
|
266
|
+
`Received tunnel event for new call ID: ${tunnel_event.call_id}. Creating call controller.`,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const newController = controllerClassFactory(this._logger);
|
|
270
|
+
newController.setDrain(async (commands) => {
|
|
271
|
+
for (const command of commands.splice(0)) {
|
|
272
|
+
this._logger.debug(
|
|
273
|
+
`Sending command: ${JSON.stringify(command)} for call ID: ${tunnel_event.call_id}`,
|
|
274
|
+
);
|
|
275
|
+
ws.send(
|
|
276
|
+
stringifyZod(InboundTunnelCommand, {
|
|
277
|
+
call_id: tunnel_event.call_id,
|
|
278
|
+
command,
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
callControllers[tunnel_event.call_id] = newController;
|
|
284
|
+
newController.onEvent(tunnel_event.event);
|
|
285
|
+
} else {
|
|
286
|
+
// no threading, so manually forward to onEvent!
|
|
287
|
+
callControllers[tunnel_event.call_id].onEvent(tunnel_event.event);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return new InboundListener(ws);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
class InboundListener {
|
|
296
|
+
private ws: WebSocket;
|
|
297
|
+
constructor(ws: WebSocket) {
|
|
298
|
+
this.ws = ws;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
close() {
|
|
302
|
+
this.ws.close();
|
|
303
|
+
}
|
|
304
|
+
}
|
package/src/helpers/openai.ts
CHANGED
|
@@ -11,7 +11,7 @@ function beta_create_openai_client(logger: Logger) {
|
|
|
11
11
|
const baseUrl = getBaseUrl();
|
|
12
12
|
// to get it working with OpenAI TS/JS client
|
|
13
13
|
const basedUrl = new URL("openai/v1/", baseUrl);
|
|
14
|
-
logger.
|
|
14
|
+
logger.debug(`Creating beta OpenAI client`);
|
|
15
15
|
return new OpenAI({
|
|
16
16
|
baseURL: basedUrl.toString(),
|
|
17
17
|
apiKey: process.env.GUAVA_API_KEY,
|
package/src/index.ts
CHANGED
|
@@ -1,312 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
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
|
-
import { SDK_VERSION } from "./version.ts";
|
|
11
|
-
import os from "node:os";
|
|
12
|
-
import { getBaseUrl, fetchOrThrow } from "./utils.ts";
|
|
13
|
-
import { telemetryClient } from "./telemetry.ts";
|
|
14
|
-
import type { CallController } from "./call-controller.ts";
|
|
1
|
+
export { Client, type InboundConnection } from "./client.ts";
|
|
15
2
|
export { CallController, type TaskObjective } from "./call-controller.ts";
|
|
16
3
|
export { Say, Field } from "./action-item.ts";
|
|
17
4
|
export { Logger, getConsoleLogger, getDefaultLogger } from "./logging.ts";
|
|
18
5
|
export { Agent, CallInfo } from "./agent.ts";
|
|
19
6
|
export { Call } from "./call.ts";
|
|
20
|
-
|
|
21
|
-
const SDK_NAME = "typescript-sdk";
|
|
22
|
-
|
|
23
|
-
let firstClient = false;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @description convenience function for stringifying data according to a schema
|
|
27
|
-
*/
|
|
28
|
-
function stringifyZod<Schema extends z.ZodType>(schema: Schema, data: z.input<Schema>): string {
|
|
29
|
-
return JSON.stringify(schema.parse(data));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type InboundConnection = { agent_number: string } | { webrtc_code: string };
|
|
33
|
-
|
|
34
|
-
const http_start = /^http:\/\//;
|
|
35
|
-
const https_start = /^https:\/\//;
|
|
36
|
-
|
|
37
|
-
@telemetryClient.trackClass()
|
|
38
|
-
export class Client {
|
|
39
|
-
private _apiKey: string;
|
|
40
|
-
private _baseUrl: string;
|
|
41
|
-
private _logger: Logger;
|
|
42
|
-
private _ws?: WebSocket;
|
|
43
|
-
private _controller?: CallController;
|
|
44
|
-
private messageHandler?: (_: WebSocket.MessageEvent) => void;
|
|
45
|
-
|
|
46
|
-
constructor(apiKey?: string, baseUrl?: string, logger?: Logger, captureWarnings: boolean = true) {
|
|
47
|
-
// Set up the default logger.
|
|
48
|
-
if (logger) {
|
|
49
|
-
this._logger = logger;
|
|
50
|
-
} else {
|
|
51
|
-
this._logger = getDefaultLogger();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Resolve the API base URL.
|
|
55
|
-
if (baseUrl) {
|
|
56
|
-
this._baseUrl = baseUrl;
|
|
57
|
-
} else {
|
|
58
|
-
this._baseUrl = getBaseUrl();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Resolve the API key.
|
|
62
|
-
if (apiKey) {
|
|
63
|
-
this._apiKey = apiKey;
|
|
64
|
-
} else if (process.env.GUAVA_API_KEY) {
|
|
65
|
-
this._apiKey = process.env.GUAVA_API_KEY;
|
|
66
|
-
} else {
|
|
67
|
-
throw new Error(
|
|
68
|
-
"Guava API key must be provided either as argument to client constructor, or in environment variable GUAVA_API_KEY.",
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!firstClient) {
|
|
73
|
-
firstClient = true;
|
|
74
|
-
|
|
75
|
-
if (captureWarnings) {
|
|
76
|
-
process.on("warning", (warning) => {
|
|
77
|
-
this._logger.warn(warning.toString());
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
telemetryClient.setSdkHeaders(this.headers());
|
|
82
|
-
this._checkSdkDeprecation();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
getWebsocketBase() {
|
|
87
|
-
if (http_start.test(this._baseUrl)) {
|
|
88
|
-
return `ws://${this._baseUrl.substring("ws://".length)}`;
|
|
89
|
-
} else if (https_start.test(this._baseUrl)) {
|
|
90
|
-
return `wss://${this._baseUrl.substring("wss://".length)}`;
|
|
91
|
-
} else {
|
|
92
|
-
throw new Error(`Invalid base URL: ${this._baseUrl}}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
getHttpBase() {
|
|
97
|
-
return this._baseUrl;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
headers() {
|
|
101
|
-
return {
|
|
102
|
-
Authorization: `Bearer ${this._apiKey}`,
|
|
103
|
-
"x-guava-platform": os.platform(),
|
|
104
|
-
"x-guava-runtime": process.release.name,
|
|
105
|
-
"x-guava-runtime-version": process.version,
|
|
106
|
-
"x-guava-sdk": SDK_NAME,
|
|
107
|
-
"x-guava-sdk-version": SDK_VERSION,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private async _checkSdkDeprecation() {
|
|
112
|
-
this._logger.debug(`Checking deprecation for SDK ${SDK_NAME}, ${SDK_VERSION}.`);
|
|
113
|
-
try {
|
|
114
|
-
const url = new URL("v1/check-sdk-deprecation", this.getHttpBase());
|
|
115
|
-
url.searchParams.set("sdk_name", SDK_NAME);
|
|
116
|
-
url.searchParams.set("sdk_version", SDK_VERSION);
|
|
117
|
-
const response = await fetchOrThrow(url, {
|
|
118
|
-
method: "POST",
|
|
119
|
-
headers: this.headers(),
|
|
120
|
-
});
|
|
121
|
-
const body = (await response.json()) as { deprecation_status: string };
|
|
122
|
-
if (body.deprecation_status === "supported") {
|
|
123
|
-
this._logger.info("SDK version still supported.");
|
|
124
|
-
} else if (body.deprecation_status === "deprecated") {
|
|
125
|
-
process.emitWarning(
|
|
126
|
-
"This SDK version is deprecated. Please update to a newer version of the SDK.",
|
|
127
|
-
);
|
|
128
|
-
} else {
|
|
129
|
-
this._logger.warn("SDK deprecation status unknown.");
|
|
130
|
-
}
|
|
131
|
-
} catch (e) {
|
|
132
|
-
this._logger.error("Encountered issue while checking for deprecation.");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* @description use the Guava API to call out to a number
|
|
138
|
-
*/
|
|
139
|
-
createOutbound(fromNumber: string | undefined, toNumber: string, callController: CallController) {
|
|
140
|
-
const url = new URL("v1/create-outbound", this.getWebsocketBase());
|
|
141
|
-
const ws = new WebSocket(url, {
|
|
142
|
-
headers: this.headers(),
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
ws.addEventListener("open", async (_ev) => {
|
|
146
|
-
ws.send(
|
|
147
|
-
stringifyZod(StartOutboundCallCommand, {
|
|
148
|
-
command_type: "start-outbound",
|
|
149
|
-
to_number: toNumber,
|
|
150
|
-
from_number: fromNumber,
|
|
151
|
-
}),
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
// set the callController drain function to send all commands
|
|
155
|
-
// through the now open websocket
|
|
156
|
-
callController.setDrain(async (commands) => {
|
|
157
|
-
for (const command of commands.splice(0)) {
|
|
158
|
-
this._logger.debug(`Sending command ${JSON.stringify(command)}`);
|
|
159
|
-
ws.send(JSON.stringify(command));
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
await callController.onCallStart();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
ws.addEventListener("close", (_ev) => {
|
|
167
|
-
// we are closing the socket, so don't trigger any other listeners
|
|
168
|
-
ws.removeAllListeners();
|
|
169
|
-
this._ws = undefined;
|
|
170
|
-
this._controller = undefined;
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
this._ws = ws;
|
|
174
|
-
this._controller = callController;
|
|
175
|
-
this.replaceHandler(this.uninitializedOutbound.bind(this));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private replaceHandler(newHandler?: (_: WebSocket.MessageEvent) => void) {
|
|
179
|
-
if (this.messageHandler) {
|
|
180
|
-
this._ws?.removeEventListener("message", this.messageHandler);
|
|
181
|
-
}
|
|
182
|
-
if (newHandler) {
|
|
183
|
-
this._ws?.addEventListener("message", newHandler);
|
|
184
|
-
}
|
|
185
|
-
this.messageHandler = newHandler;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// eventlistener handlers for server events
|
|
189
|
-
// (a state machine in functions)
|
|
190
|
-
private uninitializedOutbound(ev: WebSocket.MessageEvent) {
|
|
191
|
-
// for correctness (and type correctness)
|
|
192
|
-
if (!this._ws) {
|
|
193
|
-
throw new Error("[internal] Uninitialized WebSocket");
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const session_started = z
|
|
197
|
-
.union([SessionStartedEvent, ErrorEvent])
|
|
198
|
-
.parse(JSON.parse(ev.data.toString("utf8")));
|
|
199
|
-
if (session_started.event_type === "error") {
|
|
200
|
-
throw new Error(`Outbound call failed: ${session_started.content}`);
|
|
201
|
-
} else {
|
|
202
|
-
this._logger.info(`Started session with ID: ${session_started.session_id}`);
|
|
203
|
-
// move to next state
|
|
204
|
-
this.replaceHandler(this.initializedOutbound.bind(this));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
private async initializedOutbound(ev: WebSocket.MessageEvent) {
|
|
209
|
-
// for correctness (and type correctness)
|
|
210
|
-
if (!this._ws) {
|
|
211
|
-
throw new Error("[internal] Uninitialized WebSocket");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// handle the received event
|
|
215
|
-
const event = decodeEvent(ev.data);
|
|
216
|
-
if (event) {
|
|
217
|
-
if (this._controller) {
|
|
218
|
-
await this._controller.onEvent(event);
|
|
219
|
-
}
|
|
220
|
-
if (event.event_type === "outbound-call-failed" || event.event_type === "bot-session-ended") {
|
|
221
|
-
// shutdown the websocket
|
|
222
|
-
this._ws.close();
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* @description use the Guava API to receive calls at a given number
|
|
229
|
-
*/
|
|
230
|
-
listenInbound<U extends CallController>(
|
|
231
|
-
conn: InboundConnection,
|
|
232
|
-
controllerClassFactory: (logger: Logger) => U,
|
|
233
|
-
) {
|
|
234
|
-
const callControllers: Record<string, U> = {};
|
|
235
|
-
|
|
236
|
-
// return a way to *stop* listening
|
|
237
|
-
const url = new URL("v1/listen-inbound", this.getWebsocketBase());
|
|
238
|
-
const ws = new WebSocket(url, {
|
|
239
|
-
headers: this.headers(),
|
|
240
|
-
});
|
|
241
|
-
let agent_number: string | undefined;
|
|
242
|
-
let webrtc_code: string | undefined;
|
|
243
|
-
if ("agent_number" in conn) {
|
|
244
|
-
agent_number = conn.agent_number;
|
|
245
|
-
} else {
|
|
246
|
-
webrtc_code = conn.webrtc_code;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
this._logger.info(`Listening for calls to ${agent_number ?? webrtc_code}`);
|
|
250
|
-
|
|
251
|
-
if (webrtc_code) {
|
|
252
|
-
const debugurl = new URL(`debug-webrtc?webrtc_code=${webrtc_code}`, this.getHttpBase());
|
|
253
|
-
this._logger.debug(`WebRTC DebugURL: ${debugurl}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
ws.addEventListener("open", (_ev) => {
|
|
257
|
-
ws.send(
|
|
258
|
-
stringifyZod(ListenInboundCommand, {
|
|
259
|
-
command_type: "listen-inbound",
|
|
260
|
-
agent_number: agent_number,
|
|
261
|
-
webrtc_code: webrtc_code,
|
|
262
|
-
}),
|
|
263
|
-
);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
ws.addEventListener("close", (_ev) => {
|
|
267
|
-
ws.removeAllListeners();
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
ws.addEventListener("message", (ev) => {
|
|
271
|
-
const tunnel_event = InboundTunnelEvent.parse(JSON.parse(ev.data.toString("utf8")));
|
|
272
|
-
if (!(tunnel_event.call_id in callControllers)) {
|
|
273
|
-
this._logger.info(
|
|
274
|
-
`Received tunnel event for new call ID: ${tunnel_event.call_id}. Creating call controller.`,
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
const newController = controllerClassFactory(this._logger);
|
|
278
|
-
newController.setDrain(async (commands) => {
|
|
279
|
-
for (const command of commands.splice(0)) {
|
|
280
|
-
this._logger.debug(
|
|
281
|
-
`Sending command: ${JSON.stringify(command)} for call ID: ${tunnel_event.call_id}`,
|
|
282
|
-
);
|
|
283
|
-
ws.send(
|
|
284
|
-
stringifyZod(InboundTunnelCommand, {
|
|
285
|
-
call_id: tunnel_event.call_id,
|
|
286
|
-
command,
|
|
287
|
-
}),
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
callControllers[tunnel_event.call_id] = newController;
|
|
292
|
-
newController.onEvent(tunnel_event.event);
|
|
293
|
-
} else {
|
|
294
|
-
// no threading, so manually forward to onEvent!
|
|
295
|
-
callControllers[tunnel_event.call_id].onEvent(tunnel_event.event);
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
return new InboundListener(ws);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
class InboundListener {
|
|
304
|
-
private ws: WebSocket;
|
|
305
|
-
constructor(ws: WebSocket) {
|
|
306
|
-
this.ws = ws;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
close() {
|
|
310
|
-
this.ws.close();
|
|
311
|
-
}
|
|
312
|
-
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.
|
|
1
|
+
export const SDK_VERSION = "0.13.0";
|