@kognitivedev/cloud-voice 0.2.29
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/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-test.log +13 -0
- package/CHANGELOG.md +10 -0
- package/README.md +226 -0
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +301 -0
- package/dist/client.d.ts +219 -0
- package/dist/client.js +535 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +15 -0
- package/dist/server.d.ts +58 -0
- package/dist/server.js +92 -0
- package/dist/server.test.d.ts +1 -0
- package/dist/server.test.js +78 -0
- package/dist/sse.d.ts +7 -0
- package/dist/sse.js +75 -0
- package/dist/types.d.ts +865 -0
- package/dist/types.js +2 -0
- package/package.json +52 -0
- package/src/__tests__/browser.test.ts +196 -0
- package/src/__tests__/client.test.ts +482 -0
- package/src/__tests__/server.test.ts +84 -0
- package/src/browser.ts +342 -0
- package/src/client.ts +610 -0
- package/src/index.ts +100 -0
- package/src/server.ts +140 -0
- package/src/sse.ts +57 -0
- package/src/types.ts +927 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +8 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { HttpTransport, KognitiveApiError, type HttpTransportConfig } from "@kognitivedev/client-core";
|
|
2
|
+
import { readSSEStream } from "./sse";
|
|
3
|
+
import type {
|
|
4
|
+
CloudVoiceAgentRecord,
|
|
5
|
+
CloudVoiceClientConfig,
|
|
6
|
+
CloudVoiceEmbedConfig,
|
|
7
|
+
CloudVoiceSessionBootstrap,
|
|
8
|
+
CloudVoiceSessionEventRecord,
|
|
9
|
+
CloudVoiceSessionRecord,
|
|
10
|
+
CloudVoiceToolCatalog,
|
|
11
|
+
CloudVoiceCustomVoiceRecord,
|
|
12
|
+
CloudVoiceCallStatusChange,
|
|
13
|
+
CloudVoiceCallWaitOptions,
|
|
14
|
+
CloudVoiceCallWaitResult,
|
|
15
|
+
CloudVoiceCallLegRecord,
|
|
16
|
+
CloudVoiceLiveCallRecord,
|
|
17
|
+
CloudVoicePhoneBridgeHealth,
|
|
18
|
+
CloudVoicePhoneConnectionRecord,
|
|
19
|
+
CloudVoicePhoneNumberRecord,
|
|
20
|
+
CloudVoiceSipDiagnostics,
|
|
21
|
+
CloudVoiceRecordingAsset,
|
|
22
|
+
CloudVoiceHandoffRecord,
|
|
23
|
+
CloudVoiceListenTokenResult,
|
|
24
|
+
CreateCloudVoicePhoneConnectionInput,
|
|
25
|
+
CreateCloudVoicePhoneNumberInput,
|
|
26
|
+
CreateCloudVoiceOutboundCallInput,
|
|
27
|
+
CreateCloudVoiceAgentInput,
|
|
28
|
+
CreateCloudVoiceSessionInput,
|
|
29
|
+
ImportCloudVoiceCustomVoiceInput,
|
|
30
|
+
CloneCloudVoiceCustomVoiceInput,
|
|
31
|
+
CreateCloudVoiceHandoffInput,
|
|
32
|
+
AcceptCloudVoiceHandoffInput,
|
|
33
|
+
CancelCloudVoiceHandoffInput,
|
|
34
|
+
UpdateCloudVoicePhoneNumberInput,
|
|
35
|
+
UpdateCloudVoiceAgentInput,
|
|
36
|
+
UpdateCloudVoiceCustomVoiceInput,
|
|
37
|
+
SyncCloudVoiceToolCatalogInput,
|
|
38
|
+
UseCloudVoiceAgentInput,
|
|
39
|
+
UseCloudVoiceAgentResult,
|
|
40
|
+
} from "./types";
|
|
41
|
+
|
|
42
|
+
function isTransportLike(value: unknown): value is HttpTransport {
|
|
43
|
+
return Boolean(value && typeof value === "object" && "baseUrl" in value && "json" in value && "raw" in value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertString(value: string, name: string) {
|
|
47
|
+
if (!value || !value.trim()) throw new Error(`${name} is required`);
|
|
48
|
+
return value.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isNotFoundError(error: unknown) {
|
|
52
|
+
return error instanceof KognitiveApiError && error.status === 404
|
|
53
|
+
|| Boolean(error && typeof error === "object" && "status" in error && (error as { status?: unknown }).status === 404);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const TERMINAL_CALL_STATUSES = new Set(["completed", "cancelled", "canceled", "failed", "busy", "no-answer", "error"]);
|
|
57
|
+
const TERMINAL_SESSION_STATUSES = new Set(["completed", "cancelled", "canceled", "error"]);
|
|
58
|
+
|
|
59
|
+
function sleep(ms: number) {
|
|
60
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isTerminalCall(call: CloudVoiceLiveCallRecord) {
|
|
64
|
+
return Boolean(call.endedAt)
|
|
65
|
+
|| TERMINAL_CALL_STATUSES.has(call.status)
|
|
66
|
+
|| Boolean(call.sessionStatus && TERMINAL_SESSION_STATUSES.has(call.sessionStatus));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function callStatusFingerprint(call: CloudVoiceLiveCallRecord) {
|
|
70
|
+
return [
|
|
71
|
+
call.status,
|
|
72
|
+
call.sessionStatus ?? "",
|
|
73
|
+
call.stateLabel,
|
|
74
|
+
call.latestEventType ?? "",
|
|
75
|
+
call.outcomeStatus ?? "",
|
|
76
|
+
].join("|");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toCallStatusChange(call: CloudVoiceLiveCallRecord, previousStatus?: string): CloudVoiceCallStatusChange {
|
|
80
|
+
return {
|
|
81
|
+
call,
|
|
82
|
+
status: call.status,
|
|
83
|
+
previousStatus,
|
|
84
|
+
sessionStatus: call.sessionStatus,
|
|
85
|
+
stateLabel: call.stateLabel,
|
|
86
|
+
latestEventType: call.latestEventType,
|
|
87
|
+
outcomeStatus: call.outcomeStatus,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class KognitiveCloudVoiceClient {
|
|
92
|
+
private readonly transport: HttpTransport;
|
|
93
|
+
|
|
94
|
+
readonly agents = {
|
|
95
|
+
list: () => this.listAgents(),
|
|
96
|
+
get: (slug: string) => this.getAgent(slug),
|
|
97
|
+
use: (slug: string, input: UseCloudVoiceAgentInput = {}) => this.useAgent(slug, input),
|
|
98
|
+
management: {
|
|
99
|
+
list: () => this.listAgents(),
|
|
100
|
+
create: (input: CreateCloudVoiceAgentInput) => this.createAgent(input),
|
|
101
|
+
createOrUse: (slug: string, input: UseCloudVoiceAgentInput = {}) => this.useAgent(slug, input),
|
|
102
|
+
get: (slug: string) => this.getAgent(slug),
|
|
103
|
+
update: (slug: string, input: UpdateCloudVoiceAgentInput) => this.updateAgent(slug, input),
|
|
104
|
+
delete: (slug: string) => this.deleteAgent(slug),
|
|
105
|
+
publish: (slug: string) => this.publishAgent(slug),
|
|
106
|
+
unpublish: (slug: string) => this.unpublishAgent(slug),
|
|
107
|
+
use: (slug: string, input: UseCloudVoiceAgentInput = {}) => this.useAgent(slug, input),
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
readonly sessions = {
|
|
112
|
+
list: (agentId?: string) => this.listSessions(agentId),
|
|
113
|
+
create: (agentSlug: string, input: Omit<CreateCloudVoiceSessionInput, "agentSlug" | "agent"> = {}) =>
|
|
114
|
+
this.createSession({ ...input, agentSlug }),
|
|
115
|
+
get: (sessionId: string) => this.getSession(sessionId),
|
|
116
|
+
cancel: (sessionId: string, reason?: string) => this.cancelSession(sessionId, reason),
|
|
117
|
+
events: (sessionId: string) => this.getSessionEvents(sessionId),
|
|
118
|
+
appendEvent: (sessionId: string, event: Record<string, unknown>) => this.appendSessionEvent(sessionId, event),
|
|
119
|
+
stream: (sessionId: string, init?: RequestInit) => this.streamSessionEvents(sessionId, init),
|
|
120
|
+
subscribe: (sessionId: string, init?: RequestInit) => this.subscribeToSession(sessionId, init),
|
|
121
|
+
executeTool: (sessionId: string, toolId: string, input: unknown) => this.executeTool(sessionId, toolId, input),
|
|
122
|
+
recordings: (sessionId: string) => this.getSessionRecordings(sessionId),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
readonly calls = {
|
|
126
|
+
list: () => this.listCalls(),
|
|
127
|
+
live: (input: { status?: "live" | "all"; limit?: number } = {}) => this.listLiveCalls(input),
|
|
128
|
+
get: (sessionId: string) => this.getCall(sessionId),
|
|
129
|
+
create: (input: CreateCloudVoiceOutboundCallInput) => this.createCall(input),
|
|
130
|
+
wait: (sessionId: string, options?: CloudVoiceCallWaitOptions) => this.waitForCall(sessionId, options),
|
|
131
|
+
hangup: (sessionId: string, reason?: string) => this.hangupCall(sessionId, reason),
|
|
132
|
+
listenToken: (sessionId: string, input: { managerId?: string; takeover?: boolean } = {}) => this.createCallListenToken(sessionId, input),
|
|
133
|
+
handoff: {
|
|
134
|
+
create: (sessionId: string, input: CreateCloudVoiceHandoffInput = {}) => this.createCallHandoff(sessionId, input),
|
|
135
|
+
accept: (sessionId: string, handoffId: string, input: AcceptCloudVoiceHandoffInput = {}) => this.acceptCallHandoff(sessionId, handoffId, input),
|
|
136
|
+
cancel: (sessionId: string, handoffId: string, input: CancelCloudVoiceHandoffInput = {}) => this.cancelCallHandoff(sessionId, handoffId, input),
|
|
137
|
+
},
|
|
138
|
+
events: (sessionId: string) => this.getSessionEvents(sessionId),
|
|
139
|
+
stream: (sessionId: string, init?: RequestInit) => this.streamSessionEvents(sessionId, init),
|
|
140
|
+
subscribe: (sessionId: string, init?: RequestInit) => this.subscribeToSession(sessionId, init),
|
|
141
|
+
executeTool: (sessionId: string, toolId: string, input: unknown) => this.executeCallTool(sessionId, toolId, input),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
readonly phone = {
|
|
145
|
+
connections: {
|
|
146
|
+
list: () => this.listPhoneConnections(),
|
|
147
|
+
create: (input: CreateCloudVoicePhoneConnectionInput) => this.createPhoneConnection(input),
|
|
148
|
+
},
|
|
149
|
+
numbers: {
|
|
150
|
+
list: () => this.listPhoneNumbers(),
|
|
151
|
+
create: (input: CreateCloudVoicePhoneNumberInput) => this.createPhoneNumber(input),
|
|
152
|
+
update: (input: UpdateCloudVoicePhoneNumberInput) => this.updatePhoneNumber(input),
|
|
153
|
+
},
|
|
154
|
+
bridge: {
|
|
155
|
+
health: () => this.getPhoneBridgeHealth(),
|
|
156
|
+
},
|
|
157
|
+
sip: {
|
|
158
|
+
diagnostics: () => this.getSipDiagnostics(),
|
|
159
|
+
reload: () => this.reloadSipBridge(),
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
readonly recordings = {
|
|
164
|
+
getForSession: (sessionId: string) => this.getSessionRecordings(sessionId),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
readonly tools = {
|
|
168
|
+
catalog: {
|
|
169
|
+
list: () => this.listToolCatalog(),
|
|
170
|
+
sync: (input: SyncCloudVoiceToolCatalogInput) => this.syncToolCatalog(input),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
readonly customVoices = {
|
|
175
|
+
list: () => this.listCustomVoices(),
|
|
176
|
+
import: (input: ImportCloudVoiceCustomVoiceInput) => this.importCustomVoice(input),
|
|
177
|
+
clone: (input: CloneCloudVoiceCustomVoiceInput) => this.cloneCustomVoice(input),
|
|
178
|
+
update: (voiceId: string, input: UpdateCloudVoiceCustomVoiceInput) => this.updateCustomVoice(voiceId, input),
|
|
179
|
+
delete: (voiceId: string) => this.deleteCustomVoice(voiceId),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
constructor(config: CloudVoiceClientConfig | HttpTransport) {
|
|
183
|
+
this.transport = isTransportLike(config) ? config : new HttpTransport(config as HttpTransportConfig);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async listAgents(): Promise<CloudVoiceAgentRecord[]> {
|
|
187
|
+
const res = await this.transport.json<{ agents: CloudVoiceAgentRecord[] }>("/api/cloud/voice/agents");
|
|
188
|
+
return res.agents;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async getAgent(slug: string): Promise<CloudVoiceAgentRecord> {
|
|
192
|
+
return this.transport.json<CloudVoiceAgentRecord>(`/api/cloud/voice/agents/${encodeURIComponent(assertString(slug, "slug"))}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async createAgent(input: CreateCloudVoiceAgentInput): Promise<CloudVoiceAgentRecord> {
|
|
196
|
+
return this.transport.json<CloudVoiceAgentRecord>("/api/cloud/voice/agents", {
|
|
197
|
+
method: "POST",
|
|
198
|
+
body: JSON.stringify(input),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async listCustomVoices(): Promise<CloudVoiceCustomVoiceRecord[]> {
|
|
203
|
+
const res = await this.transport.json<{ voices: CloudVoiceCustomVoiceRecord[] }>("/api/cloud/voice/custom-voices");
|
|
204
|
+
return res.voices;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async importCustomVoice(input: ImportCloudVoiceCustomVoiceInput): Promise<CloudVoiceCustomVoiceRecord> {
|
|
208
|
+
return this.transport.json<CloudVoiceCustomVoiceRecord>("/api/cloud/voice/custom-voices/import", {
|
|
209
|
+
method: "POST",
|
|
210
|
+
body: JSON.stringify(input),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async cloneCustomVoice(input: CloneCloudVoiceCustomVoiceInput): Promise<CloudVoiceCustomVoiceRecord> {
|
|
215
|
+
const form = new FormData();
|
|
216
|
+
form.append("file", input.file, input.fileName ?? "reference.wav");
|
|
217
|
+
const fields = {
|
|
218
|
+
name: input.name,
|
|
219
|
+
description: input.description,
|
|
220
|
+
gender: input.gender,
|
|
221
|
+
accent: input.accent,
|
|
222
|
+
age: input.age,
|
|
223
|
+
language: input.language,
|
|
224
|
+
use_case: input.useCase,
|
|
225
|
+
tone: input.tone,
|
|
226
|
+
};
|
|
227
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
228
|
+
if (typeof value === "string" && value.trim()) form.append(key, value.trim());
|
|
229
|
+
}
|
|
230
|
+
form.append("consent", JSON.stringify(input.consent));
|
|
231
|
+
return this.transport.multipart<CloudVoiceCustomVoiceRecord>("/api/cloud/voice/custom-voices/clone", form, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async updateCustomVoice(voiceId: string, input: UpdateCloudVoiceCustomVoiceInput): Promise<CloudVoiceCustomVoiceRecord> {
|
|
237
|
+
return this.transport.json<CloudVoiceCustomVoiceRecord>(`/api/cloud/voice/custom-voices/${encodeURIComponent(assertString(voiceId, "voiceId"))}`, {
|
|
238
|
+
method: "PATCH",
|
|
239
|
+
body: JSON.stringify({
|
|
240
|
+
...input,
|
|
241
|
+
...(input.useCase !== undefined ? { use_case: input.useCase } : {}),
|
|
242
|
+
}),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async deleteCustomVoice(voiceId: string): Promise<{ success: boolean }> {
|
|
247
|
+
return this.transport.json<{ success: boolean }>(`/api/cloud/voice/custom-voices/${encodeURIComponent(assertString(voiceId, "voiceId"))}`, {
|
|
248
|
+
method: "DELETE",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async updateAgent(slug: string, input: UpdateCloudVoiceAgentInput): Promise<CloudVoiceAgentRecord> {
|
|
253
|
+
return this.transport.json<CloudVoiceAgentRecord>(`/api/cloud/voice/agents/${encodeURIComponent(assertString(slug, "slug"))}`, {
|
|
254
|
+
method: "PUT",
|
|
255
|
+
body: JSON.stringify(input),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async useAgent(slug: string, input: UseCloudVoiceAgentInput = {}): Promise<UseCloudVoiceAgentResult> {
|
|
260
|
+
const normalizedSlug = assertString(slug, "slug");
|
|
261
|
+
let existing: CloudVoiceAgentRecord | null = null;
|
|
262
|
+
try {
|
|
263
|
+
existing = await this.getAgent(normalizedSlug);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (!isNotFoundError(error)) throw error;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const hasUpdates = input.name !== undefined
|
|
269
|
+
|| input.description !== undefined
|
|
270
|
+
|| input.status !== undefined
|
|
271
|
+
|| input.config !== undefined;
|
|
272
|
+
|
|
273
|
+
let action: UseCloudVoiceAgentResult["action"] = existing ? "used" : "created";
|
|
274
|
+
let agent = existing;
|
|
275
|
+
if (existing && hasUpdates) {
|
|
276
|
+
agent = await this.updateAgent(normalizedSlug, {
|
|
277
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
278
|
+
...(input.description !== undefined ? { description: input.description } : {}),
|
|
279
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
280
|
+
...(input.config !== undefined ? { config: input.config } : {}),
|
|
281
|
+
});
|
|
282
|
+
action = "updated";
|
|
283
|
+
} else if (!existing) {
|
|
284
|
+
agent = await this.createAgent({
|
|
285
|
+
name: input.name ?? normalizedSlug,
|
|
286
|
+
slug: normalizedSlug,
|
|
287
|
+
description: input.description,
|
|
288
|
+
config: input.config,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!agent) {
|
|
293
|
+
throw new Error(`Cloud voice agent "${normalizedSlug}" could not be created or resolved`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (input.publish) {
|
|
297
|
+
agent = await this.publishAgent(agent.slug);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { action, agent };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async deleteAgent(slug: string): Promise<{ success: boolean }> {
|
|
304
|
+
return this.transport.json<{ success: boolean }>(`/api/cloud/voice/agents/${encodeURIComponent(assertString(slug, "slug"))}`, {
|
|
305
|
+
method: "DELETE",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async publishAgent(slug: string): Promise<CloudVoiceAgentRecord> {
|
|
310
|
+
return this.transport.json<CloudVoiceAgentRecord>(`/api/cloud/voice/agents/${encodeURIComponent(assertString(slug, "slug"))}/publish`, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async unpublishAgent(slug: string): Promise<CloudVoiceAgentRecord> {
|
|
316
|
+
return this.transport.json<CloudVoiceAgentRecord>(`/api/cloud/voice/agents/${encodeURIComponent(assertString(slug, "slug"))}/unpublish`, {
|
|
317
|
+
method: "POST",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async listSessions(agentId?: string): Promise<CloudVoiceSessionRecord[]> {
|
|
322
|
+
const path = agentId ? `/api/cloud/voice/sessions?agentId=${encodeURIComponent(agentId)}` : "/api/cloud/voice/sessions";
|
|
323
|
+
const res = await this.transport.json<{ sessions: CloudVoiceSessionRecord[] }>(path);
|
|
324
|
+
return res.sessions;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async createSession(input: CreateCloudVoiceSessionInput): Promise<CloudVoiceSessionBootstrap> {
|
|
328
|
+
return this.transport.json<CloudVoiceSessionBootstrap>("/api/cloud/voice/sessions", {
|
|
329
|
+
method: "POST",
|
|
330
|
+
body: JSON.stringify(input),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async getSession(sessionId: string): Promise<CloudVoiceSessionRecord> {
|
|
335
|
+
return this.transport.json<CloudVoiceSessionRecord>(`/api/cloud/voice/sessions/${encodeURIComponent(assertString(sessionId, "sessionId"))}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async cancelSession(sessionId: string, reason?: string): Promise<CloudVoiceSessionRecord> {
|
|
339
|
+
return this.transport.json<CloudVoiceSessionRecord>(`/api/cloud/voice/sessions/${encodeURIComponent(assertString(sessionId, "sessionId"))}/cancel`, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
body: JSON.stringify({ reason }),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async getSessionEvents(sessionId: string): Promise<CloudVoiceSessionEventRecord[]> {
|
|
346
|
+
const res = await this.transport.json<{ events: CloudVoiceSessionEventRecord[] }>(`/api/cloud/voice/sessions/${encodeURIComponent(assertString(sessionId, "sessionId"))}/events`);
|
|
347
|
+
return res.events;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async appendSessionEvent(sessionId: string, event: Record<string, unknown>): Promise<CloudVoiceSessionEventRecord> {
|
|
351
|
+
return this.transport.json<CloudVoiceSessionEventRecord>(`/api/cloud/voice/sessions/${encodeURIComponent(assertString(sessionId, "sessionId"))}/events`, {
|
|
352
|
+
method: "POST",
|
|
353
|
+
body: JSON.stringify(event),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async streamSessionEvents(sessionId: string, init?: RequestInit): Promise<Response> {
|
|
358
|
+
return this.transport.raw(`/api/cloud/voice/sessions/${encodeURIComponent(assertString(sessionId, "sessionId"))}/events/stream`, init);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async subscribeToSession(sessionId: string, init?: RequestInit): Promise<AsyncGenerator<CloudVoiceSessionEventRecord | { type: "unknown"; frame: unknown }>> {
|
|
362
|
+
const response = await this.streamSessionEvents(sessionId, init);
|
|
363
|
+
if (!response.body) throw new Error("Cloud voice event stream response did not include a body");
|
|
364
|
+
const events = readSSEStream(response.body);
|
|
365
|
+
async function* decode() {
|
|
366
|
+
for await (const frame of events) {
|
|
367
|
+
try {
|
|
368
|
+
yield JSON.parse(frame.data) as CloudVoiceSessionEventRecord;
|
|
369
|
+
} catch {
|
|
370
|
+
yield { type: "unknown" as const, frame };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return decode();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async executeTool(sessionId: string, toolId: string, input: unknown): Promise<{ result: unknown }> {
|
|
378
|
+
return this.transport.json<{ result: unknown }>(`/api/cloud/voice/sessions/${encodeURIComponent(assertString(sessionId, "sessionId"))}/tools/${encodeURIComponent(assertString(toolId, "toolId"))}/execute`, {
|
|
379
|
+
method: "POST",
|
|
380
|
+
body: JSON.stringify({ input }),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async getSessionRecordings(sessionId: string): Promise<{ recording: unknown | null; assets: CloudVoiceRecordingAsset[] }> {
|
|
385
|
+
return this.transport.json<{ recording: unknown | null; assets: CloudVoiceRecordingAsset[] }>(`/api/cloud/voice/sessions/${encodeURIComponent(assertString(sessionId, "sessionId"))}/recordings`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async listPhoneConnections(): Promise<CloudVoicePhoneConnectionRecord[]> {
|
|
389
|
+
const res = await this.transport.json<{ connections: CloudVoicePhoneConnectionRecord[] }>("/api/cloud/voice/phone/connections");
|
|
390
|
+
return res.connections;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async createPhoneConnection(input: CreateCloudVoicePhoneConnectionInput): Promise<CloudVoicePhoneConnectionRecord> {
|
|
394
|
+
return this.transport.json<CloudVoicePhoneConnectionRecord>("/api/cloud/voice/phone/connections", {
|
|
395
|
+
method: "POST",
|
|
396
|
+
body: JSON.stringify(input),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async listPhoneNumbers(): Promise<CloudVoicePhoneNumberRecord[]> {
|
|
401
|
+
const res = await this.transport.json<{ numbers: CloudVoicePhoneNumberRecord[] }>("/api/cloud/voice/phone/numbers");
|
|
402
|
+
return res.numbers;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async createPhoneNumber(input: CreateCloudVoicePhoneNumberInput): Promise<CloudVoicePhoneNumberRecord> {
|
|
406
|
+
return this.transport.json<CloudVoicePhoneNumberRecord>("/api/cloud/voice/phone/numbers", {
|
|
407
|
+
method: "POST",
|
|
408
|
+
body: JSON.stringify(input),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async updatePhoneNumber(input: UpdateCloudVoicePhoneNumberInput): Promise<CloudVoicePhoneNumberRecord | null> {
|
|
413
|
+
return this.transport.json<CloudVoicePhoneNumberRecord | null>("/api/cloud/voice/phone/numbers", {
|
|
414
|
+
method: "PATCH",
|
|
415
|
+
body: JSON.stringify(input),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async getPhoneBridgeHealth(): Promise<CloudVoicePhoneBridgeHealth> {
|
|
420
|
+
const res = await this.transport.json<{ bridge: CloudVoicePhoneBridgeHealth }>("/api/cloud/voice/phone/bridge");
|
|
421
|
+
return res.bridge;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async getSipDiagnostics(): Promise<CloudVoiceSipDiagnostics> {
|
|
425
|
+
const res = await this.transport.json<{ sip: CloudVoiceSipDiagnostics }>("/api/cloud/voice/phone/sip");
|
|
426
|
+
return res.sip;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async reloadSipBridge(): Promise<Record<string, unknown>> {
|
|
430
|
+
const res = await this.transport.json<{ sip: Record<string, unknown> }>("/api/cloud/voice/phone/sip", {
|
|
431
|
+
method: "POST",
|
|
432
|
+
body: "{}",
|
|
433
|
+
});
|
|
434
|
+
return res.sip;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async listToolCatalog(): Promise<CloudVoiceToolCatalog> {
|
|
438
|
+
return this.transport.json<CloudVoiceToolCatalog>("/api/cloud/voice/tools/catalog");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async syncToolCatalog(input: SyncCloudVoiceToolCatalogInput): Promise<CloudVoiceToolCatalog> {
|
|
442
|
+
return this.transport.json<CloudVoiceToolCatalog>("/api/cloud/voice/tools/sync", {
|
|
443
|
+
method: "POST",
|
|
444
|
+
body: JSON.stringify(input),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async listCalls(): Promise<CloudVoiceCallLegRecord[]> {
|
|
449
|
+
const res = await this.transport.json<{ calls: CloudVoiceCallLegRecord[] }>("/api/cloud/voice/calls");
|
|
450
|
+
return res.calls;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async listLiveCalls(input: { status?: "live" | "all"; limit?: number } = {}): Promise<CloudVoiceLiveCallRecord[]> {
|
|
454
|
+
const params = new URLSearchParams();
|
|
455
|
+
if (input.status === "all") params.set("status", "all");
|
|
456
|
+
if (input.limit !== undefined) params.set("limit", String(input.limit));
|
|
457
|
+
const qs = params.toString();
|
|
458
|
+
const res = await this.transport.json<{ calls: CloudVoiceLiveCallRecord[] }>(`/api/cloud/voice/calls/live${qs ? `?${qs}` : ""}`);
|
|
459
|
+
return res.calls;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async getCall(sessionId: string): Promise<CloudVoiceLiveCallRecord> {
|
|
463
|
+
return this.transport.json<CloudVoiceLiveCallRecord>(`/api/cloud/voice/calls/${encodeURIComponent(assertString(sessionId, "sessionId"))}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async createCall(input: CreateCloudVoiceOutboundCallInput): Promise<{
|
|
467
|
+
session: CloudVoiceSessionBootstrap;
|
|
468
|
+
callLeg: CloudVoiceCallLegRecord;
|
|
469
|
+
providerCall: unknown;
|
|
470
|
+
}> {
|
|
471
|
+
return this.transport.json("/api/cloud/voice/calls", {
|
|
472
|
+
method: "POST",
|
|
473
|
+
body: JSON.stringify(input),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async finalizeCall(sessionId: string): Promise<CloudVoiceCallWaitResult> {
|
|
478
|
+
return this.transport.json<CloudVoiceCallWaitResult>(`/api/cloud/voice/calls/${encodeURIComponent(assertString(sessionId, "sessionId"))}/finalize`, {
|
|
479
|
+
method: "POST",
|
|
480
|
+
body: JSON.stringify({}),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async waitForCall(sessionId: string, options: CloudVoiceCallWaitOptions = {}): Promise<CloudVoiceCallWaitResult> {
|
|
485
|
+
const normalizedSessionId = assertString(sessionId, "sessionId");
|
|
486
|
+
const intervalMs = Math.max(10, options.intervalMs ?? 1_000);
|
|
487
|
+
const timeoutMs = Math.max(intervalMs, options.timeoutMs ?? 30 * 60_000);
|
|
488
|
+
const deadline = Date.now() + timeoutMs;
|
|
489
|
+
let lastFingerprint: string | null = null;
|
|
490
|
+
let previousStatus: string | undefined;
|
|
491
|
+
|
|
492
|
+
while (true) {
|
|
493
|
+
const call = await this.getCall(normalizedSessionId);
|
|
494
|
+
const fingerprint = callStatusFingerprint(call);
|
|
495
|
+
if (fingerprint !== lastFingerprint) {
|
|
496
|
+
const event = toCallStatusChange(call, previousStatus);
|
|
497
|
+
options.onStatusChange?.(event);
|
|
498
|
+
options.onEvent?.(event);
|
|
499
|
+
lastFingerprint = fingerprint;
|
|
500
|
+
previousStatus = call.status;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (isTerminalCall(call)) {
|
|
504
|
+
const result = await this.finalizeCall(normalizedSessionId);
|
|
505
|
+
const finalFingerprint = callStatusFingerprint(result.call);
|
|
506
|
+
if (finalFingerprint !== lastFingerprint) {
|
|
507
|
+
const event = toCallStatusChange(result.call, previousStatus);
|
|
508
|
+
options.onStatusChange?.(event);
|
|
509
|
+
options.onEvent?.(event);
|
|
510
|
+
}
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (Date.now() >= deadline) {
|
|
515
|
+
throw new Error(`Cloud voice call ${normalizedSessionId} did not complete within ${timeoutMs}ms`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
await sleep(intervalMs);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async executeCallTool(sessionId: string, toolId: string, input: unknown): Promise<{ result: unknown }> {
|
|
523
|
+
return this.transport.json<{ result: unknown }>(`/api/cloud/voice/calls/${encodeURIComponent(assertString(sessionId, "sessionId"))}/tools/${encodeURIComponent(assertString(toolId, "toolId"))}`, {
|
|
524
|
+
method: "POST",
|
|
525
|
+
body: JSON.stringify({ input }),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async hangupCall(sessionId: string, reason?: string): Promise<{ call: CloudVoiceLiveCallRecord; result: unknown }> {
|
|
530
|
+
return this.transport.json<{ call: CloudVoiceLiveCallRecord; result: unknown }>(`/api/cloud/voice/calls/${encodeURIComponent(assertString(sessionId, "sessionId"))}/hangup`, {
|
|
531
|
+
method: "POST",
|
|
532
|
+
body: JSON.stringify({ reason }),
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async createCallListenToken(sessionId: string, input: { managerId?: string; takeover?: boolean } = {}): Promise<CloudVoiceListenTokenResult> {
|
|
537
|
+
return this.transport.json<CloudVoiceListenTokenResult>(`/api/cloud/voice/calls/${encodeURIComponent(assertString(sessionId, "sessionId"))}/listen-token`, {
|
|
538
|
+
method: "POST",
|
|
539
|
+
body: JSON.stringify(input),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async createCallHandoff(sessionId: string, input: CreateCloudVoiceHandoffInput = {}): Promise<{ handoff: CloudVoiceHandoffRecord }> {
|
|
544
|
+
return this.transport.json<{ handoff: CloudVoiceHandoffRecord }>(`/api/cloud/voice/calls/${encodeURIComponent(assertString(sessionId, "sessionId"))}/handoff`, {
|
|
545
|
+
method: "POST",
|
|
546
|
+
body: JSON.stringify(input),
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async acceptCallHandoff(sessionId: string, handoffId: string, input: AcceptCloudVoiceHandoffInput = {}): Promise<{ handoff: CloudVoiceHandoffRecord }> {
|
|
551
|
+
return this.transport.json<{ handoff: CloudVoiceHandoffRecord }>(`/api/cloud/voice/calls/${encodeURIComponent(assertString(sessionId, "sessionId"))}/handoff/${encodeURIComponent(assertString(handoffId, "handoffId"))}/accept`, {
|
|
552
|
+
method: "POST",
|
|
553
|
+
body: JSON.stringify(input),
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async cancelCallHandoff(sessionId: string, handoffId: string, input: CancelCloudVoiceHandoffInput = {}): Promise<{ handoff: CloudVoiceHandoffRecord }> {
|
|
558
|
+
return this.transport.json<{ handoff: CloudVoiceHandoffRecord }>(`/api/cloud/voice/calls/${encodeURIComponent(assertString(sessionId, "sessionId"))}/handoff/${encodeURIComponent(assertString(handoffId, "handoffId"))}/cancel`, {
|
|
559
|
+
method: "POST",
|
|
560
|
+
body: JSON.stringify(input),
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export class KognitiveCloudVoiceEmbedClient {
|
|
566
|
+
private readonly baseUrl: string;
|
|
567
|
+
private readonly publicKey: string;
|
|
568
|
+
private readonly fetchImpl: typeof fetch;
|
|
569
|
+
|
|
570
|
+
constructor(config: { baseUrl: string; publicKey: string; fetch?: typeof fetch }) {
|
|
571
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
572
|
+
this.publicKey = assertString(config.publicKey, "publicKey");
|
|
573
|
+
this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private headers(extra?: HeadersInit) {
|
|
577
|
+
const headers = new Headers(extra);
|
|
578
|
+
headers.set("x-kognitive-public-key", this.publicKey);
|
|
579
|
+
headers.set("Content-Type", "application/json");
|
|
580
|
+
return headers;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async config(agent: string): Promise<CloudVoiceEmbedConfig> {
|
|
584
|
+
const res = await this.fetchImpl(`${this.baseUrl}/api/cloud/voice/embed/config?agent=${encodeURIComponent(agent)}`, {
|
|
585
|
+
headers: this.headers(),
|
|
586
|
+
});
|
|
587
|
+
if (!res.ok) throw new Error(await res.text());
|
|
588
|
+
return res.json() as Promise<CloudVoiceEmbedConfig>;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async createSession(input: CreateCloudVoiceSessionInput): Promise<CloudVoiceSessionBootstrap> {
|
|
592
|
+
const res = await this.fetchImpl(`${this.baseUrl}/api/cloud/voice/embed/sessions`, {
|
|
593
|
+
method: "POST",
|
|
594
|
+
headers: this.headers(),
|
|
595
|
+
body: JSON.stringify(input),
|
|
596
|
+
});
|
|
597
|
+
if (!res.ok) throw new Error(await res.text());
|
|
598
|
+
return res.json() as Promise<CloudVoiceSessionBootstrap>;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async executeTool(sessionId: string, toolId: string, input: unknown): Promise<{ result: unknown }> {
|
|
602
|
+
const res = await this.fetchImpl(`${this.baseUrl}/api/cloud/voice/sessions/${encodeURIComponent(assertString(sessionId, "sessionId"))}/tools/${encodeURIComponent(assertString(toolId, "toolId"))}/execute`, {
|
|
603
|
+
method: "POST",
|
|
604
|
+
headers: this.headers(),
|
|
605
|
+
body: JSON.stringify({ input }),
|
|
606
|
+
});
|
|
607
|
+
if (!res.ok) throw new Error(await res.text());
|
|
608
|
+
return res.json() as Promise<{ result: unknown }>;
|
|
609
|
+
}
|
|
610
|
+
}
|