@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/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
+ }
@@ -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.info(`Creating beta OpenAI client`);
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
- 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";
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.12.0";
1
+ export const SDK_VERSION = "0.13.0";