@guava-ai/guava-sdk 0.2.0 → 0.4.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/README.md +2 -7
- package/bin/example-runner.js +16 -7
- package/dist/examples/credit-card-activation.js +94 -112
- package/dist/examples/credit-card-activation.js.map +1 -1
- package/dist/examples/property-insurance.js +12 -26
- package/dist/examples/property-insurance.js.map +1 -1
- package/dist/examples/scheduling-outbound.d.ts +1 -0
- package/dist/examples/scheduling-outbound.js +62 -0
- package/dist/examples/scheduling-outbound.js.map +1 -0
- package/dist/examples/thai-palace.js +61 -0
- package/dist/examples/thai-palace.js.map +1 -0
- package/dist/package.json +7 -5
- package/dist/src/action_item.d.ts +34 -12
- package/dist/src/action_item.js +34 -7
- package/dist/src/action_item.js.map +1 -1
- package/dist/src/call-controller.d.ts +137 -0
- package/dist/src/call-controller.js +433 -0
- package/dist/src/call-controller.js.map +1 -0
- package/dist/src/commands.d.ts +67 -27
- package/dist/src/commands.js +41 -27
- package/dist/src/commands.js.map +1 -1
- package/dist/src/events.d.ts +47 -30
- package/dist/src/events.js +42 -36
- package/dist/src/events.js.map +1 -1
- package/dist/src/example_data.d.ts +1 -0
- package/dist/src/example_data.js +33 -0
- package/dist/src/example_data.js.map +1 -1
- package/dist/src/helpers/openai.d.ts +12 -1
- package/dist/src/helpers/openai.js +168 -68
- package/dist/src/helpers/openai.js.map +1 -1
- package/dist/src/index.d.ts +6 -121
- package/dist/src/index.js +249 -483
- package/dist/src/index.js.map +1 -1
- package/dist/src/logging.d.ts +2 -1
- package/dist/src/logging.js +32 -7
- package/dist/src/logging.js.map +1 -1
- package/dist/src/telemetry.d.ts +23 -0
- package/dist/src/telemetry.js +98 -0
- package/dist/src/telemetry.js.map +1 -0
- package/dist/src/utils.d.ts +3 -0
- package/dist/src/utils.js +28 -0
- package/dist/src/utils.js.map +1 -0
- package/examples/biome.json +5 -0
- package/examples/credit-card-activation.ts +20 -26
- package/examples/property-insurance.ts +6 -16
- package/examples/scheduling-outbound.ts +80 -0
- package/examples/{thai_palace.ts → thai-palace.ts} +10 -13
- package/package.json +7 -5
- package/src/action_item.ts +53 -13
- package/src/call-controller.ts +451 -0
- package/src/commands.ts +58 -42
- package/src/events.ts +66 -51
- package/src/example_data.ts +42 -0
- package/src/helpers/openai.ts +73 -18
- package/src/index.ts +81 -403
- package/src/logging.ts +39 -7
- package/src/telemetry.ts +125 -0
- package/src/utils.ts +32 -0
- package/dist/examples/thai_palace.js +0 -79
- package/dist/examples/thai_palace.js.map +0 -1
- /package/dist/examples/{thai_palace.d.ts → thai-palace.d.ts} +0 -0
package/src/index.ts
CHANGED
|
@@ -1,358 +1,37 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
-
import { type Logger,
|
|
2
|
+
import { type Logger, getDefaultLogger } from "./logging.ts";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
setPersona,
|
|
8
|
-
setTaskCommand,
|
|
9
|
-
answerQuestionCommand,
|
|
10
|
-
sendInstructionCommand,
|
|
11
|
-
readScriptCommand,
|
|
12
|
-
rejectInboundCallCommand,
|
|
13
|
-
transferCommand,
|
|
14
|
-
listenInboundCommand,
|
|
15
|
-
inboundTunnelCommand,
|
|
4
|
+
StartOutboundCallCommand,
|
|
5
|
+
ListenInboundCommand,
|
|
6
|
+
InboundTunnelCommand,
|
|
16
7
|
} from "./commands.ts";
|
|
17
8
|
import * as z from "zod";
|
|
18
|
-
import {
|
|
19
|
-
errorEvent,
|
|
20
|
-
type GuavaEvent,
|
|
21
|
-
sessionStartedEvent,
|
|
22
|
-
decodeEvent,
|
|
23
|
-
type CallerSpeechEvent,
|
|
24
|
-
type AgentSpeechEvent,
|
|
25
|
-
inboundTunnelEvent,
|
|
26
|
-
} from "./events.ts";
|
|
27
|
-
import type { ActionItem, Field, Say, Todo } from "./action_item.ts";
|
|
9
|
+
import { ErrorEvent, SessionStartedEvent, decodeEvent, InboundTunnelEvent } from "./events.ts";
|
|
28
10
|
import pkgdata from "../package.json" with { type: "json" };
|
|
29
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
|
+
export { CallController, type TaskObjective } from "./call-controller.ts";
|
|
16
|
+
export { Say, Field } from "./action_item.ts";
|
|
17
|
+
|
|
18
|
+
const SDK_NAME = "typescript-sdk";
|
|
30
19
|
|
|
31
|
-
|
|
32
|
-
const DEFAULT_LOG_LEVEL = "debug";
|
|
20
|
+
let firstClient = false;
|
|
33
21
|
|
|
34
22
|
/**
|
|
35
23
|
* @description convenience function for stringifying data according to a schema
|
|
36
24
|
*/
|
|
37
|
-
function stringifyZod<Schema extends z.ZodType>(
|
|
38
|
-
schema: Schema,
|
|
39
|
-
data: z.input<Schema>,
|
|
40
|
-
): string {
|
|
25
|
+
function stringifyZod<Schema extends z.ZodType>(schema: Schema, data: z.input<Schema>): string {
|
|
41
26
|
return JSON.stringify(schema.parse(data));
|
|
42
27
|
}
|
|
43
28
|
|
|
44
|
-
|
|
45
|
-
* Interface between Guava services and user-supplied code
|
|
46
|
-
*/
|
|
47
|
-
export class CallController {
|
|
48
|
-
private _commandQueue: Command[] = [];
|
|
49
|
-
private _on_complete_current_task?: () => void;
|
|
50
|
-
// private _field_values: Record<string, any>;
|
|
51
|
-
private _current_task_id?: string;
|
|
52
|
-
/**
|
|
53
|
-
* @protected
|
|
54
|
-
* @description logger used to emit diagnostics
|
|
55
|
-
*/
|
|
56
|
-
protected logger: Logger;
|
|
57
|
-
// drain functions are expected to cleanup
|
|
58
|
-
// the part of the queue that is successfully sent from its
|
|
59
|
-
// input (mutating it) (i.e. _drain should use Array.splice)
|
|
60
|
-
private _drain?: (_: Command[]) => Promise<void>;
|
|
61
|
-
private _fieldValues: Record<string, unknown> = {};
|
|
62
|
-
|
|
63
|
-
constructor(logger: Logger) {
|
|
64
|
-
// Set up the default logger.
|
|
65
|
-
this.logger = logger;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* @description Supply a function used to consume commands from the internal command queue.
|
|
70
|
-
*
|
|
71
|
-
* The function is expected to remove from the argument array commands that it has handled (iterating
|
|
72
|
-
* through the result of `Array.splice(0)` is sufficient)
|
|
73
|
-
*/
|
|
74
|
-
setDrain(newDrain: (_: Command[]) => Promise<void>) {
|
|
75
|
-
this._drain = newDrain;
|
|
76
|
-
this.flush();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* @description [inbound] receive a call, and process further.
|
|
81
|
-
*/
|
|
82
|
-
protected async acceptCall() {
|
|
83
|
-
await this.sendCommand(acceptInboundCallCommand, {
|
|
84
|
-
command_type: "accept-inbound",
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* @description read a span of text verbatim
|
|
90
|
-
*/
|
|
91
|
-
protected async readScript(script: string) {
|
|
92
|
-
await this.sendCommand(readScriptCommand, {
|
|
93
|
-
command_type: "read-script",
|
|
94
|
-
script: script,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* @description [inbound] reject a call
|
|
100
|
-
*/
|
|
101
|
-
protected async rejectCall() {
|
|
102
|
-
await this.sendCommand(rejectInboundCallCommand, {
|
|
103
|
-
command_type: "reject-inbound",
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
protected async addInfo(_info: string) {
|
|
108
|
-
throw new Error("not implemeneted");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* @description read a span of text non-verbatim
|
|
113
|
-
*/
|
|
114
|
-
protected async sendInstruction(instruction: string) {
|
|
115
|
-
await this.sendCommand(sendInstructionCommand, {
|
|
116
|
-
command_type: "send-instruction",
|
|
117
|
-
instruction: instruction,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* @description provide identifiers the agent will use to identify the virtual agent
|
|
123
|
-
*/
|
|
124
|
-
protected async setPersona(args: {
|
|
125
|
-
organizationName?: string;
|
|
126
|
-
agentName?: string;
|
|
127
|
-
agentPurpose?: string;
|
|
128
|
-
}) {
|
|
129
|
-
await this.sendCommand(setPersona, {
|
|
130
|
-
command_type: "set-persona",
|
|
131
|
-
organization_name: args.organizationName,
|
|
132
|
-
agent_name: args.agentName,
|
|
133
|
-
agent_purpose: args.agentPurpose,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* @description direct the agent to collect information
|
|
139
|
-
* @param goal {} an objective string and/or a checklist of information to collect
|
|
140
|
-
* @param on_complete {} a callback to call once the information is available from the agent
|
|
141
|
-
* @param args {} arguments to pass through to the `on_complete` callback
|
|
142
|
-
*/
|
|
143
|
-
protected setTask(
|
|
144
|
-
goal: TaskObjective,
|
|
145
|
-
on_complete: (...c: any[]) => void = () => {},
|
|
146
|
-
...args: any[]
|
|
147
|
-
) {
|
|
148
|
-
this._current_task_id = Math.random().toString(16).substring(2, 8);
|
|
149
|
-
this._on_complete_current_task = on_complete.bind(this, ...args);
|
|
150
|
-
if (!("checklist" in goal)) {
|
|
151
|
-
this.sendCommand(setTaskCommand, {
|
|
152
|
-
command_type: "set-task",
|
|
153
|
-
task_id: this._current_task_id,
|
|
154
|
-
objective: goal.objective,
|
|
155
|
-
action_items: [],
|
|
156
|
-
});
|
|
157
|
-
} else {
|
|
158
|
-
const action_items = goal.checklist.map((item) =>
|
|
159
|
-
typeof item == "string"
|
|
160
|
-
? ({
|
|
161
|
-
item_type: "todo",
|
|
162
|
-
description: item,
|
|
163
|
-
} satisfies Todo)
|
|
164
|
-
: item,
|
|
165
|
-
) satisfies ActionItem[];
|
|
166
|
-
this.sendCommand(setTaskCommand, {
|
|
167
|
-
command_type: "set-task",
|
|
168
|
-
task_id: this._current_task_id,
|
|
169
|
-
objective: goal.objective ?? "",
|
|
170
|
-
action_items,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* @description direct the agent to collect information, continuing execution once the agent has collected the information
|
|
177
|
-
* @param goal {} an objective string and/or a checklist of information to collect
|
|
178
|
-
*/
|
|
179
|
-
protected async awaitTask(goal: TaskObjective): Promise<void> {
|
|
180
|
-
return new Promise((resolve) => {
|
|
181
|
-
this.setTask(goal, (_args) => {
|
|
182
|
-
resolve();
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* @description retrieve a piece of information that the agent has collected
|
|
189
|
-
* @param key {string} key of the field checklist item
|
|
190
|
-
*/
|
|
191
|
-
protected getField(key: string) {
|
|
192
|
-
if (key in this._fieldValues) {
|
|
193
|
-
return this._fieldValues[key];
|
|
194
|
-
} else {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* @description [inbound] hang up an accepted call
|
|
201
|
-
*/
|
|
202
|
-
protected async hangup(final_instructions: string = "") {
|
|
203
|
-
let instructions: string;
|
|
204
|
-
if (final_instructions) {
|
|
205
|
-
instructions = `Start ending the conversation. Here are your final instructions: ${final_instructions} Once you've completed the final instructions, naturally end the conversation and hang up the call.`;
|
|
206
|
-
} else {
|
|
207
|
-
instructions = "Naturally end the conversation and hang up the call.";
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
this.sendInstruction(instructions);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* @description transfer an accepted call
|
|
215
|
-
*/
|
|
216
|
-
protected transfer(to_number: string, transfer_message?: string) {
|
|
217
|
-
const message = transfer_message ?? "I'm transferring you now";
|
|
218
|
-
this.sendCommand(transferCommand, {
|
|
219
|
-
command_type: "transfer-call",
|
|
220
|
-
transfer_message: message,
|
|
221
|
-
to_number: to_number,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private async sendCommand<C extends Command, Schema extends z.ZodType<C>>(
|
|
226
|
-
schema: Schema,
|
|
227
|
-
data: z.input<Schema>,
|
|
228
|
-
) {
|
|
229
|
-
const command = schema.parse(data);
|
|
230
|
-
this._commandQueue.push(command);
|
|
231
|
-
this.logger.debug(`Command queued: ${JSON.stringify(command)}`);
|
|
232
|
-
await this.flush();
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
private async flush() {
|
|
236
|
-
await this._drain?.call(this, this._commandQueue);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async onEvent(event: GuavaEvent) {
|
|
240
|
-
if (event.event_type == "caller-speech") {
|
|
241
|
-
this.onCallerSpeech(event);
|
|
242
|
-
} else if (event.event_type == "agent-speech") {
|
|
243
|
-
this.onAgentSpeech(event);
|
|
244
|
-
} else if (event.event_type == "agent-question") {
|
|
245
|
-
try {
|
|
246
|
-
this.logger.info(`Received question from bot: ${event.question}`);
|
|
247
|
-
const answer = await this.onQuestion(event.question);
|
|
248
|
-
await this.sendCommand(answerQuestionCommand, {
|
|
249
|
-
command_type: "answer-question",
|
|
250
|
-
question_id: event.question_id,
|
|
251
|
-
answer: answer,
|
|
252
|
-
});
|
|
253
|
-
} catch (e) {
|
|
254
|
-
this.logger.error("Error occured while answering question.");
|
|
255
|
-
await this.sendCommand(answerQuestionCommand, {
|
|
256
|
-
command_type: "answer-question",
|
|
257
|
-
question_id: event.question_id,
|
|
258
|
-
answer: "An error occured and the question could not be answered.",
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
} else if (event.event_type == "intent") {
|
|
262
|
-
this.logger.info(
|
|
263
|
-
`Received intent ${event.intent_id} from bot: ${event.intent_summary}`,
|
|
264
|
-
);
|
|
265
|
-
const intent_response = await this.onIntent(event.intent_summary);
|
|
266
|
-
if (intent_response) {
|
|
267
|
-
const response_str = `Responding to intent ${event.intent_id}: ${intent_response}`;
|
|
268
|
-
this.logger.info(response_str);
|
|
269
|
-
this.sendInstruction(intent_response);
|
|
270
|
-
}
|
|
271
|
-
} else if (event.event_type == "task-done") {
|
|
272
|
-
// ignore obsolete task_completed events
|
|
273
|
-
if (event.task_id == this._current_task_id) {
|
|
274
|
-
// assertion is implied
|
|
275
|
-
const on_complete = this._on_complete_current_task;
|
|
276
|
-
this._on_complete_current_task = undefined;
|
|
277
|
-
if (on_complete) {
|
|
278
|
-
on_complete();
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
} else if (event.event_type == "action-item-done") {
|
|
282
|
-
this._fieldValues[event.key] = event.payload;
|
|
283
|
-
if (event.key && event.payload) {
|
|
284
|
-
this.logger.info(
|
|
285
|
-
`Field ${event.key} updated with value: ${event.payload}`,
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
} else if (event.event_type == "inbound-call") {
|
|
289
|
-
this.onIncomingCall(event.caller_number);
|
|
290
|
-
} else if (
|
|
291
|
-
event.event_type == "outbound-call-connected" ||
|
|
292
|
-
event.event_type == "bot-session-ended"
|
|
293
|
-
) {
|
|
294
|
-
// no-op, don't warn
|
|
295
|
-
} else if (event.event_type == "error") {
|
|
296
|
-
this.logger.error(`The Guava agent reported an error: ${event.content}`);
|
|
297
|
-
} else {
|
|
298
|
-
this.logger.warn(`Unhandled event: ${JSON.stringify(event)}`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// callbacks
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* @abstract
|
|
306
|
-
* @description called when an inbound call is received. The overriding function must start
|
|
307
|
-
* with `await super.onIncomingCall(from_number)`
|
|
308
|
-
*/
|
|
309
|
-
async onIncomingCall(from_number?: string) {
|
|
310
|
-
await this.onCallStart();
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* @abstract
|
|
315
|
-
* @description called when a call is connected by the API, whether inbound or outbound
|
|
316
|
-
*/
|
|
317
|
-
async onCallStart(): Promise<void> {}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* @abstract
|
|
321
|
-
* @description called when the caller speaks to the agent.
|
|
322
|
-
*/
|
|
323
|
-
async onCallerSpeech(event: CallerSpeechEvent) {}
|
|
324
|
-
/**
|
|
325
|
-
* @abstract
|
|
326
|
-
* @description called when the agent speaks to the caller.
|
|
327
|
-
*/
|
|
328
|
-
async onAgentSpeech(event: AgentSpeechEvent) {}
|
|
329
|
-
/**
|
|
330
|
-
* @abstract
|
|
331
|
-
* @description called when the caller expresses a task they wish to execute
|
|
332
|
-
*/
|
|
333
|
-
async onIntent(intent: string): Promise<string | null> {
|
|
334
|
-
return "Unfortunately I'm not able to help with that.";
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* @abstract
|
|
338
|
-
* @description called when the agent needs to respond to a question that it doesn't know
|
|
339
|
-
* the answer to.
|
|
340
|
-
*/
|
|
341
|
-
async onQuestion(question: string): Promise<string> {
|
|
342
|
-
return "I don't have an answer to that question.";
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
export type InboundConnection =
|
|
347
|
-
| { agent_number: string }
|
|
348
|
-
| { webrtc_code: string };
|
|
349
|
-
|
|
350
|
-
export type TaskObjective =
|
|
351
|
-
| { objective: string }
|
|
352
|
-
| { objective?: string; checklist: (Field | Say | string)[] };
|
|
29
|
+
export type InboundConnection = { agent_number: string } | { webrtc_code: string };
|
|
353
30
|
|
|
354
31
|
const http_start = /^http:\/\//;
|
|
355
32
|
const https_start = /^https:\/\//;
|
|
33
|
+
|
|
34
|
+
@telemetryClient.trackClass()
|
|
356
35
|
export class Client {
|
|
357
36
|
private _apiKey: string;
|
|
358
37
|
private _baseUrl: string;
|
|
@@ -361,21 +40,19 @@ export class Client {
|
|
|
361
40
|
private _controller?: CallController;
|
|
362
41
|
private messageHandler?: (_: WebSocket.MessageEvent) => void;
|
|
363
42
|
|
|
364
|
-
constructor(apiKey?: string, baseUrl?: string, logger?: Logger) {
|
|
43
|
+
constructor(apiKey?: string, baseUrl?: string, logger?: Logger, captureWarnings: boolean = true) {
|
|
365
44
|
// Set up the default logger.
|
|
366
45
|
if (logger) {
|
|
367
46
|
this._logger = logger;
|
|
368
47
|
} else {
|
|
369
|
-
this._logger =
|
|
48
|
+
this._logger = getDefaultLogger();
|
|
370
49
|
}
|
|
371
50
|
|
|
372
51
|
// Resolve the API base URL.
|
|
373
52
|
if (baseUrl) {
|
|
374
53
|
this._baseUrl = baseUrl;
|
|
375
|
-
} else if (process.env.GUAVA_BASE_URL) {
|
|
376
|
-
this._baseUrl = process.env.GUAVA_BASE_URL;
|
|
377
54
|
} else {
|
|
378
|
-
this._baseUrl =
|
|
55
|
+
this._baseUrl = getBaseUrl();
|
|
379
56
|
}
|
|
380
57
|
|
|
381
58
|
// Resolve the API key.
|
|
@@ -388,6 +65,19 @@ export class Client {
|
|
|
388
65
|
"Guava API key must be provided either as argument to client constructor, or in environment variable GUAVA_API_KEY.",
|
|
389
66
|
);
|
|
390
67
|
}
|
|
68
|
+
|
|
69
|
+
if (!firstClient) {
|
|
70
|
+
firstClient = true;
|
|
71
|
+
|
|
72
|
+
if (captureWarnings) {
|
|
73
|
+
process.on("warning", (warning) => {
|
|
74
|
+
this._logger.warn(warning.toString());
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
telemetryClient.setSdkHeaders(this.headers());
|
|
79
|
+
this._checkSdkDeprecation();
|
|
80
|
+
}
|
|
391
81
|
}
|
|
392
82
|
|
|
393
83
|
private getWebsocketBase() {
|
|
@@ -410,37 +100,66 @@ export class Client {
|
|
|
410
100
|
"x-guava-platform": os.platform(),
|
|
411
101
|
"x-guava-runtime": process.release.name,
|
|
412
102
|
"x-guava-runtime-version": process.version,
|
|
413
|
-
"x-guava-sdk":
|
|
103
|
+
"x-guava-sdk": SDK_NAME,
|
|
414
104
|
"x-guava-sdk-version": pkgdata.version,
|
|
415
105
|
};
|
|
416
106
|
}
|
|
417
107
|
|
|
108
|
+
private async _checkSdkDeprecation() {
|
|
109
|
+
this._logger.debug(`Checking deprecation for SDK ${SDK_NAME}, ${pkgdata.version}.`);
|
|
110
|
+
try {
|
|
111
|
+
const url = new URL("v1/check-sdk-deprecation", this.getHttpBase());
|
|
112
|
+
url.searchParams.set("sdk_name", SDK_NAME);
|
|
113
|
+
url.searchParams.set("sdk_version", pkgdata.version);
|
|
114
|
+
const response = await fetchOrThrow(url, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: this.headers(),
|
|
117
|
+
});
|
|
118
|
+
const body = (await response.json()) as { deprecation_status: string };
|
|
119
|
+
if (body.deprecation_status === "supported") {
|
|
120
|
+
this._logger.info("SDK version still supported.");
|
|
121
|
+
} else if (body.deprecation_status === "deprecated") {
|
|
122
|
+
process.emitWarning(
|
|
123
|
+
"This SDK version is deprecated. Please update to a newer version of the SDK.",
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
this._logger.warn("SDK deprecation status unknown.");
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
this._logger.error("Encountered issue while checking for deprecation.");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
418
133
|
/**
|
|
419
134
|
* @description use the Guava API to call out to a number
|
|
420
135
|
*/
|
|
421
|
-
createOutbound(
|
|
422
|
-
fromNumber: string | undefined,
|
|
423
|
-
toNumber: string,
|
|
424
|
-
callControllerFactory?: (logger: Logger) => CallController,
|
|
425
|
-
) {
|
|
136
|
+
createOutbound(fromNumber: string | undefined, toNumber: string, callController: CallController) {
|
|
426
137
|
const url = new URL("v1/create-outbound", this.getWebsocketBase());
|
|
427
138
|
const ws = new WebSocket(url, {
|
|
428
139
|
headers: this.headers(),
|
|
429
140
|
});
|
|
430
|
-
const callController = (callControllerFactory ?? ((_) => undefined))(
|
|
431
|
-
this._logger,
|
|
432
|
-
);
|
|
433
141
|
|
|
434
142
|
ws.addEventListener("open", async (_ev) => {
|
|
435
143
|
ws.send(
|
|
436
|
-
stringifyZod(
|
|
144
|
+
stringifyZod(StartOutboundCallCommand, {
|
|
437
145
|
command_type: "start-outbound",
|
|
438
146
|
to_number: toNumber,
|
|
439
147
|
from_number: fromNumber,
|
|
440
148
|
}),
|
|
441
149
|
);
|
|
442
|
-
|
|
150
|
+
|
|
151
|
+
// set the callController drain function to send all commands
|
|
152
|
+
// through the now open websocket
|
|
153
|
+
callController.setDrain(async (commands) => {
|
|
154
|
+
for (const command of commands.splice(0)) {
|
|
155
|
+
this._logger.debug(`Sending command ${JSON.stringify(command)}`);
|
|
156
|
+
ws.send(JSON.stringify(command));
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await callController.onCallStart();
|
|
443
161
|
});
|
|
162
|
+
|
|
444
163
|
ws.addEventListener("close", (_ev) => {
|
|
445
164
|
// we are closing the socket, so don't trigger any other listeners
|
|
446
165
|
ws.removeAllListeners();
|
|
@@ -451,15 +170,6 @@ export class Client {
|
|
|
451
170
|
this._ws = ws;
|
|
452
171
|
this._controller = callController;
|
|
453
172
|
this.replaceHandler(this.uninitializedOutbound.bind(this));
|
|
454
|
-
|
|
455
|
-
// set the callController drain function to send all commands
|
|
456
|
-
// through the websocket
|
|
457
|
-
callController?.setDrain(async (commands) => {
|
|
458
|
-
for (const command of commands.splice(0)) {
|
|
459
|
-
this._logger.debug(`Sending command ${JSON.stringify(command)}`);
|
|
460
|
-
ws.send(JSON.stringify(command));
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
173
|
}
|
|
464
174
|
|
|
465
175
|
private replaceHandler(newHandler?: (_: WebSocket.MessageEvent) => void) {
|
|
@@ -481,14 +191,12 @@ export class Client {
|
|
|
481
191
|
}
|
|
482
192
|
|
|
483
193
|
const session_started = z
|
|
484
|
-
.union([
|
|
194
|
+
.union([SessionStartedEvent, ErrorEvent])
|
|
485
195
|
.parse(JSON.parse(ev.data.toString("utf8")));
|
|
486
|
-
if (session_started.event_type
|
|
196
|
+
if (session_started.event_type === "error") {
|
|
487
197
|
throw new Error(`Outbound call failed: ${session_started.content}`);
|
|
488
198
|
} else {
|
|
489
|
-
this._logger.info(
|
|
490
|
-
`Started session with ID: ${session_started.session_id}`,
|
|
491
|
-
);
|
|
199
|
+
this._logger.info(`Started session with ID: ${session_started.session_id}`);
|
|
492
200
|
// move to next state
|
|
493
201
|
this.replaceHandler(this.initializedOutbound.bind(this));
|
|
494
202
|
}
|
|
@@ -506,38 +214,13 @@ export class Client {
|
|
|
506
214
|
if (this._controller) {
|
|
507
215
|
await this._controller.onEvent(event);
|
|
508
216
|
}
|
|
509
|
-
if (
|
|
510
|
-
event.event_type == "outbound-call-failed" ||
|
|
511
|
-
event.event_type == "bot-session-ended"
|
|
512
|
-
) {
|
|
217
|
+
if (event.event_type === "outbound-call-failed" || event.event_type === "bot-session-ended") {
|
|
513
218
|
// shutdown the websocket
|
|
514
219
|
this._ws.close();
|
|
515
220
|
}
|
|
516
221
|
}
|
|
517
222
|
}
|
|
518
223
|
|
|
519
|
-
async setInboundHandler(
|
|
520
|
-
agent_number: string,
|
|
521
|
-
public_url: string,
|
|
522
|
-
inbound_token: string,
|
|
523
|
-
) {
|
|
524
|
-
const response = await fetch(
|
|
525
|
-
new URL(`v1/inbound-handler/${agent_number}`, this.getHttpBase()),
|
|
526
|
-
{
|
|
527
|
-
method: "PUT",
|
|
528
|
-
headers: this.headers(),
|
|
529
|
-
body: JSON.stringify({
|
|
530
|
-
handler_url: public_url,
|
|
531
|
-
handler_token: inbound_token,
|
|
532
|
-
}),
|
|
533
|
-
},
|
|
534
|
-
);
|
|
535
|
-
|
|
536
|
-
if (!response.ok) {
|
|
537
|
-
throw new Error("Failed to set inbound handler");
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
224
|
/**
|
|
542
225
|
* @description use the Guava API to receive calls at a given number
|
|
543
226
|
*/
|
|
@@ -563,16 +246,13 @@ export class Client {
|
|
|
563
246
|
this._logger.info(`Listening for calls to ${agent_number ?? webrtc_code}`);
|
|
564
247
|
|
|
565
248
|
if (webrtc_code) {
|
|
566
|
-
const debugurl = new URL(
|
|
567
|
-
`debug-webrtc?webrtc_code=${webrtc_code}`,
|
|
568
|
-
this.getHttpBase(),
|
|
569
|
-
);
|
|
249
|
+
const debugurl = new URL(`debug-webrtc?webrtc_code=${webrtc_code}`, this.getHttpBase());
|
|
570
250
|
this._logger.debug(`WebRTC DebugURL: ${debugurl}`);
|
|
571
251
|
}
|
|
572
252
|
|
|
573
253
|
ws.addEventListener("open", (_ev) => {
|
|
574
254
|
ws.send(
|
|
575
|
-
stringifyZod(
|
|
255
|
+
stringifyZod(ListenInboundCommand, {
|
|
576
256
|
command_type: "listen-inbound",
|
|
577
257
|
agent_number: agent_number,
|
|
578
258
|
webrtc_code: webrtc_code,
|
|
@@ -585,9 +265,7 @@ export class Client {
|
|
|
585
265
|
});
|
|
586
266
|
|
|
587
267
|
ws.addEventListener("message", (ev) => {
|
|
588
|
-
const tunnel_event =
|
|
589
|
-
JSON.parse(ev.data.toString("utf8")),
|
|
590
|
-
);
|
|
268
|
+
const tunnel_event = InboundTunnelEvent.parse(JSON.parse(ev.data.toString("utf8")));
|
|
591
269
|
if (!(tunnel_event.call_id in callControllers)) {
|
|
592
270
|
this._logger.info(
|
|
593
271
|
`Received tunnel event for new call ID: ${tunnel_event.call_id}. Creating call controller.`,
|
|
@@ -600,7 +278,7 @@ export class Client {
|
|
|
600
278
|
`Sending command: ${JSON.stringify(command)} for call ID: ${tunnel_event.call_id}`,
|
|
601
279
|
);
|
|
602
280
|
ws.send(
|
|
603
|
-
stringifyZod(
|
|
281
|
+
stringifyZod(InboundTunnelCommand, {
|
|
604
282
|
call_id: tunnel_event.call_id,
|
|
605
283
|
command,
|
|
606
284
|
}),
|
package/src/logging.ts
CHANGED
|
@@ -8,6 +8,15 @@ const LOG_RANKS: Record<LogLevel, number> = {
|
|
|
8
8
|
debug: 4,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
const ANSI_RESET = "\x1b[0m";
|
|
12
|
+
const LEVEL_COLORS: Record<LogLevel, string> = {
|
|
13
|
+
off: "",
|
|
14
|
+
debug: "\x1b[38;5;245m", // gray
|
|
15
|
+
info: "",
|
|
16
|
+
warn: "\x1b[38;5;214m", // orange
|
|
17
|
+
error: "\x1b[38;5;196m", // red
|
|
18
|
+
};
|
|
19
|
+
|
|
11
20
|
export interface Logger {
|
|
12
21
|
debug(format: string, ...args: unknown[]);
|
|
13
22
|
info(format: string, ...args: unknown[]);
|
|
@@ -16,10 +25,10 @@ export interface Logger {
|
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
function shouldLog(messageLevel: LogLevel, loggerLevel: LogLevel) {
|
|
19
|
-
if (!
|
|
28
|
+
if (!Object.hasOwn(LOG_RANKS, messageLevel)) {
|
|
20
29
|
throw new Error(`Invalid log level: ${String(messageLevel)}`);
|
|
21
30
|
}
|
|
22
|
-
if (!
|
|
31
|
+
if (!Object.hasOwn(LOG_RANKS, loggerLevel)) {
|
|
23
32
|
throw new Error(`Invalid logger level: ${String(loggerLevel)}`);
|
|
24
33
|
}
|
|
25
34
|
|
|
@@ -28,11 +37,34 @@ function shouldLog(messageLevel: LogLevel, loggerLevel: LogLevel) {
|
|
|
28
37
|
|
|
29
38
|
function noop(format: string, ...args: unknown[]) {}
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
function makeColoredMethod(
|
|
41
|
+
fn: (...args: unknown[]) => void,
|
|
42
|
+
level: LogLevel,
|
|
43
|
+
useColor: boolean,
|
|
44
|
+
): (format: string, ...args: unknown[]) => void {
|
|
45
|
+
if (!useColor) return fn.bind(console);
|
|
46
|
+
return (format: string, ...args: unknown[]) =>
|
|
47
|
+
fn(`${LEVEL_COLORS[level]}[${level}] ${format}${ANSI_RESET}`, ...args);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getConsoleLogger(loggerLevel: LogLevel, useColor = false): Logger {
|
|
32
51
|
return {
|
|
33
|
-
debug: shouldLog("debug", loggerLevel)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
debug: shouldLog("debug", loggerLevel)
|
|
53
|
+
? makeColoredMethod(console.debug, "debug", useColor)
|
|
54
|
+
: noop,
|
|
55
|
+
info: shouldLog("info", loggerLevel) ? makeColoredMethod(console.info, "info", useColor) : noop,
|
|
56
|
+
warn: shouldLog("warn", loggerLevel) ? makeColoredMethod(console.warn, "warn", useColor) : noop,
|
|
57
|
+
error: shouldLog("error", loggerLevel)
|
|
58
|
+
? makeColoredMethod(console.error, "error", useColor)
|
|
59
|
+
: noop,
|
|
37
60
|
};
|
|
38
61
|
}
|
|
62
|
+
|
|
63
|
+
export function getDefaultLogger(): Logger {
|
|
64
|
+
const level = (process.env.LOG_LEVEL ?? "info").toLowerCase();
|
|
65
|
+
if (!Object.hasOwn(LOG_RANKS, level)) {
|
|
66
|
+
throw new Error(`Unknown log level in LOG_LEVEL env var: ${level}`);
|
|
67
|
+
}
|
|
68
|
+
const useColor = process.stderr.isTTY === true && !process.env.NO_COLOR;
|
|
69
|
+
return getConsoleLogger(level as LogLevel, useColor);
|
|
70
|
+
}
|