@guava-ai/guava-sdk 0.1.0 → 0.3.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 +19 -0
- package/dist/examples/credit-card-activation.d.ts +1 -0
- package/dist/examples/credit-card-activation.js +162 -0
- package/dist/examples/credit-card-activation.js.map +1 -0
- package/dist/examples/property-insurance.js +6 -2
- package/dist/examples/property-insurance.js.map +1 -1
- package/dist/examples/thai-palace.d.ts +1 -0
- package/dist/examples/thai-palace.js +79 -0
- package/dist/examples/thai-palace.js.map +1 -0
- package/dist/package.json +1 -1
- package/dist/src/action_item.d.ts +2 -2
- package/dist/src/action_item.js +1 -1
- package/dist/src/action_item.js.map +1 -1
- package/dist/src/commands.d.ts +9 -9
- package/dist/src/commands.js +5 -4
- package/dist/src/commands.js.map +1 -1
- package/dist/src/example_data.js.map +1 -1
- package/dist/src/helpers/openai.d.ts +6 -1
- package/dist/src/helpers/openai.js +24 -4
- package/dist/src/helpers/openai.js.map +1 -1
- package/dist/src/index.d.ts +105 -9
- package/dist/src/index.js +197 -53
- package/dist/src/index.js.map +1 -1
- package/examples/credit-card-activation.ts +188 -0
- package/examples/property-insurance.ts +11 -7
- package/examples/thai-palace.ts +74 -0
- package/package.json +2 -2
- package/src/action_item.ts +5 -2
- package/src/commands.ts +11 -5
- package/src/example_data.ts +1 -1
- package/src/helpers/openai.ts +35 -7
- package/src/index.ts +246 -78
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
rejectInboundCallCommand,
|
|
13
13
|
transferCommand,
|
|
14
14
|
listenInboundCommand,
|
|
15
|
+
inboundTunnelCommand,
|
|
15
16
|
} from "./commands.ts";
|
|
16
17
|
import * as z from "zod";
|
|
17
18
|
import {
|
|
@@ -21,13 +22,14 @@ import {
|
|
|
21
22
|
decodeEvent,
|
|
22
23
|
type CallerSpeechEvent,
|
|
23
24
|
type AgentSpeechEvent,
|
|
25
|
+
inboundTunnelEvent,
|
|
24
26
|
} from "./events.ts";
|
|
25
27
|
import type { ActionItem, Field, Say, Todo } from "./action_item.ts";
|
|
26
28
|
import pkgdata from "../package.json" with { type: "json" };
|
|
27
29
|
import os from "node:os";
|
|
28
30
|
|
|
29
|
-
const DEFAULT_BASE_URL = "
|
|
30
|
-
const DEFAULT_LOG_LEVEL = "
|
|
31
|
+
const DEFAULT_BASE_URL = "https://guava-dev.gridspace.com/";
|
|
32
|
+
const DEFAULT_LOG_LEVEL = "debug";
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* @description convenience function for stringifying data according to a schema
|
|
@@ -39,33 +41,53 @@ function stringifyZod<Schema extends z.ZodType>(
|
|
|
39
41
|
return JSON.stringify(schema.parse(data));
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Interface between Guava services and user-supplied code
|
|
46
|
+
*/
|
|
42
47
|
export class CallController {
|
|
43
48
|
private _commandQueue: Command[] = [];
|
|
44
49
|
private _on_complete_current_task?: () => void;
|
|
45
50
|
// private _field_values: Record<string, any>;
|
|
46
51
|
private _current_task_id?: string;
|
|
47
|
-
|
|
52
|
+
/**
|
|
53
|
+
* @protected
|
|
54
|
+
* @description logger used to emit diagnostics
|
|
55
|
+
*/
|
|
56
|
+
protected logger: Logger;
|
|
48
57
|
// drain functions are expected to cleanup
|
|
49
58
|
// the part of the queue that is successfully sent from its
|
|
50
59
|
// input (mutating it) (i.e. _drain should use Array.splice)
|
|
51
60
|
private _drain?: (_: Command[]) => Promise<void>;
|
|
61
|
+
private _fieldValues: Record<string, unknown> = {};
|
|
52
62
|
|
|
53
63
|
constructor(logger: Logger) {
|
|
54
64
|
// Set up the default logger.
|
|
55
|
-
this.
|
|
65
|
+
this.logger = logger;
|
|
56
66
|
}
|
|
57
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
|
+
*/
|
|
58
74
|
setDrain(newDrain: (_: Command[]) => Promise<void>) {
|
|
59
75
|
this._drain = newDrain;
|
|
60
76
|
this.flush();
|
|
61
77
|
}
|
|
62
78
|
|
|
79
|
+
/**
|
|
80
|
+
* @description [inbound] receive a call, and process further.
|
|
81
|
+
*/
|
|
63
82
|
protected async acceptCall() {
|
|
64
83
|
await this.sendCommand(acceptInboundCallCommand, {
|
|
65
84
|
command_type: "accept-inbound",
|
|
66
85
|
});
|
|
67
86
|
}
|
|
68
87
|
|
|
88
|
+
/**
|
|
89
|
+
* @description read a span of text verbatim
|
|
90
|
+
*/
|
|
69
91
|
protected async readScript(script: string) {
|
|
70
92
|
await this.sendCommand(readScriptCommand, {
|
|
71
93
|
command_type: "read-script",
|
|
@@ -73,16 +95,22 @@ export class CallController {
|
|
|
73
95
|
});
|
|
74
96
|
}
|
|
75
97
|
|
|
98
|
+
/**
|
|
99
|
+
* @description [inbound] reject a call
|
|
100
|
+
*/
|
|
76
101
|
protected async rejectCall() {
|
|
77
102
|
await this.sendCommand(rejectInboundCallCommand, {
|
|
78
103
|
command_type: "reject-inbound",
|
|
79
104
|
});
|
|
80
105
|
}
|
|
81
106
|
|
|
82
|
-
protected async addInfo(
|
|
107
|
+
protected async addInfo(_info: string) {
|
|
83
108
|
throw new Error("not implemeneted");
|
|
84
109
|
}
|
|
85
110
|
|
|
111
|
+
/**
|
|
112
|
+
* @description read a span of text non-verbatim
|
|
113
|
+
*/
|
|
86
114
|
protected async sendInstruction(instruction: string) {
|
|
87
115
|
await this.sendCommand(sendInstructionCommand, {
|
|
88
116
|
command_type: "send-instruction",
|
|
@@ -90,49 +118,87 @@ export class CallController {
|
|
|
90
118
|
});
|
|
91
119
|
}
|
|
92
120
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
}) {
|
|
98
129
|
await this.sendCommand(setPersona, {
|
|
99
130
|
command_type: "set-persona",
|
|
100
|
-
organization_name:
|
|
101
|
-
agent_name:
|
|
102
|
-
agent_purpose:
|
|
131
|
+
organization_name: args.organizationName,
|
|
132
|
+
agent_name: args.agentName,
|
|
133
|
+
agent_purpose: args.agentPurpose,
|
|
103
134
|
});
|
|
104
135
|
}
|
|
105
136
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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[]
|
|
111
147
|
) {
|
|
112
|
-
if (!objective && !checklist) {
|
|
113
|
-
throw new Error(
|
|
114
|
-
"At least one of args ['objective','checklist'] must be provided.",
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
148
|
this._current_task_id = Math.random().toString(16).substring(2, 8);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
});
|
|
133
184
|
});
|
|
134
185
|
}
|
|
135
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
|
+
*/
|
|
136
202
|
protected async hangup(final_instructions: string = "") {
|
|
137
203
|
let instructions: string;
|
|
138
204
|
if (final_instructions) {
|
|
@@ -144,6 +210,9 @@ export class CallController {
|
|
|
144
210
|
this.sendInstruction(instructions);
|
|
145
211
|
}
|
|
146
212
|
|
|
213
|
+
/**
|
|
214
|
+
* @description transfer an accepted call
|
|
215
|
+
*/
|
|
147
216
|
protected transfer(to_number: string, transfer_message?: string) {
|
|
148
217
|
const message = transfer_message ?? "I'm transferring you now";
|
|
149
218
|
this.sendCommand(transferCommand, {
|
|
@@ -159,7 +228,7 @@ export class CallController {
|
|
|
159
228
|
) {
|
|
160
229
|
const command = schema.parse(data);
|
|
161
230
|
this._commandQueue.push(command);
|
|
162
|
-
this.
|
|
231
|
+
this.logger.debug(`Command queued: ${JSON.stringify(command)}`);
|
|
163
232
|
await this.flush();
|
|
164
233
|
}
|
|
165
234
|
|
|
@@ -174,7 +243,7 @@ export class CallController {
|
|
|
174
243
|
this.onAgentSpeech(event);
|
|
175
244
|
} else if (event.event_type == "agent-question") {
|
|
176
245
|
try {
|
|
177
|
-
this.
|
|
246
|
+
this.logger.info(`Received question from bot: ${event.question}`);
|
|
178
247
|
const answer = await this.onQuestion(event.question);
|
|
179
248
|
await this.sendCommand(answerQuestionCommand, {
|
|
180
249
|
command_type: "answer-question",
|
|
@@ -182,7 +251,7 @@ export class CallController {
|
|
|
182
251
|
answer: answer,
|
|
183
252
|
});
|
|
184
253
|
} catch (e) {
|
|
185
|
-
this.
|
|
254
|
+
this.logger.error("Error occured while answering question.");
|
|
186
255
|
await this.sendCommand(answerQuestionCommand, {
|
|
187
256
|
command_type: "answer-question",
|
|
188
257
|
question_id: event.question_id,
|
|
@@ -190,14 +259,14 @@ export class CallController {
|
|
|
190
259
|
});
|
|
191
260
|
}
|
|
192
261
|
} else if (event.event_type == "intent") {
|
|
193
|
-
this.
|
|
262
|
+
this.logger.info(
|
|
194
263
|
`Received intent ${event.intent_id} from bot: ${event.intent_summary}`,
|
|
195
264
|
);
|
|
196
265
|
const intent_response = await this.onIntent(event.intent_summary);
|
|
197
266
|
if (intent_response) {
|
|
198
|
-
const response_str = `Responding to intent ${event.intent_id}: ${
|
|
199
|
-
this.
|
|
200
|
-
this.sendInstruction(
|
|
267
|
+
const response_str = `Responding to intent ${event.intent_id}: ${intent_response}`;
|
|
268
|
+
this.logger.info(response_str);
|
|
269
|
+
this.sendInstruction(intent_response);
|
|
201
270
|
}
|
|
202
271
|
} else if (event.event_type == "task-done") {
|
|
203
272
|
// ignore obsolete task_completed events
|
|
@@ -210,7 +279,12 @@ export class CallController {
|
|
|
210
279
|
}
|
|
211
280
|
}
|
|
212
281
|
} else if (event.event_type == "action-item-done") {
|
|
213
|
-
|
|
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
|
+
}
|
|
214
288
|
} else if (event.event_type == "inbound-call") {
|
|
215
289
|
this.onIncomingCall(event.caller_number);
|
|
216
290
|
} else if (
|
|
@@ -219,28 +293,51 @@ export class CallController {
|
|
|
219
293
|
) {
|
|
220
294
|
// no-op, don't warn
|
|
221
295
|
} else if (event.event_type == "error") {
|
|
222
|
-
this.
|
|
296
|
+
this.logger.error(`The Guava agent reported an error: ${event.content}`);
|
|
223
297
|
} else {
|
|
224
|
-
this.
|
|
298
|
+
this.logger.warn(`Unhandled event: ${JSON.stringify(event)}`);
|
|
225
299
|
}
|
|
226
300
|
}
|
|
227
301
|
|
|
228
302
|
// callbacks
|
|
229
303
|
|
|
230
304
|
/**
|
|
231
|
-
* @
|
|
305
|
+
* @abstract
|
|
306
|
+
* @description called when an inbound call is received. The overriding function must start
|
|
307
|
+
* with `await super.onIncomingCall(from_number)`
|
|
232
308
|
*/
|
|
233
309
|
async onIncomingCall(from_number?: string) {
|
|
234
310
|
await this.onCallStart();
|
|
235
311
|
}
|
|
236
312
|
|
|
313
|
+
/**
|
|
314
|
+
* @abstract
|
|
315
|
+
* @description called when a call is connected by the API, whether inbound or outbound
|
|
316
|
+
*/
|
|
237
317
|
async onCallStart(): Promise<void> {}
|
|
238
318
|
|
|
319
|
+
/**
|
|
320
|
+
* @abstract
|
|
321
|
+
* @description called when the caller speaks to the agent.
|
|
322
|
+
*/
|
|
239
323
|
async onCallerSpeech(event: CallerSpeechEvent) {}
|
|
324
|
+
/**
|
|
325
|
+
* @abstract
|
|
326
|
+
* @description called when the agent speaks to the caller.
|
|
327
|
+
*/
|
|
240
328
|
async onAgentSpeech(event: AgentSpeechEvent) {}
|
|
329
|
+
/**
|
|
330
|
+
* @abstract
|
|
331
|
+
* @description called when the caller expresses a task they wish to execute
|
|
332
|
+
*/
|
|
241
333
|
async onIntent(intent: string): Promise<string | null> {
|
|
242
334
|
return "Unfortunately I'm not able to help with that.";
|
|
243
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
|
+
*/
|
|
244
341
|
async onQuestion(question: string): Promise<string> {
|
|
245
342
|
return "I don't have an answer to that question.";
|
|
246
343
|
}
|
|
@@ -250,8 +347,12 @@ export type InboundConnection =
|
|
|
250
347
|
| { agent_number: string }
|
|
251
348
|
| { webrtc_code: string };
|
|
252
349
|
|
|
253
|
-
|
|
254
|
-
|
|
350
|
+
export type TaskObjective =
|
|
351
|
+
| { objective: string }
|
|
352
|
+
| { objective?: string; checklist: (Field | Say | string)[] };
|
|
353
|
+
|
|
354
|
+
const http_start = /^http:\/\//;
|
|
355
|
+
const https_start = /^https:\/\//;
|
|
255
356
|
export class Client {
|
|
256
357
|
private _apiKey: string;
|
|
257
358
|
private _baseUrl: string;
|
|
@@ -289,17 +390,17 @@ export class Client {
|
|
|
289
390
|
}
|
|
290
391
|
}
|
|
291
392
|
|
|
292
|
-
private
|
|
293
|
-
if (
|
|
393
|
+
private getWebsocketBase() {
|
|
394
|
+
if (http_start.test(this._baseUrl)) {
|
|
294
395
|
return `ws://${this._baseUrl.substring("ws://".length)}`;
|
|
295
|
-
} else if (
|
|
396
|
+
} else if (https_start.test(this._baseUrl)) {
|
|
296
397
|
return `wss://${this._baseUrl.substring("wss://".length)}`;
|
|
297
398
|
} else {
|
|
298
399
|
throw new Error(`Invalid base URL: ${this._baseUrl}}`);
|
|
299
400
|
}
|
|
300
401
|
}
|
|
301
402
|
|
|
302
|
-
private
|
|
403
|
+
private getHttpBase() {
|
|
303
404
|
return this._baseUrl;
|
|
304
405
|
}
|
|
305
406
|
|
|
@@ -314,16 +415,21 @@ export class Client {
|
|
|
314
415
|
};
|
|
315
416
|
}
|
|
316
417
|
|
|
418
|
+
/**
|
|
419
|
+
* @description use the Guava API to call out to a number
|
|
420
|
+
*/
|
|
317
421
|
createOutbound(
|
|
318
422
|
fromNumber: string | undefined,
|
|
319
423
|
toNumber: string,
|
|
320
424
|
callControllerFactory?: (logger: Logger) => CallController,
|
|
321
425
|
) {
|
|
322
|
-
const url = new URL("create-outbound", this.getWebsocketBase())
|
|
426
|
+
const url = new URL("v1/create-outbound", this.getWebsocketBase());
|
|
323
427
|
const ws = new WebSocket(url, {
|
|
324
428
|
headers: this.headers(),
|
|
325
429
|
});
|
|
326
|
-
const callController = (callControllerFactory ?? ((_) => undefined))(
|
|
430
|
+
const callController = (callControllerFactory ?? ((_) => undefined))(
|
|
431
|
+
this._logger,
|
|
432
|
+
);
|
|
327
433
|
|
|
328
434
|
ws.addEventListener("open", async (_ev) => {
|
|
329
435
|
ws.send(
|
|
@@ -356,7 +462,7 @@ export class Client {
|
|
|
356
462
|
});
|
|
357
463
|
}
|
|
358
464
|
|
|
359
|
-
replaceHandler(newHandler?: (_: WebSocket.MessageEvent) => void) {
|
|
465
|
+
private replaceHandler(newHandler?: (_: WebSocket.MessageEvent) => void) {
|
|
360
466
|
if (this.messageHandler) {
|
|
361
467
|
this._ws?.removeEventListener("message", this.messageHandler);
|
|
362
468
|
}
|
|
@@ -386,8 +492,6 @@ export class Client {
|
|
|
386
492
|
// move to next state
|
|
387
493
|
this.replaceHandler(this.initializedOutbound.bind(this));
|
|
388
494
|
}
|
|
389
|
-
|
|
390
|
-
// this._controller?.flush();
|
|
391
495
|
}
|
|
392
496
|
|
|
393
497
|
private async initializedOutbound(ev: WebSocket.MessageEvent) {
|
|
@@ -410,26 +514,41 @@ export class Client {
|
|
|
410
514
|
this._ws.close();
|
|
411
515
|
}
|
|
412
516
|
}
|
|
413
|
-
|
|
414
|
-
// this._controller?.flush();
|
|
415
517
|
}
|
|
416
518
|
|
|
417
|
-
setInboundHandler(
|
|
519
|
+
async setInboundHandler(
|
|
418
520
|
agent_number: string,
|
|
419
521
|
public_url: string,
|
|
420
522
|
inbound_token: string,
|
|
421
523
|
) {
|
|
422
|
-
|
|
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
|
+
}
|
|
423
539
|
}
|
|
424
540
|
|
|
425
|
-
|
|
541
|
+
/**
|
|
542
|
+
* @description use the Guava API to receive calls at a given number
|
|
543
|
+
*/
|
|
426
544
|
listenInbound<U extends CallController>(
|
|
427
545
|
conn: InboundConnection,
|
|
428
|
-
|
|
546
|
+
controllerClassFactory: (logger: Logger) => U,
|
|
429
547
|
) {
|
|
430
|
-
const
|
|
548
|
+
const callControllers: Record<string, U> = {};
|
|
431
549
|
|
|
432
|
-
|
|
550
|
+
// return a way to *stop* listening
|
|
551
|
+
const url = new URL("v1/listen-inbound", this.getWebsocketBase());
|
|
433
552
|
const ws = new WebSocket(url, {
|
|
434
553
|
headers: this.headers(),
|
|
435
554
|
});
|
|
@@ -441,23 +560,72 @@ export class Client {
|
|
|
441
560
|
webrtc_code = conn.webrtc_code;
|
|
442
561
|
}
|
|
443
562
|
|
|
444
|
-
ws.send(
|
|
445
|
-
stringifyZod(listenInboundCommand, {
|
|
446
|
-
command_type: "listen-inbound",
|
|
447
|
-
agent_number: agent_number,
|
|
448
|
-
webrtc_code: webrtc_code,
|
|
449
|
-
}),
|
|
450
|
-
);
|
|
451
|
-
|
|
452
563
|
this._logger.info(`Listening for calls to ${agent_number ?? webrtc_code}`);
|
|
453
564
|
|
|
454
565
|
if (webrtc_code) {
|
|
455
|
-
// converted to print, but should be logger?
|
|
456
566
|
const debugurl = new URL(
|
|
457
567
|
`debug-webrtc?webrtc_code=${webrtc_code}`,
|
|
458
568
|
this.getHttpBase(),
|
|
459
569
|
);
|
|
460
|
-
this._logger.debug(`WebRTC DebugURL: ${
|
|
570
|
+
this._logger.debug(`WebRTC DebugURL: ${debugurl}`);
|
|
461
571
|
}
|
|
572
|
+
|
|
573
|
+
ws.addEventListener("open", (_ev) => {
|
|
574
|
+
ws.send(
|
|
575
|
+
stringifyZod(listenInboundCommand, {
|
|
576
|
+
command_type: "listen-inbound",
|
|
577
|
+
agent_number: agent_number,
|
|
578
|
+
webrtc_code: webrtc_code,
|
|
579
|
+
}),
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
ws.addEventListener("close", (_ev) => {
|
|
584
|
+
ws.removeAllListeners();
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
ws.addEventListener("message", (ev) => {
|
|
588
|
+
const tunnel_event = inboundTunnelEvent.parse(
|
|
589
|
+
JSON.parse(ev.data.toString("utf8")),
|
|
590
|
+
);
|
|
591
|
+
if (!(tunnel_event.call_id in callControllers)) {
|
|
592
|
+
this._logger.info(
|
|
593
|
+
`Received tunnel event for new call ID: ${tunnel_event.call_id}. Creating call controller.`,
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
const newController = controllerClassFactory(this._logger);
|
|
597
|
+
newController.setDrain(async (commands) => {
|
|
598
|
+
for (const command of commands.splice(0)) {
|
|
599
|
+
this._logger.debug(
|
|
600
|
+
`Sending command: ${JSON.stringify(command)} for call ID: ${tunnel_event.call_id}`,
|
|
601
|
+
);
|
|
602
|
+
ws.send(
|
|
603
|
+
stringifyZod(inboundTunnelCommand, {
|
|
604
|
+
call_id: tunnel_event.call_id,
|
|
605
|
+
command,
|
|
606
|
+
}),
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
callControllers[tunnel_event.call_id] = newController;
|
|
611
|
+
newController.onEvent(tunnel_event.event);
|
|
612
|
+
} else {
|
|
613
|
+
// no threading, so manually forward to onEvent!
|
|
614
|
+
callControllers[tunnel_event.call_id].onEvent(tunnel_event.event);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
return new InboundListener(ws);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
class InboundListener {
|
|
623
|
+
private ws: WebSocket;
|
|
624
|
+
constructor(ws: WebSocket) {
|
|
625
|
+
this.ws = ws;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
close() {
|
|
629
|
+
this.ws.close();
|
|
462
630
|
}
|
|
463
631
|
}
|