@spekoai/sdk 0.5.1 → 0.5.2
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/CHANGELOG.md +6 -0
- package/README.md +88 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/lib/client.d.ts +13 -1
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +22 -2
- package/dist/lib/http.d.ts +11 -3
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +29 -12
- package/dist/lib/resources/agents.js +1 -0
- package/dist/lib/resources/calls.d.ts +19 -1
- package/dist/lib/resources/calls.d.ts.map +1 -1
- package/dist/lib/resources/calls.js +22 -0
- package/dist/lib/resources/phone-numbers.d.ts +2 -1
- package/dist/lib/resources/phone-numbers.d.ts.map +1 -1
- package/dist/lib/resources/phone-numbers.js +2 -1
- package/dist/lib/resources/realtime.d.ts +3 -5
- package/dist/lib/resources/realtime.d.ts.map +1 -1
- package/dist/lib/resources/realtime.js +829 -91
- package/dist/lib/resources/sessions.d.ts +53 -0
- package/dist/lib/resources/sessions.d.ts.map +1 -0
- package/dist/lib/resources/sessions.js +166 -0
- package/dist/lib/resources/sms.d.ts +80 -0
- package/dist/lib/resources/sms.d.ts.map +1 -0
- package/dist/lib/resources/sms.js +152 -0
- package/dist/lib/resources/transcribe.d.ts.map +1 -1
- package/dist/lib/resources/transcribe.js +5 -2
- package/dist/lib/resources/voice.d.ts +283 -1
- package/dist/lib/resources/voice.d.ts.map +1 -1
- package/dist/lib/resources/voice.js +345 -0
- package/dist/lib/resources/webhooks.d.ts +25 -0
- package/dist/lib/resources/webhooks.d.ts.map +1 -0
- package/dist/lib/resources/webhooks.js +46 -0
- package/dist/lib/types/index.d.ts +944 -9
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/voice-contract.d.ts +280 -0
- package/dist/lib/voice-contract.d.ts.map +1 -0
- package/dist/lib/voice-contract.js +115 -0
- package/package.json +2 -1
- package/src/index.ts +212 -0
- package/src/lib/client.ts +169 -0
- package/src/lib/errors.ts +28 -0
- package/src/lib/http.ts +442 -0
- package/src/lib/resources/agents.ts +211 -0
- package/src/lib/resources/callbacks.ts +40 -0
- package/src/lib/resources/calls.ts +113 -0
- package/src/lib/resources/complete.ts +63 -0
- package/src/lib/resources/credits.ts +41 -0
- package/src/lib/resources/knowledge-bases.ts +199 -0
- package/src/lib/resources/phone-numbers.ts +109 -0
- package/src/lib/resources/realtime-globals.d.ts +31 -0
- package/src/lib/resources/realtime.spec.ts +565 -0
- package/src/lib/resources/realtime.ts +1169 -0
- package/src/lib/resources/sessions.ts +191 -0
- package/src/lib/resources/sms.ts +214 -0
- package/src/lib/resources/synthesize.ts +101 -0
- package/src/lib/resources/transcribe.ts +91 -0
- package/src/lib/resources/usage.ts +24 -0
- package/src/lib/resources/voice.ts +426 -0
- package/src/lib/resources/voices.ts +32 -0
- package/src/lib/resources/webhooks.ts +67 -0
- package/src/lib/types/index.ts +2409 -0
- package/src/lib/voice-contract.ts +358 -0
package/src/lib/http.ts
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { SpekoApiError, SpekoAuthError, SpekoRateLimitError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
export interface HttpClientOptions {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
apiKey: string;
|
|
6
|
+
timeout: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const USER_AGENT = '@spekoai/sdk/0.5.2';
|
|
10
|
+
|
|
11
|
+
export class HttpClient {
|
|
12
|
+
private readonly baseUrl: string;
|
|
13
|
+
private readonly authHeader: string;
|
|
14
|
+
private readonly jsonHeaders: Record<string, string>;
|
|
15
|
+
private readonly timeout: number;
|
|
16
|
+
|
|
17
|
+
constructor(options: HttpClientOptions) {
|
|
18
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
19
|
+
this.authHeader = `Bearer ${options.apiKey}`;
|
|
20
|
+
this.jsonHeaders = {
|
|
21
|
+
Authorization: this.authHeader,
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
'User-Agent': USER_AGENT,
|
|
24
|
+
};
|
|
25
|
+
this.timeout = options.timeout;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async request<T>(
|
|
29
|
+
method: string,
|
|
30
|
+
path: string,
|
|
31
|
+
body?: unknown,
|
|
32
|
+
externalSignal?: AbortSignal,
|
|
33
|
+
extraHeaders?: Record<string, string>,
|
|
34
|
+
): Promise<T> {
|
|
35
|
+
const url = `${this.baseUrl}${path}`;
|
|
36
|
+
const { signal, cleanup } = this.buildSignal(externalSignal);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch(url, {
|
|
40
|
+
method,
|
|
41
|
+
headers: { ...this.jsonHeaders, ...extraHeaders },
|
|
42
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
43
|
+
signal,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
await this.handleError(response);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (response.status === 204) return undefined as T;
|
|
51
|
+
return (await response.json()) as T;
|
|
52
|
+
} finally {
|
|
53
|
+
cleanup();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async get<T>(path: string, externalSignal?: AbortSignal): Promise<T> {
|
|
58
|
+
return this.request<T>('GET', path, undefined, externalSignal);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async post<T>(
|
|
62
|
+
path: string,
|
|
63
|
+
body: unknown,
|
|
64
|
+
externalSignal?: AbortSignal,
|
|
65
|
+
extraHeaders?: Record<string, string>,
|
|
66
|
+
): Promise<T> {
|
|
67
|
+
return this.request<T>('POST', path, body, externalSignal, extraHeaders);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async put<T>(path: string, body: unknown, externalSignal?: AbortSignal): Promise<T> {
|
|
71
|
+
return this.request<T>('PUT', path, body, externalSignal);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async delete<T>(path: string, externalSignal?: AbortSignal): Promise<T> {
|
|
75
|
+
return this.request<T>('DELETE', path, undefined, externalSignal);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async patch<T>(path: string, body: unknown, externalSignal?: AbortSignal): Promise<T> {
|
|
79
|
+
return this.request<T>('PATCH', path, body, externalSignal);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Send raw bytes as the request body and parse a JSON response.
|
|
84
|
+
* Used by `speko.transcribe()` to upload audio and receive a transcript.
|
|
85
|
+
*/
|
|
86
|
+
async requestRaw<T>(
|
|
87
|
+
method: string,
|
|
88
|
+
path: string,
|
|
89
|
+
bodyBytes: Uint8Array,
|
|
90
|
+
extraHeaders: Record<string, string>,
|
|
91
|
+
externalSignal?: AbortSignal,
|
|
92
|
+
): Promise<T> {
|
|
93
|
+
const url = `${this.baseUrl}${path}`;
|
|
94
|
+
const { signal, cleanup } = this.buildSignal(externalSignal);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const headers: Record<string, string> = {
|
|
98
|
+
Authorization: this.authHeader,
|
|
99
|
+
'User-Agent': USER_AGENT,
|
|
100
|
+
...extraHeaders,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const response = await fetch(url, {
|
|
104
|
+
method,
|
|
105
|
+
headers,
|
|
106
|
+
body: bodyBytes as unknown as string,
|
|
107
|
+
signal,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
await this.handleError(response);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (await response.json()) as T;
|
|
115
|
+
} finally {
|
|
116
|
+
cleanup();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Send JSON and receive a binary response (e.g. synthesized audio).
|
|
122
|
+
* Returns the raw bytes plus the response headers so callers can read
|
|
123
|
+
* `Content-Type`, `X-Speko-Provider`, etc.
|
|
124
|
+
*/
|
|
125
|
+
async requestBinary(
|
|
126
|
+
method: string,
|
|
127
|
+
path: string,
|
|
128
|
+
body: unknown,
|
|
129
|
+
externalSignal?: AbortSignal,
|
|
130
|
+
): Promise<{ bytes: Uint8Array; headers: Record<string, string> }> {
|
|
131
|
+
const url = `${this.baseUrl}${path}`;
|
|
132
|
+
const { signal, cleanup } = this.buildSignal(externalSignal);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetch(url, {
|
|
136
|
+
method,
|
|
137
|
+
headers: this.jsonHeaders,
|
|
138
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
139
|
+
signal,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
await this.handleError(response);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const buffer = await response.arrayBuffer();
|
|
147
|
+
const headers: Record<string, string> = {};
|
|
148
|
+
response.headers.forEach((value, key) => {
|
|
149
|
+
headers[key] = value;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return { bytes: new Uint8Array(buffer), headers };
|
|
153
|
+
} finally {
|
|
154
|
+
cleanup();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async *requestSse(
|
|
159
|
+
method: string,
|
|
160
|
+
path: string,
|
|
161
|
+
body: unknown,
|
|
162
|
+
externalSignal?: AbortSignal,
|
|
163
|
+
extraHeaders?: Record<string, string>,
|
|
164
|
+
/**
|
|
165
|
+
* Per-request override of the client timeout. Long-lived SSE responses
|
|
166
|
+
* (session observation) outlive the default 30 s, which would abort them
|
|
167
|
+
* mid-stream.
|
|
168
|
+
*/
|
|
169
|
+
timeoutMs?: number,
|
|
170
|
+
): AsyncIterableIterator<{ event: string; id?: string; data: unknown }> {
|
|
171
|
+
const url = `${this.baseUrl}${path}`;
|
|
172
|
+
const { signal, cleanup } = this.buildSignal(externalSignal, timeoutMs);
|
|
173
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const response = await fetch(url, {
|
|
177
|
+
method,
|
|
178
|
+
headers: { ...this.jsonHeaders, ...extraHeaders },
|
|
179
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
180
|
+
signal,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
await this.handleError(response);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!response.body) {
|
|
188
|
+
throw new SpekoApiError('Response body is empty', response.status, 'EMPTY_BODY');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
reader = response.body.getReader();
|
|
192
|
+
const decoder = new TextDecoder();
|
|
193
|
+
let buffer = '';
|
|
194
|
+
|
|
195
|
+
while (true) {
|
|
196
|
+
const { done, value } = await reader.read();
|
|
197
|
+
if (done) break;
|
|
198
|
+
buffer += decoder.decode(value, { stream: true });
|
|
199
|
+
const events = drainSseEvents(buffer);
|
|
200
|
+
buffer = events.remainder;
|
|
201
|
+
for (const event of events.items) {
|
|
202
|
+
yield event;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
buffer += decoder.decode();
|
|
207
|
+
const events = drainSseEvents(buffer + '\n\n');
|
|
208
|
+
for (const event of events.items) {
|
|
209
|
+
yield event;
|
|
210
|
+
}
|
|
211
|
+
} finally {
|
|
212
|
+
reader?.releaseLock();
|
|
213
|
+
cleanup();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async requestRawSse(
|
|
218
|
+
method: string,
|
|
219
|
+
path: string,
|
|
220
|
+
bodyBytes: Uint8Array,
|
|
221
|
+
extraHeaders: Record<string, string>,
|
|
222
|
+
externalSignal?: AbortSignal,
|
|
223
|
+
): Promise<AsyncIterableIterator<{ event: string; id?: string; data: unknown }>> {
|
|
224
|
+
const url = `${this.baseUrl}${path}`;
|
|
225
|
+
const { signal, cleanup } = this.buildSignal(externalSignal);
|
|
226
|
+
|
|
227
|
+
const response = await fetch(url, {
|
|
228
|
+
method,
|
|
229
|
+
headers: {
|
|
230
|
+
Authorization: this.authHeader,
|
|
231
|
+
'User-Agent': USER_AGENT,
|
|
232
|
+
...extraHeaders,
|
|
233
|
+
},
|
|
234
|
+
body: bodyBytes as unknown as string,
|
|
235
|
+
signal,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
cleanup();
|
|
240
|
+
await this.handleError(response);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!response.body) {
|
|
244
|
+
cleanup();
|
|
245
|
+
throw new SpekoApiError('Response body is empty', response.status, 'EMPTY_BODY');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return this.readSseBody(response.body, cleanup);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async requestBinaryStream(
|
|
252
|
+
method: string,
|
|
253
|
+
path: string,
|
|
254
|
+
body: unknown,
|
|
255
|
+
externalSignal?: AbortSignal,
|
|
256
|
+
extraHeaders?: Record<string, string>,
|
|
257
|
+
): Promise<{ chunks: AsyncIterableIterator<Uint8Array>; headers: Record<string, string> }> {
|
|
258
|
+
const url = `${this.baseUrl}${path}`;
|
|
259
|
+
const { signal, cleanup } = this.buildSignal(externalSignal);
|
|
260
|
+
|
|
261
|
+
const response = await fetch(url, {
|
|
262
|
+
method,
|
|
263
|
+
headers: { ...this.jsonHeaders, ...extraHeaders },
|
|
264
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
265
|
+
signal,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
cleanup();
|
|
270
|
+
await this.handleError(response);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!response.body) {
|
|
274
|
+
cleanup();
|
|
275
|
+
throw new SpekoApiError('Response body is empty', response.status, 'EMPTY_BODY');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const headers: Record<string, string> = {};
|
|
279
|
+
response.headers.forEach((value, key) => {
|
|
280
|
+
headers[key] = value;
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
headers,
|
|
285
|
+
chunks: this.readBinaryBody(response.body, cleanup),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private async *readSseBody(
|
|
290
|
+
body: ReadableStream<Uint8Array>,
|
|
291
|
+
cleanup: () => void,
|
|
292
|
+
): AsyncIterableIterator<{ event: string; id?: string; data: unknown }> {
|
|
293
|
+
const reader = body.getReader();
|
|
294
|
+
const decoder = new TextDecoder();
|
|
295
|
+
let buffer = '';
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
while (true) {
|
|
299
|
+
const { done, value } = await reader.read();
|
|
300
|
+
if (done) break;
|
|
301
|
+
buffer += decoder.decode(value, { stream: true });
|
|
302
|
+
const events = drainSseEvents(buffer);
|
|
303
|
+
buffer = events.remainder;
|
|
304
|
+
for (const event of events.items) {
|
|
305
|
+
yield event;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
buffer += decoder.decode();
|
|
310
|
+
const events = drainSseEvents(buffer + '\n\n');
|
|
311
|
+
for (const event of events.items) {
|
|
312
|
+
yield event;
|
|
313
|
+
}
|
|
314
|
+
} finally {
|
|
315
|
+
reader.releaseLock();
|
|
316
|
+
cleanup();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private async *readBinaryBody(
|
|
321
|
+
body: ReadableStream<Uint8Array>,
|
|
322
|
+
cleanup: () => void,
|
|
323
|
+
): AsyncIterableIterator<Uint8Array> {
|
|
324
|
+
const reader = body.getReader();
|
|
325
|
+
try {
|
|
326
|
+
while (true) {
|
|
327
|
+
const { done, value } = await reader.read();
|
|
328
|
+
if (done) break;
|
|
329
|
+
yield value;
|
|
330
|
+
}
|
|
331
|
+
} finally {
|
|
332
|
+
reader.releaseLock();
|
|
333
|
+
cleanup();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Compose the internal timeout signal with an optional external signal so
|
|
339
|
+
* that callers can cancel in-flight requests (e.g. LiveKit Agents tearing
|
|
340
|
+
* down a session) while still enforcing the client's configured timeout.
|
|
341
|
+
*/
|
|
342
|
+
private buildSignal(
|
|
343
|
+
externalSignal?: AbortSignal,
|
|
344
|
+
timeoutMs?: number,
|
|
345
|
+
): {
|
|
346
|
+
signal: AbortSignal;
|
|
347
|
+
cleanup: () => void;
|
|
348
|
+
} {
|
|
349
|
+
const controller = new AbortController();
|
|
350
|
+
const effectiveTimeout = timeoutMs ?? this.timeout;
|
|
351
|
+
const timer =
|
|
352
|
+
effectiveTimeout > 0 ? setTimeout(() => controller.abort(), effectiveTimeout) : null;
|
|
353
|
+
|
|
354
|
+
if (externalSignal) {
|
|
355
|
+
if (externalSignal.aborted) {
|
|
356
|
+
controller.abort(externalSignal.reason);
|
|
357
|
+
} else {
|
|
358
|
+
const onAbort = () => controller.abort(externalSignal.reason);
|
|
359
|
+
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
360
|
+
return {
|
|
361
|
+
signal: controller.signal,
|
|
362
|
+
cleanup: () => {
|
|
363
|
+
if (timer) clearTimeout(timer);
|
|
364
|
+
externalSignal.removeEventListener('abort', onAbort);
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
signal: controller.signal,
|
|
372
|
+
cleanup: () => {
|
|
373
|
+
if (timer) clearTimeout(timer);
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private async handleError(response: Response): Promise<never> {
|
|
379
|
+
const text = await response.text();
|
|
380
|
+
let message: string;
|
|
381
|
+
let code: string;
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const json = JSON.parse(text) as { error?: string; code?: string };
|
|
385
|
+
message = json.error ?? text;
|
|
386
|
+
code = json.code ?? 'UNKNOWN';
|
|
387
|
+
} catch {
|
|
388
|
+
message = text || response.statusText;
|
|
389
|
+
code = 'UNKNOWN';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (response.status === 401) {
|
|
393
|
+
throw new SpekoAuthError(message);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (response.status === 429) {
|
|
397
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
398
|
+
throw new SpekoRateLimitError(message, retryAfter ? parseInt(retryAfter, 10) : null);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
throw new SpekoApiError(message, response.status, code);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function drainSseEvents(buffer: string): {
|
|
406
|
+
items: Array<{ event: string; id?: string; data: unknown }>;
|
|
407
|
+
remainder: string;
|
|
408
|
+
} {
|
|
409
|
+
const items: Array<{ event: string; id?: string; data: unknown }> = [];
|
|
410
|
+
let cursor = 0;
|
|
411
|
+
|
|
412
|
+
while (true) {
|
|
413
|
+
const next = buffer.indexOf('\n\n', cursor);
|
|
414
|
+
if (next === -1) break;
|
|
415
|
+
const block = buffer.slice(cursor, next);
|
|
416
|
+
cursor = next + 2;
|
|
417
|
+
if (!block.trim()) continue;
|
|
418
|
+
|
|
419
|
+
let event = 'message';
|
|
420
|
+
let id: string | undefined;
|
|
421
|
+
const dataLines: string[] = [];
|
|
422
|
+
for (const line of block.split(/\r?\n/)) {
|
|
423
|
+
if (line.startsWith('event:')) {
|
|
424
|
+
event = line.slice('event:'.length).trim();
|
|
425
|
+
} else if (line.startsWith('id:')) {
|
|
426
|
+
id = line.slice('id:'.length).trim();
|
|
427
|
+
} else if (line.startsWith('data:')) {
|
|
428
|
+
dataLines.push(line.slice('data:'.length).trimStart());
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const rawData = dataLines.join('\n');
|
|
432
|
+
let data: unknown = rawData;
|
|
433
|
+
try {
|
|
434
|
+
data = JSON.parse(rawData);
|
|
435
|
+
} catch {
|
|
436
|
+
// Keep non-JSON SSE payloads as strings.
|
|
437
|
+
}
|
|
438
|
+
items.push({ event, ...(id ? { id } : {}), data });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return { items, remainder: buffer.slice(cursor) };
|
|
442
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { HttpClient } from '../http.js';
|
|
2
|
+
import type {
|
|
3
|
+
AgentCallListPage,
|
|
4
|
+
AgentCallListParams,
|
|
5
|
+
AgentCreateParams,
|
|
6
|
+
AgentRow,
|
|
7
|
+
AgentToolCreateParams,
|
|
8
|
+
AgentToolRow,
|
|
9
|
+
AgentToolUpdateParams,
|
|
10
|
+
AgentUpdateParams,
|
|
11
|
+
ChatTool,
|
|
12
|
+
PhoneNumberRow,
|
|
13
|
+
} from '../types/index.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Per-org agent definitions — the system prompt, voice, intent, and
|
|
17
|
+
* routing constraints used when this agent answers (or places) a call.
|
|
18
|
+
*
|
|
19
|
+
* Every agent created via {@link create} auto-provisions a `Default`
|
|
20
|
+
* knowledge base, so callers can upload documents through
|
|
21
|
+
* {@link Speko.knowledgeBases} without an extra setup step.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const agent = await speko.agents.create({
|
|
26
|
+
* name: 'Support Bot',
|
|
27
|
+
* systemPrompt: 'You are a helpful support agent for Acme.',
|
|
28
|
+
* voice: 'sophia',
|
|
29
|
+
* intent: { language: 'en', optimizeFor: 'latency' },
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* await speko.agents.attachPhoneNumber(agent.id, num.id);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class Agents {
|
|
36
|
+
readonly tools: AgentTools;
|
|
37
|
+
|
|
38
|
+
constructor(private readonly http: HttpClient) {
|
|
39
|
+
this.tools = new AgentTools(http);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
list(): Promise<AgentRow[]> {
|
|
43
|
+
return this.http.get<AgentRow[]>('/v1/agents');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
create(params: AgentCreateParams): Promise<AgentRow> {
|
|
47
|
+
return this.http.post<AgentRow>('/v1/agents', params);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get(agentId: string): Promise<AgentRow> {
|
|
51
|
+
return this.http.get<AgentRow>(`/v1/agents/${encodeURIComponent(agentId)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
update(agentId: string, params: AgentUpdateParams): Promise<AgentRow> {
|
|
55
|
+
return this.http.patch<AgentRow>(`/v1/agents/${encodeURIComponent(agentId)}`, params);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
delete(agentId: string): Promise<{ deleted: boolean }> {
|
|
59
|
+
return this.http.delete<{ deleted: boolean }>(`/v1/agents/${encodeURIComponent(agentId)}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Bind a phone number to this agent so inbound calls hydrate the
|
|
64
|
+
* agent's pipeline config from the agent row. Internally calls
|
|
65
|
+
* `PATCH /v1/phone-numbers/:id` with `{ agentId }`.
|
|
66
|
+
*/
|
|
67
|
+
attachPhoneNumber(agentId: string, phoneNumberId: string): Promise<PhoneNumberRow> {
|
|
68
|
+
return this.http.patch<PhoneNumberRow>(
|
|
69
|
+
`/v1/phone-numbers/${encodeURIComponent(phoneNumberId)}`,
|
|
70
|
+
{ agentId },
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Unlink a phone number from any agent. Inbound calls fall back to
|
|
76
|
+
* the number's `dispatchMetadataTemplate` (or fail if neither is
|
|
77
|
+
* configured). Internally calls `PATCH /v1/phone-numbers/:id` with
|
|
78
|
+
* `{ agentId: null }`.
|
|
79
|
+
*/
|
|
80
|
+
detachPhoneNumber(phoneNumberId: string): Promise<PhoneNumberRow> {
|
|
81
|
+
return this.http.patch<PhoneNumberRow>(
|
|
82
|
+
`/v1/phone-numbers/${encodeURIComponent(phoneNumberId)}`,
|
|
83
|
+
{ agentId: null },
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* List recent calls for an agent. Use `next_cursor` from the returned page
|
|
89
|
+
* as `cursor` to page backward in time.
|
|
90
|
+
*/
|
|
91
|
+
listCalls(agentId: string, params: AgentCallListParams = {}): Promise<AgentCallListPage> {
|
|
92
|
+
const query = new URLSearchParams();
|
|
93
|
+
if (params.limit !== undefined) query.set('limit', String(params.limit));
|
|
94
|
+
if (params.cursor) query.set('cursor', params.cursor);
|
|
95
|
+
if (params.since) query.set('since', params.since);
|
|
96
|
+
const suffix = query.toString() ? `?${query}` : '';
|
|
97
|
+
return this.http.get<AgentCallListPage>(
|
|
98
|
+
`/v1/agents/${encodeURIComponent(agentId)}/calls${suffix}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Per-agent tool definitions exposed to the LLM mid-call. Four execution
|
|
105
|
+
* modes: `inline` (caller runs the tool), `webhook` (Speko POSTs to your URL
|
|
106
|
+
* with a Standard-Webhooks signature), `builtin` (Speko-managed tools like
|
|
107
|
+
* `search_knowledge_base`, `transfer_call`, `end_call`), and `integration`
|
|
108
|
+
* (an org-installed Speko app action such as Google Calendar or Slack).
|
|
109
|
+
*
|
|
110
|
+
* Webhook secrets are encrypted server-side at creation; the returned
|
|
111
|
+
* row carries a `secretRef` pointer instead of the plaintext.
|
|
112
|
+
*
|
|
113
|
+
* Every method accepts an optional trailing `AbortSignal` to cancel the
|
|
114
|
+
* in-flight request — useful when a calling framework tears down a session
|
|
115
|
+
* mid-call.
|
|
116
|
+
*/
|
|
117
|
+
export class AgentTools {
|
|
118
|
+
constructor(private readonly http: HttpClient) {}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param opts.available When true, the server returns only tools the agent
|
|
122
|
+
* can actually run right now — integration tools whose backing installation
|
|
123
|
+
* is disconnected/missing are omitted. Use this on the runtime path so the
|
|
124
|
+
* model is never offered a tool that would fail.
|
|
125
|
+
*/
|
|
126
|
+
list(
|
|
127
|
+
agentId: string,
|
|
128
|
+
abortSignal?: AbortSignal,
|
|
129
|
+
opts?: { available?: boolean },
|
|
130
|
+
): Promise<AgentToolRow[]> {
|
|
131
|
+
const query = opts?.available ? '?available=1' : '';
|
|
132
|
+
return this.http.get<AgentToolRow[]>(
|
|
133
|
+
`/v1/agents/${encodeURIComponent(agentId)}/tools${query}`,
|
|
134
|
+
abortSignal,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
create(
|
|
139
|
+
agentId: string,
|
|
140
|
+
params: AgentToolCreateParams,
|
|
141
|
+
abortSignal?: AbortSignal,
|
|
142
|
+
): Promise<AgentToolRow> {
|
|
143
|
+
return this.http.post<AgentToolRow>(
|
|
144
|
+
`/v1/agents/${encodeURIComponent(agentId)}/tools`,
|
|
145
|
+
params,
|
|
146
|
+
abortSignal,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get(agentId: string, toolId: string, abortSignal?: AbortSignal): Promise<AgentToolRow> {
|
|
151
|
+
return this.http.get<AgentToolRow>(
|
|
152
|
+
`/v1/agents/${encodeURIComponent(agentId)}/tools/${encodeURIComponent(toolId)}`,
|
|
153
|
+
abortSignal,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
update(
|
|
158
|
+
agentId: string,
|
|
159
|
+
toolId: string,
|
|
160
|
+
params: AgentToolUpdateParams,
|
|
161
|
+
abortSignal?: AbortSignal,
|
|
162
|
+
): Promise<AgentToolRow> {
|
|
163
|
+
return this.http.patch<AgentToolRow>(
|
|
164
|
+
`/v1/agents/${encodeURIComponent(agentId)}/tools/${encodeURIComponent(toolId)}`,
|
|
165
|
+
params,
|
|
166
|
+
abortSignal,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
delete(
|
|
171
|
+
agentId: string,
|
|
172
|
+
toolId: string,
|
|
173
|
+
abortSignal?: AbortSignal,
|
|
174
|
+
): Promise<{ deleted: boolean }> {
|
|
175
|
+
return this.http.delete<{ deleted: boolean }>(
|
|
176
|
+
`/v1/agents/${encodeURIComponent(agentId)}/tools/${encodeURIComponent(toolId)}`,
|
|
177
|
+
abortSignal,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Fetch this agent's registered tools and convert them into the
|
|
183
|
+
* {@link ChatTool}[] shape that {@link Speko.complete} accepts. Handles all
|
|
184
|
+
* four source kinds (`inline`, `webhook`, `builtin`, `integration`) — load
|
|
185
|
+
* once and pass the result straight to `speko.complete({ tools })`.
|
|
186
|
+
*/
|
|
187
|
+
async listChatTools(
|
|
188
|
+
agentId: string,
|
|
189
|
+
abortSignal?: AbortSignal,
|
|
190
|
+
opts?: { available?: boolean },
|
|
191
|
+
): Promise<ChatTool[]> {
|
|
192
|
+
const rows = await this.list(agentId, abortSignal, opts);
|
|
193
|
+
return rows.map(toChatTool);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Convert a serialized {@link AgentToolRow} into a {@link ChatTool}. The row's
|
|
199
|
+
* `source` is structurally identical to `ChatToolSource` for every kind, so the
|
|
200
|
+
* mapping is a direct passthrough; `executionMode` is derived from `source.kind`.
|
|
201
|
+
*/
|
|
202
|
+
function toChatTool(row: AgentToolRow): ChatTool {
|
|
203
|
+
return {
|
|
204
|
+
name: row.name,
|
|
205
|
+
description: row.description,
|
|
206
|
+
parameters: row.parameters,
|
|
207
|
+
executionMode: row.source.kind,
|
|
208
|
+
source: row.source,
|
|
209
|
+
...(row.preToolSpeech !== undefined && { preToolSpeech: row.preToolSpeech }),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { HttpClient } from '../http.js';
|
|
2
|
+
import type {
|
|
3
|
+
CancelScheduledCallbackParams,
|
|
4
|
+
ScheduledCallback,
|
|
5
|
+
ScheduledCallbacksListParams,
|
|
6
|
+
} from '../types/index.js';
|
|
7
|
+
|
|
8
|
+
export class Callbacks {
|
|
9
|
+
constructor(private readonly http: HttpClient) {}
|
|
10
|
+
|
|
11
|
+
list(params: ScheduledCallbacksListParams = {}): Promise<{ callbacks: ScheduledCallback[] }> {
|
|
12
|
+
const query = new URLSearchParams();
|
|
13
|
+
if (params.status) query.set('status', params.status);
|
|
14
|
+
if (params.sourceSessionId) query.set('source_session_id', params.sourceSessionId);
|
|
15
|
+
if (params.limit !== undefined) query.set('limit', String(params.limit));
|
|
16
|
+
const suffix = query.toString() ? `?${query}` : '';
|
|
17
|
+
return this.http.get<{ callbacks: ScheduledCallback[] }>(`/v1/callbacks${suffix}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get(callbackId: string): Promise<ScheduledCallback> {
|
|
21
|
+
return this.http.get<ScheduledCallback>(`/v1/callbacks/${encodeURIComponent(callbackId)}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
cancel(
|
|
25
|
+
callbackId: string,
|
|
26
|
+
params: CancelScheduledCallbackParams = {},
|
|
27
|
+
): Promise<ScheduledCallback> {
|
|
28
|
+
return this.http.post<ScheduledCallback>(
|
|
29
|
+
`/v1/callbacks/${encodeURIComponent(callbackId)}/cancel`,
|
|
30
|
+
params,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
dispatch(callbackId: string): Promise<ScheduledCallback> {
|
|
35
|
+
return this.http.post<ScheduledCallback>(
|
|
36
|
+
`/v1/callbacks/${encodeURIComponent(callbackId)}/dispatch`,
|
|
37
|
+
{},
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|