@guava-ai/guava-sdk 0.10.0 → 0.11.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 +1 -1
- package/bin/example-runner.ts +4 -4
- package/dist/bin/example-runner.js +4 -4
- package/dist/bin/example-runner.js.map +1 -1
- package/dist/examples/help-desk.js +71 -0
- package/dist/examples/help-desk.js.map +1 -0
- package/dist/examples/property-insurance.js +14 -22
- package/dist/examples/property-insurance.js.map +1 -1
- package/dist/examples/restaurant-waitlist.js +74 -0
- package/dist/examples/restaurant-waitlist.js.map +1 -0
- package/dist/examples/scheduling-outbound.js +30 -54
- package/dist/examples/scheduling-outbound.js.map +1 -1
- package/dist/src/{action_item.d.ts → action-item.d.ts} +2 -0
- package/dist/src/{action_item.js → action-item.js} +3 -1
- package/dist/src/action-item.js.map +1 -0
- package/dist/src/agent.d.ts +72 -0
- package/dist/src/agent.js +444 -0
- package/dist/src/agent.js.map +1 -0
- package/dist/src/call-controller.d.ts +1 -1
- package/dist/src/call-controller.js +1 -1
- package/dist/src/call.d.ts +73 -0
- package/dist/src/call.js +252 -0
- package/dist/src/call.js.map +1 -0
- package/dist/src/commands.d.ts +95 -0
- package/dist/src/commands.js +24 -2
- package/dist/src/commands.js.map +1 -1
- package/dist/src/events.d.ts +25 -0
- package/dist/src/events.js +12 -1
- package/dist/src/events.js.map +1 -1
- package/dist/src/example-data.d.ts +1 -0
- package/dist/src/example-data.js +41 -1
- package/dist/src/example-data.js.map +1 -1
- package/dist/src/index.d.ts +14 -5
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/logging.js +1 -1
- package/dist/src/logging.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/examples/README.md +6 -0
- package/examples/help-desk.ts +60 -0
- package/examples/property-insurance.ts +14 -30
- package/examples/restaurant-waitlist.ts +47 -0
- package/examples/scheduling-outbound.ts +40 -76
- package/package.json +2 -1
- package/src/{action_item.ts → action-item.ts} +3 -0
- package/src/agent.ts +439 -0
- package/src/call-controller.ts +1 -1
- package/src/call.ts +279 -0
- package/src/commands.ts +31 -1
- package/src/events.ts +15 -0
- package/src/example-data.ts +41 -0
- package/src/index.ts +7 -5
- package/src/logging.ts +1 -1
- package/src/version.ts +1 -1
- package/dist/examples/credit-card-activation.js +0 -177
- package/dist/examples/credit-card-activation.js.map +0 -1
- package/dist/examples/thai-palace.js +0 -94
- package/dist/examples/thai-palace.js.map +0 -1
- package/dist/src/action_item.js.map +0 -1
- package/examples/credit-card-activation.ts +0 -178
- package/examples/thai-palace.ts +0 -67
- /package/dist/examples/{credit-card-activation.d.ts → help-desk.d.ts} +0 -0
- /package/dist/examples/{thai-palace.d.ts → restaurant-waitlist.d.ts} +0 -0
package/src/agent.ts
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { Call, Client } from "./index.ts";
|
|
3
|
+
import { getDefaultLogger, type Logger } from "./logging.ts";
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
import {
|
|
6
|
+
ListenInboundCommand,
|
|
7
|
+
InboundTunnelCommand,
|
|
8
|
+
AnswerQuestionCommand,
|
|
9
|
+
ChoiceResultCommand,
|
|
10
|
+
RegisteredHooksCommand,
|
|
11
|
+
AcceptInboundCallCommand,
|
|
12
|
+
RejectInboundCallCommand,
|
|
13
|
+
StartOutboundCallCommand,
|
|
14
|
+
ActionSuggestionCommand,
|
|
15
|
+
} from "./commands.ts";
|
|
16
|
+
import {
|
|
17
|
+
type GuavaEvent,
|
|
18
|
+
type CallerSpeechEvent,
|
|
19
|
+
type AgentSpeechEvent,
|
|
20
|
+
InboundTunnelEvent,
|
|
21
|
+
ErrorEvent,
|
|
22
|
+
SessionStartedEvent,
|
|
23
|
+
decodeEvent,
|
|
24
|
+
} from "./events.ts";
|
|
25
|
+
import { telemetryClient } from "./telemetry.ts";
|
|
26
|
+
|
|
27
|
+
export interface CallInfo {
|
|
28
|
+
caller_number?: string;
|
|
29
|
+
agent_number?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type IncomingCallAction = { action: "accept" } | { action: "decline" };
|
|
33
|
+
|
|
34
|
+
export interface SuggestedAction {
|
|
35
|
+
key: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @description convenience function for stringifying data according to a schema
|
|
41
|
+
*/
|
|
42
|
+
function stringifyZod<Schema extends z.ZodType>(schema: Schema, data: z.input<Schema>): string {
|
|
43
|
+
return JSON.stringify(schema.parse(data));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type InboundConnection = { agent_number: string } | { webrtc_code: string };
|
|
47
|
+
|
|
48
|
+
@telemetryClient.trackClass()
|
|
49
|
+
export class Agent {
|
|
50
|
+
private _name?: string;
|
|
51
|
+
private _organization?: string;
|
|
52
|
+
private _purpose?: string;
|
|
53
|
+
private _logger: Logger;
|
|
54
|
+
|
|
55
|
+
private _client: Client = new Client();
|
|
56
|
+
|
|
57
|
+
private _onCallReceived: (callInfo: CallInfo) => Promise<IncomingCallAction> = async () => ({
|
|
58
|
+
action: "accept",
|
|
59
|
+
});
|
|
60
|
+
private _onCallStart?: (call: Call) => Promise<void>;
|
|
61
|
+
private _onCallerSpeech?: (call: Call, event: CallerSpeechEvent) => Promise<void>;
|
|
62
|
+
private _onAgentSpeech?: (call: Call, event: AgentSpeechEvent) => Promise<void>;
|
|
63
|
+
private _onQuestion?: (call: Call, question: string) => Promise<string>;
|
|
64
|
+
private _onTaskCompleteGeneric?: (call: Call, taskId: string) => Promise<void>;
|
|
65
|
+
private _onTaskCompleteHandlers: Record<string, (call: Call) => Promise<void>> = {};
|
|
66
|
+
private _searchQueryHandlers: Record<
|
|
67
|
+
string,
|
|
68
|
+
(call: Call, query: string) => Promise<[string[], string[]]>
|
|
69
|
+
> = {};
|
|
70
|
+
private _onActionRequested?: (
|
|
71
|
+
call: Call,
|
|
72
|
+
intentSummary: string,
|
|
73
|
+
) => Promise<SuggestedAction | undefined>;
|
|
74
|
+
private _onActionGeneric?: (call: Call, actionKey: string) => Promise<void>;
|
|
75
|
+
private _onActionHandlers: Record<string, (call: Call) => Promise<void>> = {};
|
|
76
|
+
private _onSessionEnd?: (call: Call) => Promise<void>;
|
|
77
|
+
|
|
78
|
+
constructor(args?: { name?: string; organization?: string; purpose?: string }) {
|
|
79
|
+
this._name = args?.name;
|
|
80
|
+
this._organization = args?.organization;
|
|
81
|
+
this._purpose = args?.purpose;
|
|
82
|
+
this._logger = getDefaultLogger();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
onCallReceived(callback: (callInfo: CallInfo) => Promise<IncomingCallAction>): void {
|
|
86
|
+
this._onCallReceived = callback;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
onCallStart(callback: (call: Call) => Promise<void>): void {
|
|
90
|
+
this._onCallStart = callback;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onCallerSpeech(callback: (call: Call, event: CallerSpeechEvent) => Promise<void>): void {
|
|
94
|
+
this._onCallerSpeech = callback;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
onAgentSpeech(callback: (call: Call, event: AgentSpeechEvent) => Promise<void>): void {
|
|
98
|
+
this._onAgentSpeech = callback;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
onQuestion(callback: (call: Call, question: string) => Promise<string>): void {
|
|
102
|
+
this._onQuestion = callback;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onTaskComplete(callback: (call: Call, taskId: string) => Promise<void>): void;
|
|
106
|
+
onTaskComplete(taskName: string, callback: (call: Call) => Promise<void>): void;
|
|
107
|
+
onTaskComplete(
|
|
108
|
+
callbackOrTaskName: ((call: Call, taskId: string) => Promise<void>) | string,
|
|
109
|
+
callback?: (call: Call) => Promise<void>,
|
|
110
|
+
): void {
|
|
111
|
+
const mixErr = "Cannot mix a generic onTaskComplete handler with per-task handlers.";
|
|
112
|
+
if (typeof callbackOrTaskName === "string") {
|
|
113
|
+
if (this._onTaskCompleteGeneric !== undefined) throw new Error(mixErr);
|
|
114
|
+
this._onTaskCompleteHandlers[callbackOrTaskName] = callback!;
|
|
115
|
+
} else {
|
|
116
|
+
if (Object.keys(this._onTaskCompleteHandlers).length > 0) throw new Error(mixErr);
|
|
117
|
+
this._onTaskCompleteGeneric = callbackOrTaskName;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
onSearchQuery(
|
|
122
|
+
fieldKey: string,
|
|
123
|
+
callback: (call: Call, query: string) => Promise<[string[], string[]]>,
|
|
124
|
+
): void {
|
|
125
|
+
this._searchQueryHandlers[fieldKey] = callback;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
onActionRequest(
|
|
129
|
+
callback: (call: Call, intentSummary: string) => Promise<SuggestedAction | undefined>,
|
|
130
|
+
): void {
|
|
131
|
+
this._onActionRequested = callback;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onAction(callback: (call: Call, actionKey: string) => Promise<void>): void;
|
|
135
|
+
onAction(actionKey: string, callback: (call: Call) => Promise<void>): void;
|
|
136
|
+
onAction(
|
|
137
|
+
callbackOrActionKey: ((call: Call, actionKey: string) => Promise<void>) | string,
|
|
138
|
+
callback?: (call: Call) => Promise<void>,
|
|
139
|
+
): void {
|
|
140
|
+
const mixErr = "Cannot mix a generic onAction handler with per-action handlers.";
|
|
141
|
+
if (typeof callbackOrActionKey === "string") {
|
|
142
|
+
if (this._onActionGeneric !== undefined) throw new Error(mixErr);
|
|
143
|
+
this._onActionHandlers[callbackOrActionKey] = callback!;
|
|
144
|
+
} else {
|
|
145
|
+
if (Object.keys(this._onActionHandlers).length > 0) throw new Error(mixErr);
|
|
146
|
+
this._onActionGeneric = callbackOrActionKey;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
onSessionEnd(callback: (call: Call) => Promise<void>): void {
|
|
151
|
+
this._onSessionEnd = callback;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
onReachPerson(callback: (call: Call, availability: string) => Promise<void>): void {
|
|
155
|
+
this.onTaskComplete("reach_person", async (call) => {
|
|
156
|
+
const availability = (await call.getField("contact_availability")) as string;
|
|
157
|
+
await callback(call, availability);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
inboundPhone(phoneNumber: string): InboundListener {
|
|
162
|
+
return this._listenInbound({
|
|
163
|
+
agent_number: phoneNumber,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async _dispatchEvent(call: Call, event: GuavaEvent) {
|
|
168
|
+
if (event.event_type === "caller-speech") {
|
|
169
|
+
if (this._onCallerSpeech !== undefined) {
|
|
170
|
+
await this._onCallerSpeech(call, event);
|
|
171
|
+
}
|
|
172
|
+
} else if (event.event_type === "agent-speech") {
|
|
173
|
+
if (this._onAgentSpeech !== undefined) {
|
|
174
|
+
await this._onAgentSpeech(call, event);
|
|
175
|
+
}
|
|
176
|
+
} else if (event.event_type === "inbound-call") {
|
|
177
|
+
this._logger.info(`Received inbound call from ${event.caller_number ?? "unknown"}`);
|
|
178
|
+
const action = await this._onCallReceived({
|
|
179
|
+
caller_number: event.caller_number,
|
|
180
|
+
agent_number: event.agent_number,
|
|
181
|
+
});
|
|
182
|
+
if (action.action === "accept") {
|
|
183
|
+
call.sendCommand(AcceptInboundCallCommand, { command_type: "accept-inbound" });
|
|
184
|
+
} else {
|
|
185
|
+
call.sendCommand(RejectInboundCallCommand, { command_type: "reject-inbound" });
|
|
186
|
+
}
|
|
187
|
+
} else if (event.event_type === "task-done") {
|
|
188
|
+
this._logger.info(`Task ${event.task_id} completed.`);
|
|
189
|
+
if (this._onTaskCompleteGeneric !== undefined) {
|
|
190
|
+
await this._onTaskCompleteGeneric(call, event.task_id);
|
|
191
|
+
} else if (event.task_id in this._onTaskCompleteHandlers) {
|
|
192
|
+
await this._onTaskCompleteHandlers[event.task_id](call);
|
|
193
|
+
} else {
|
|
194
|
+
this._logger.warn(`No handler registered for completion of task '${event.task_id}'`);
|
|
195
|
+
}
|
|
196
|
+
} else if (event.event_type === "agent-question") {
|
|
197
|
+
if (this._onQuestion !== undefined) {
|
|
198
|
+
this._logger.info(`Received question from bot: ${event.question}`);
|
|
199
|
+
let answer: string;
|
|
200
|
+
try {
|
|
201
|
+
answer = await this._onQuestion(call, event.question);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
this._logger.error(`Error occurred while answering question: ${err}`);
|
|
204
|
+
answer = "An error occurred and the question could not be answered.";
|
|
205
|
+
}
|
|
206
|
+
call.sendCommand(AnswerQuestionCommand, {
|
|
207
|
+
command_type: "answer-question",
|
|
208
|
+
question_id: event.question_id,
|
|
209
|
+
answer,
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
this._logger.warn(
|
|
213
|
+
`Received question but no onQuestion handler is registered: ${event.question}`,
|
|
214
|
+
);
|
|
215
|
+
call.sendCommand(AnswerQuestionCommand, {
|
|
216
|
+
command_type: "answer-question",
|
|
217
|
+
question_id: event.question_id,
|
|
218
|
+
answer: "I don't have an answer to that question.",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
} else if (event.event_type === "action-item-done") {
|
|
222
|
+
this._logger.info(`Action item '${event.key}' completed.`);
|
|
223
|
+
call._fieldValues[event.key] = event.payload;
|
|
224
|
+
} else if (event.event_type === "choice-query") {
|
|
225
|
+
this._logger.info(`Received search query for field '${event.field_key}': ${event.query}`);
|
|
226
|
+
const handler = this._searchQueryHandlers[event.field_key];
|
|
227
|
+
if (handler === undefined) {
|
|
228
|
+
this._logger.warn(
|
|
229
|
+
`Search query arrived for field '${event.field_key}' with no handler attached.`,
|
|
230
|
+
);
|
|
231
|
+
} else {
|
|
232
|
+
const [matchedChoices, otherChoices] = await handler(call, event.query);
|
|
233
|
+
call.sendCommand(ChoiceResultCommand, {
|
|
234
|
+
command_type: "choice-query-result",
|
|
235
|
+
field_key: event.field_key,
|
|
236
|
+
query_id: event.query_id,
|
|
237
|
+
matched_choices: matchedChoices,
|
|
238
|
+
other_choices: otherChoices,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
} else if (event.event_type === "action-request") {
|
|
242
|
+
this._logger.info(`Received action request ${event.intent_id}: ${event.intent_summary}`);
|
|
243
|
+
let suggestion: SuggestedAction | undefined;
|
|
244
|
+
if (this._onActionRequested !== undefined) {
|
|
245
|
+
suggestion = await this._onActionRequested(call, event.intent_summary);
|
|
246
|
+
}
|
|
247
|
+
call.sendCommand(ActionSuggestionCommand, {
|
|
248
|
+
command_type: "action-suggestion",
|
|
249
|
+
intent_id: event.intent_id,
|
|
250
|
+
action_key: suggestion?.key ?? null,
|
|
251
|
+
action_description: suggestion?.description ?? "",
|
|
252
|
+
});
|
|
253
|
+
} else if (event.event_type === "execute-action") {
|
|
254
|
+
this._logger.info(`Executing action '${event.action_key}'`);
|
|
255
|
+
let onActionFunc: (() => Promise<void>) | undefined;
|
|
256
|
+
if (this._onActionGeneric !== undefined) {
|
|
257
|
+
onActionFunc = () => this._onActionGeneric!(call, event.action_key);
|
|
258
|
+
} else if (event.action_key in this._onActionHandlers) {
|
|
259
|
+
onActionFunc = () => this._onActionHandlers[event.action_key](call);
|
|
260
|
+
}
|
|
261
|
+
if (onActionFunc !== undefined) {
|
|
262
|
+
await onActionFunc();
|
|
263
|
+
} else {
|
|
264
|
+
this._logger.warn(`No handler registered for action '${event.action_key}'`);
|
|
265
|
+
}
|
|
266
|
+
} else if (event.event_type === "bot-session-ended") {
|
|
267
|
+
this._logger.info("Session ended.");
|
|
268
|
+
if (this._onSessionEnd !== undefined) {
|
|
269
|
+
await this._onSessionEnd(call);
|
|
270
|
+
}
|
|
271
|
+
} else if (event.event_type === "error") {
|
|
272
|
+
this._logger.error(`The Guava agent reported an error: ${event.content}`);
|
|
273
|
+
} else if (event.event_type === "warning") {
|
|
274
|
+
this._logger.warn(`The Guava agent reported a warning: ${event.content}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async _startCall(variables: Record<string, any> = {}): Promise<Call> {
|
|
279
|
+
const call = new Call(variables);
|
|
280
|
+
call.setPersona({
|
|
281
|
+
agentName: this._name,
|
|
282
|
+
agentPurpose: this._purpose,
|
|
283
|
+
organizationName: this._organization,
|
|
284
|
+
});
|
|
285
|
+
call.sendCommand(RegisteredHooksCommand, {
|
|
286
|
+
command_type: "registered-hooks",
|
|
287
|
+
has_on_question: this._onQuestion !== undefined,
|
|
288
|
+
has_on_intent: false,
|
|
289
|
+
has_on_action_requested: this._onActionRequested !== undefined,
|
|
290
|
+
});
|
|
291
|
+
if (this._onCallStart !== undefined) {
|
|
292
|
+
await this._onCallStart(call);
|
|
293
|
+
}
|
|
294
|
+
return call;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_listenInbound(conn: InboundConnection): InboundListener {
|
|
298
|
+
const calls: Record<string, Call> = {};
|
|
299
|
+
|
|
300
|
+
// return a way to *stop* listening
|
|
301
|
+
const url = new URL("v1/listen-inbound", this._client.getWebsocketBase());
|
|
302
|
+
const ws = new WebSocket(url, {
|
|
303
|
+
headers: this._client.headers(),
|
|
304
|
+
});
|
|
305
|
+
let agent_number: string | undefined;
|
|
306
|
+
let webrtc_code: string | undefined;
|
|
307
|
+
if ("agent_number" in conn) {
|
|
308
|
+
agent_number = conn.agent_number;
|
|
309
|
+
} else {
|
|
310
|
+
webrtc_code = conn.webrtc_code;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this._logger.info(`Listening for calls to ${agent_number ?? webrtc_code}`);
|
|
314
|
+
|
|
315
|
+
if (webrtc_code) {
|
|
316
|
+
const debugurl = new URL(
|
|
317
|
+
`debug-webrtc?webrtc_code=${webrtc_code}`,
|
|
318
|
+
this._client.getHttpBase(),
|
|
319
|
+
);
|
|
320
|
+
this._logger.debug(`WebRTC DebugURL: ${debugurl}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
ws.addEventListener("open", (_ev) => {
|
|
324
|
+
ws.send(
|
|
325
|
+
stringifyZod(ListenInboundCommand, {
|
|
326
|
+
command_type: "listen-inbound",
|
|
327
|
+
agent_number: agent_number,
|
|
328
|
+
webrtc_code: webrtc_code,
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
ws.addEventListener("close", (_ev) => {
|
|
334
|
+
ws.removeAllListeners();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
ws.addEventListener("message", async (ev) => {
|
|
338
|
+
const tunnel_event = InboundTunnelEvent.parse(JSON.parse(ev.data.toString("utf8")));
|
|
339
|
+
if (!(tunnel_event.call_id in calls)) {
|
|
340
|
+
this._logger.info(
|
|
341
|
+
`Received tunnel event for new call ID: ${tunnel_event.call_id}. Creating call object.`,
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const call = await this._startCall();
|
|
345
|
+
await call.setDrain(async (commands) => {
|
|
346
|
+
for (const command of commands.splice(0)) {
|
|
347
|
+
this._logger.debug(
|
|
348
|
+
`Sending command: ${JSON.stringify(command)} for call ID: ${tunnel_event.call_id}`,
|
|
349
|
+
);
|
|
350
|
+
ws.send(
|
|
351
|
+
stringifyZod(InboundTunnelCommand, {
|
|
352
|
+
call_id: tunnel_event.call_id,
|
|
353
|
+
command,
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
calls[tunnel_event.call_id] = call;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this._dispatchEvent(calls[tunnel_event.call_id], tunnel_event.event);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return new InboundListener(ws);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @description use the Guava API to call out to a number
|
|
369
|
+
*/
|
|
370
|
+
async outboundPhone(
|
|
371
|
+
fromNumber: string | undefined,
|
|
372
|
+
toNumber: string,
|
|
373
|
+
variables: Record<string, any> = {},
|
|
374
|
+
) {
|
|
375
|
+
const url = new URL("v1/create-outbound", this._client.getWebsocketBase());
|
|
376
|
+
const ws = new WebSocket(url, {
|
|
377
|
+
headers: this._client.headers(),
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const call = await this._startCall(variables);
|
|
381
|
+
let socketInitialized = false;
|
|
382
|
+
|
|
383
|
+
ws.addEventListener("open", async (_ev) => {
|
|
384
|
+
ws.send(
|
|
385
|
+
stringifyZod(StartOutboundCallCommand, {
|
|
386
|
+
command_type: "start-outbound",
|
|
387
|
+
to_number: toNumber,
|
|
388
|
+
from_number: fromNumber,
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
// set the callController drain function to send all commands
|
|
393
|
+
// through the now open websocket
|
|
394
|
+
call.setDrain(async (commands) => {
|
|
395
|
+
for (const command of commands.splice(0)) {
|
|
396
|
+
this._logger.debug(`Sending command ${JSON.stringify(command)}`);
|
|
397
|
+
ws.send(JSON.stringify(command));
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
ws.addEventListener("message", (ev) => {
|
|
403
|
+
if (socketInitialized) {
|
|
404
|
+
const session_started = z
|
|
405
|
+
.union([SessionStartedEvent, ErrorEvent])
|
|
406
|
+
.parse(JSON.parse(ev.data.toString("utf8")));
|
|
407
|
+
|
|
408
|
+
if (session_started.event_type === "error") {
|
|
409
|
+
throw new Error(`Outbound call failed: ${session_started.content}`);
|
|
410
|
+
} else {
|
|
411
|
+
this._logger.info(`Started session with ID: ${session_started.session_id}`);
|
|
412
|
+
socketInitialized = true;
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
// handle the received event
|
|
416
|
+
const event = decodeEvent(ev.data);
|
|
417
|
+
if (event) {
|
|
418
|
+
this._dispatchEvent(call, event);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
ws.addEventListener("close", (_ev) => {
|
|
424
|
+
// we are closing the socket, so don't trigger any other listeners
|
|
425
|
+
ws.removeAllListeners();
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
class InboundListener {
|
|
431
|
+
private ws: WebSocket;
|
|
432
|
+
constructor(ws: WebSocket) {
|
|
433
|
+
this.ws = ws;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
close() {
|
|
437
|
+
this.ws.close();
|
|
438
|
+
}
|
|
439
|
+
}
|