@pyai/sdk 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/index.d.ts +206 -26
- package/dist/index.js +98 -12
- package/package.json +1 -1
- package/src/index.ts +259 -32
package/README.md
CHANGED
|
@@ -131,6 +131,7 @@ WS subprotocol, so it works in the browser), routes the wire frames to
|
|
|
131
131
|
const hear = pyai.audio.transcriptions.stream({
|
|
132
132
|
sampleRate: 16000,
|
|
133
133
|
endpointingMs: 800, // minimum trailing pause; may wait up to max(800, 1500) ms
|
|
134
|
+
vocabulary: ["Nguyen", "SKU-99"],
|
|
134
135
|
onConfigAck: (ack) => {
|
|
135
136
|
if (ack.warnings.length) throw new Error(JSON.stringify(ack.warnings));
|
|
136
137
|
},
|
|
@@ -145,6 +146,35 @@ vad.on("end", () => hear.commit()); // optional forced final
|
|
|
145
146
|
// hear.close() also flushes a final for any buffered audio
|
|
146
147
|
```
|
|
147
148
|
|
|
149
|
+
Streaming uses up to five sanitized vocabulary terms. To store organization
|
|
150
|
+
suggestions, use a key with `hear:configure` and set explicit activation
|
|
151
|
+
profiles first:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
await pyai.hear.vocabulary.set({
|
|
155
|
+
terms: ["Nguyen", "SKU-99"],
|
|
156
|
+
enabledFor: ["batch", "hear_stream"],
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Request-level terms come first. Stored suggestions fill remaining slots up to
|
|
161
|
+
five. The effective list is fixed when a stream opens or a batch job is created.
|
|
162
|
+
Organization Hear terms are not used by Omni and are not populated
|
|
163
|
+
automatically from CRM or dialer data. A managed Agent can opt in with its own
|
|
164
|
+
list:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
const agent = await pyai.agents.create({
|
|
168
|
+
name: "Front desk",
|
|
169
|
+
vocabulary: ["Nguyen", "Acme Dental", "SKU-99"],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await pyai.agents.update(agent.agent_id, { vocabulary: [] });
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The Agent list is sanitized to at most five effective terms and fixed when a
|
|
176
|
+
new session starts. An empty list turns the feature off.
|
|
177
|
+
|
|
148
178
|
Frame `type`s, WS close codes, and error `code`s are exported as named
|
|
149
179
|
constants so you never hardcode a magic string:
|
|
150
180
|
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,20 @@ export interface Voice {
|
|
|
31
31
|
name?: string;
|
|
32
32
|
gender?: string;
|
|
33
33
|
region?: string;
|
|
34
|
+
language?: string;
|
|
35
|
+
/** Customer-facing quality tier. */
|
|
36
|
+
tier?: "standard" | "natural";
|
|
37
|
+
/** Permanent convenience inputs accepted on the advertised surfaces. */
|
|
38
|
+
aliases?: string[];
|
|
39
|
+
/** Product surfaces on which this stock voice can be selected. */
|
|
40
|
+
available_on?: Array<"speak" | "omni">;
|
|
41
|
+
/** Accepted Speak delivery modes. Empty means the voice has no Speak surface. */
|
|
42
|
+
synthesis_modes?: Array<"streaming" | "async">;
|
|
43
|
+
/** Voice-specific amount on top of the selected product's base rate. */
|
|
44
|
+
pricing?: {
|
|
45
|
+
included_in_base_price: boolean;
|
|
46
|
+
additional_price_usd_per_minute: number;
|
|
47
|
+
};
|
|
34
48
|
[k: string]: unknown;
|
|
35
49
|
}
|
|
36
50
|
export interface ListResponse<T> {
|
|
@@ -82,6 +96,13 @@ export interface SpeechParams {
|
|
|
82
96
|
input: string;
|
|
83
97
|
voice?: string;
|
|
84
98
|
model?: SpeakModel;
|
|
99
|
+
/**
|
|
100
|
+
* Delivery mode. The API defaults to true and every catalog voice accepts
|
|
101
|
+
* both values. Set false when you want one complete buffered body with a
|
|
102
|
+
* `Content-Length`. A voice whose serving fleet has no streaming lane is
|
|
103
|
+
* rendered buffered either way and says so with `x-pyai-stream: buffered`.
|
|
104
|
+
*/
|
|
105
|
+
stream?: boolean;
|
|
85
106
|
/**
|
|
86
107
|
* Output container/codec, resampled+encoded server-side. One of
|
|
87
108
|
* {@link SpeechFormat}, anything else is a `400 unsupported_format`. Omit for
|
|
@@ -119,8 +140,57 @@ export interface CreateJobParams {
|
|
|
119
140
|
diarize?: boolean;
|
|
120
141
|
channel?: boolean;
|
|
121
142
|
numerals?: boolean;
|
|
143
|
+
smart_format?: boolean;
|
|
144
|
+
dictation?: boolean;
|
|
145
|
+
drop_fillers?: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Per-job names, brands, products, or distinctive terms. PyAI keeps up to
|
|
148
|
+
* five valid entries after trimming, deduplication, and common-word
|
|
149
|
+
* filtering.
|
|
150
|
+
*/
|
|
151
|
+
vocabulary?: string[];
|
|
122
152
|
output_formats?: Array<"json" | "srt" | "vtt">;
|
|
123
153
|
webhook_url?: string;
|
|
154
|
+
call_id?: string;
|
|
155
|
+
pack_id?: string;
|
|
156
|
+
call_direction?: "inbound" | "outbound";
|
|
157
|
+
customer_name?: string;
|
|
158
|
+
}
|
|
159
|
+
export type HearVocabularyProfile = "batch" | "hear_stream";
|
|
160
|
+
export interface HearVocabularyInput {
|
|
161
|
+
/** Organization-owned names, brands, products, or distinctive terms. */
|
|
162
|
+
terms: string[];
|
|
163
|
+
/** Stored terms remain inert unless a use case is selected here. */
|
|
164
|
+
enabledFor: HearVocabularyProfile[];
|
|
165
|
+
}
|
|
166
|
+
export interface HearVocabulary {
|
|
167
|
+
object: "hear.vocabulary";
|
|
168
|
+
/** Sanitized stored list, at most five terms. */
|
|
169
|
+
terms: string[];
|
|
170
|
+
/** Use cases that may add stored suggestions. */
|
|
171
|
+
enabled_for: HearVocabularyProfile[];
|
|
172
|
+
/** Unix milliseconds, or null before the first save. */
|
|
173
|
+
updated_at: number | null;
|
|
174
|
+
}
|
|
175
|
+
export interface AgentConfig {
|
|
176
|
+
name?: string;
|
|
177
|
+
persona_system_prompt?: string | null;
|
|
178
|
+
greeting?: string | null;
|
|
179
|
+
voice_id?: string | null;
|
|
180
|
+
language?: "en" | "fr" | "es" | "de" | "hi" | null;
|
|
181
|
+
/**
|
|
182
|
+
* Opt-in speech-recognition vocabulary. PyAI sanitizes and keeps at most
|
|
183
|
+
* five terms. An empty list or null turns it off.
|
|
184
|
+
*/
|
|
185
|
+
vocabulary?: string[] | null;
|
|
186
|
+
[key: string]: unknown;
|
|
187
|
+
}
|
|
188
|
+
export interface Agent {
|
|
189
|
+
object: "agent";
|
|
190
|
+
agent_id: string;
|
|
191
|
+
name: string;
|
|
192
|
+
vocabulary: string[];
|
|
193
|
+
[key: string]: unknown;
|
|
124
194
|
}
|
|
125
195
|
export interface RealtimeOptions {
|
|
126
196
|
/**
|
|
@@ -183,10 +253,16 @@ export declare const HearFrameType: {
|
|
|
183
253
|
readonly Error: "error";
|
|
184
254
|
};
|
|
185
255
|
export type HearFrameType = (typeof HearFrameType)[keyof typeof HearFrameType];
|
|
186
|
-
/**
|
|
256
|
+
/**
|
|
257
|
+
* WebSocket close codes used across the PyAI realtime/streaming surfaces.
|
|
258
|
+
* Browser callers may initiate only Normal or values in 3000-4999; the
|
|
259
|
+
* standards-reserved values below are server close observations.
|
|
260
|
+
*/
|
|
187
261
|
export declare const WSCloseCode: {
|
|
188
262
|
/** Normal closure. */
|
|
189
263
|
readonly Normal: 1000;
|
|
264
|
+
/** Browser-safe private application code for a malformed peer protocol frame. */
|
|
265
|
+
readonly ProtocolViolation: 4002;
|
|
190
266
|
/** Auth/policy: bad key, missing scope, or revoked token. */
|
|
191
267
|
readonly PolicyViolation: 1008;
|
|
192
268
|
/** Engine/internal error. */
|
|
@@ -214,6 +290,7 @@ export declare const ErrorCode: {
|
|
|
214
290
|
readonly IdempotencyConflict: "idempotency_conflict";
|
|
215
291
|
readonly NotFound: "not_found";
|
|
216
292
|
readonly NumberInUse: "number_in_use";
|
|
293
|
+
readonly UnsupportedToolTransport: "unsupported_tool_transport";
|
|
217
294
|
};
|
|
218
295
|
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
219
296
|
/** A top-3 knowledge-base passage attached to Cue (grounded) finals. */
|
|
@@ -303,28 +380,59 @@ export interface WebSocketLike {
|
|
|
303
380
|
reason: string;
|
|
304
381
|
}) => void) | null;
|
|
305
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* The non-secret marker every PyAI realtime client offers ALONGSIDE its
|
|
385
|
+
* `pyai-key.<KEY>` credential token.
|
|
386
|
+
*
|
|
387
|
+
* A browser cannot set `Authorization` on a WebSocket, so the key rides in
|
|
388
|
+
* `Sec-WebSocket-Protocol`. RFC 6455 makes the server echo the subprotocol it
|
|
389
|
+
* SELECTS, so a server that selects the credential publishes the credential —
|
|
390
|
+
* which api.pyai.com did until 2026-09-07. The edge now echoes only this
|
|
391
|
+
* marker. Offering it is not optional for Node clients: `ws` throws
|
|
392
|
+
* "Server sent no subprotocol" when it offered subprotocols and the 101
|
|
393
|
+
* selected none, and RFC 6455 lets the server select only a value the client
|
|
394
|
+
* actually offered.
|
|
395
|
+
*/
|
|
396
|
+
export declare const REALTIME_SUBPROTOCOL_MARKER = "pyai.v1";
|
|
306
397
|
export type WebSocketCtor = new (url: string, protocols?: string | string[]) => WebSocketLike;
|
|
307
398
|
export interface HearStreamOptions {
|
|
308
399
|
/** Streaming STT model. Server default `pyai-hear`. */
|
|
309
400
|
model?: string;
|
|
310
|
-
/**
|
|
311
|
-
|
|
312
|
-
* and does not enable language detection. Other values receive
|
|
313
|
-
* `400 unsupported_language`.
|
|
314
|
-
*/
|
|
315
|
-
language?: "en";
|
|
401
|
+
/** Omit or use auto for automatic detection; an explicit code pins recognition. */
|
|
402
|
+
language?: "auto" | "en" | "es" | "fr" | "de" | "hi" | "it" | "pt" | "nl";
|
|
316
403
|
/** Input PCM sample rate in Hz. Default 16000 server-side. */
|
|
317
404
|
sampleRate?: number;
|
|
318
405
|
/** Audio frame encoding. Default "pcm16". */
|
|
319
|
-
encoding?: "pcm16"
|
|
406
|
+
encoding?: "pcm16";
|
|
320
407
|
/** Emit eager partial hypotheses. Default true server-side. */
|
|
321
408
|
interimResults?: boolean;
|
|
322
409
|
/**
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
410
|
+
* Tri-state number formatting on **final** transcripts. `true` forces digits,
|
|
411
|
+
* `false` keeps spoken form, omitted keeps the live engine default (ITN on).
|
|
412
|
+
* Never applied to interim partials. Independent of `smartFormat`.
|
|
326
413
|
*/
|
|
327
414
|
numerals?: boolean;
|
|
415
|
+
/**
|
|
416
|
+
* Opt-in English punctuation and sentence capitalization on **final**
|
|
417
|
+
* transcripts only. Interim partials are never formatted. Default false.
|
|
418
|
+
*/
|
|
419
|
+
smartFormat?: boolean;
|
|
420
|
+
/**
|
|
421
|
+
* Spoken punctuation commands (`period`, `comma`, `new paragraph`,
|
|
422
|
+
* `question mark`) on finals only. Separate from `smartFormat`. Off by default.
|
|
423
|
+
*/
|
|
424
|
+
dictation?: boolean;
|
|
425
|
+
/**
|
|
426
|
+
* Strip `um` / `uh` / `umm` / `uhh` / `er` on finals. Off by default.
|
|
427
|
+
* Do not enable on legal or compliance audio by default.
|
|
428
|
+
*/
|
|
429
|
+
dropFillers?: boolean;
|
|
430
|
+
/**
|
|
431
|
+
* Per-session names, brands, products, or other distinctive terms. PyAI
|
|
432
|
+
* sanitizes up to five effective terms. Request terms come first, then stored
|
|
433
|
+
* `hear_stream` suggestions fill remaining slots. The list is fixed at open.
|
|
434
|
+
*/
|
|
435
|
+
vocabulary?: string[];
|
|
328
436
|
/**
|
|
329
437
|
* Minimum trailing-pause length before an utterance may end (50-5000 ms).
|
|
330
438
|
* Turn detection may wait longer, bounded at `max(endpointingMs, 1500)`.
|
|
@@ -439,6 +547,8 @@ export declare const OmniEvent: {
|
|
|
439
547
|
readonly Flush: "flush";
|
|
440
548
|
/** Engine requests a client-loop tool invocation. */
|
|
441
549
|
readonly ToolCall: "tool_call";
|
|
550
|
+
/** A write tool is waiting for the caller to confirm; it has not run. */
|
|
551
|
+
readonly ToolConfirmationRequired: "tool_confirmation_required";
|
|
442
552
|
/** Session is closing; see close code. */
|
|
443
553
|
readonly SessionEnd: "session_end";
|
|
444
554
|
/** Server fault frame. */
|
|
@@ -471,8 +581,6 @@ export interface OmniToolDef {
|
|
|
471
581
|
name: string;
|
|
472
582
|
description?: string;
|
|
473
583
|
parameters?: Record<string, unknown>;
|
|
474
|
-
/** When set, engine-POST mode. Omit for client-loop (default). */
|
|
475
|
-
endpoint?: string;
|
|
476
584
|
}
|
|
477
585
|
export interface OmniToolCallFrame {
|
|
478
586
|
event: "tool_call";
|
|
@@ -508,7 +616,9 @@ export interface OmniConfigure {
|
|
|
508
616
|
* staged, see the Language support reference.
|
|
509
617
|
*/
|
|
510
618
|
language?: "en" | "fr" | "es" | "de" | "hi";
|
|
511
|
-
/** Function calling
|
|
619
|
+
/** Function calling: hosted catalog names, client-loop schemas, or names of
|
|
620
|
+
* server tools already registered with POST /v1/tools. An inline `endpoint`
|
|
621
|
+
* is rejected with `unsupported_tool_transport`. */
|
|
512
622
|
tools?: OmniToolDef[];
|
|
513
623
|
/** Forward-compatible: any other key the engine honors. */
|
|
514
624
|
[k: string]: unknown;
|
|
@@ -874,8 +984,45 @@ export interface RecapCallSummary {
|
|
|
874
984
|
created_at?: number;
|
|
875
985
|
completed_at?: number | null;
|
|
876
986
|
}
|
|
987
|
+
export interface RecapActionItem {
|
|
988
|
+
owner?: string | null;
|
|
989
|
+
task: string;
|
|
990
|
+
due?: string | null;
|
|
991
|
+
}
|
|
992
|
+
export interface RecapTalkRatio {
|
|
993
|
+
agent: number;
|
|
994
|
+
customer: number;
|
|
995
|
+
}
|
|
996
|
+
export interface RecapSignal {
|
|
997
|
+
kind: string;
|
|
998
|
+
text: string;
|
|
999
|
+
at_s?: number | null;
|
|
1000
|
+
}
|
|
1001
|
+
export interface RecapRecord {
|
|
1002
|
+
format: "recap.record.v1";
|
|
1003
|
+
tldr: string | null;
|
|
1004
|
+
summary: string | null;
|
|
1005
|
+
action_items: RecapActionItem[];
|
|
1006
|
+
disposition: string | null;
|
|
1007
|
+
next_steps: string | null;
|
|
1008
|
+
talk_ratio: RecapTalkRatio | null;
|
|
1009
|
+
signals: RecapSignal[];
|
|
1010
|
+
fields: Record<string, unknown>;
|
|
1011
|
+
rep?: Record<string, unknown>;
|
|
1012
|
+
manager?: Record<string, unknown>;
|
|
1013
|
+
ops?: Record<string, unknown>;
|
|
1014
|
+
}
|
|
877
1015
|
export interface RecapCall extends RecapCallSummary {
|
|
878
|
-
record?:
|
|
1016
|
+
record?: RecapRecord;
|
|
1017
|
+
transcript?: {
|
|
1018
|
+
format: "utterances.v1";
|
|
1019
|
+
utterances: Array<{
|
|
1020
|
+
speaker_role: "agent" | "customer";
|
|
1021
|
+
text: string;
|
|
1022
|
+
offset_s: number;
|
|
1023
|
+
duration_s: number;
|
|
1024
|
+
}>;
|
|
1025
|
+
};
|
|
879
1026
|
error?: string | null;
|
|
880
1027
|
crm_write_status?: string | null;
|
|
881
1028
|
}
|
|
@@ -994,19 +1141,43 @@ export declare class PyAI {
|
|
|
994
1141
|
list: (params?: {
|
|
995
1142
|
gender?: string;
|
|
996
1143
|
region?: string;
|
|
1144
|
+
language?: string;
|
|
1145
|
+
tier?: "standard" | "natural";
|
|
1146
|
+
q?: string;
|
|
997
1147
|
}) => Promise<ListResponse<Voice>>;
|
|
998
1148
|
get: (id: string) => Promise<Voice>;
|
|
999
1149
|
};
|
|
1150
|
+
hear: {
|
|
1151
|
+
vocabulary: {
|
|
1152
|
+
/** Read organization-owned vocabulary. Scope `hear:configure`. */
|
|
1153
|
+
get: () => Promise<HearVocabulary>;
|
|
1154
|
+
/** Replace vocabulary and activation profiles. Scope `hear:configure`. */
|
|
1155
|
+
set: (input: HearVocabularyInput) => Promise<HearVocabulary>;
|
|
1156
|
+
};
|
|
1157
|
+
};
|
|
1158
|
+
agents: {
|
|
1159
|
+
list: () => Promise<ListResponse<Agent>>;
|
|
1160
|
+
get: (agentId: string) => Promise<Agent>;
|
|
1161
|
+
create: (input: AgentConfig & {
|
|
1162
|
+
name: string;
|
|
1163
|
+
}) => Promise<Agent>;
|
|
1164
|
+
update: (agentId: string, patch: AgentConfig) => Promise<Agent>;
|
|
1165
|
+
delete: (agentId: string) => Promise<{
|
|
1166
|
+
object: "agent.deleted";
|
|
1167
|
+
agent_id: string;
|
|
1168
|
+
deleted: boolean;
|
|
1169
|
+
}>;
|
|
1170
|
+
};
|
|
1000
1171
|
audio: {
|
|
1001
1172
|
/** Text-to-speech. Returns the raw audio bytes (default WAV). */
|
|
1002
1173
|
speech: (params: SpeechParams) => Promise<ArrayBuffer>;
|
|
1003
1174
|
/**
|
|
1004
1175
|
* Text-to-speech, streamed. Resolves as soon as the response headers arrive
|
|
1005
1176
|
* with the body as a `ReadableStream` of audio bytes, so you can start
|
|
1006
|
-
* playback or forward the audio at the first chunk
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1177
|
+
* playback or forward the audio at the first chunk. Use `pcm`, `wav`,
|
|
1178
|
+
* or G.711 for streaming; `mp3` and `opus` are buffered server-side.
|
|
1179
|
+
* Consume the stream immediately and cancel its reader when stopping early.
|
|
1180
|
+
* HTTP connection reuse and protocol negotiation are controlled by fetch.
|
|
1010
1181
|
*/
|
|
1011
1182
|
speechStream: (params: SpeechParams) => Promise<ReadableStream<Uint8Array>>;
|
|
1012
1183
|
/** Synchronous speech-to-text (multipart upload). */
|
|
@@ -1015,11 +1186,13 @@ export declare class PyAI {
|
|
|
1015
1186
|
file: Blob;
|
|
1016
1187
|
filename?: string;
|
|
1017
1188
|
model?: string;
|
|
1018
|
-
/**
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1189
|
+
/** Optional language hint. Omission enables automatic detection. */
|
|
1190
|
+
language?: "en" | "es" | "fr" | "de" | "hi" | "it" | "pt" | "nl";
|
|
1191
|
+
numerals?: boolean;
|
|
1192
|
+
smart_format?: boolean;
|
|
1193
|
+
dictation?: boolean;
|
|
1194
|
+
drop_fillers?: boolean;
|
|
1195
|
+
vocabulary?: string[];
|
|
1023
1196
|
response_format?: "json" | "text" | "verbose_json";
|
|
1024
1197
|
/**
|
|
1025
1198
|
* Deterministic seed for reproducible eval runs. Forward-compatible:
|
|
@@ -1200,7 +1373,7 @@ export declare class PyAI {
|
|
|
1200
1373
|
* realtime. **Call this from your server** with a secret key holding
|
|
1201
1374
|
* `omni:session`; never ship the secret key to a page. Hand the returned
|
|
1202
1375
|
* `token` to the browser, which connects with
|
|
1203
|
-
* `new WebSocket(session.url, ["pyai-key." + session.token])`. The token
|
|
1376
|
+
* `new WebSocket(session.url, ["pyai.v1", "pyai-key." + session.token])`. The token
|
|
1204
1377
|
* expires after `ttlSeconds` (default 60s) and only works from
|
|
1205
1378
|
* `allowedOrigins`. Scope `omni:session`.
|
|
1206
1379
|
*/
|
|
@@ -1218,8 +1391,15 @@ export declare class PyAI {
|
|
|
1218
1391
|
};
|
|
1219
1392
|
/** Build the canonical Omni WebSocket URL. */
|
|
1220
1393
|
realtimeURL(opts?: RealtimeOptions): string;
|
|
1221
|
-
/** The subprotocol that carries the key on a WS upgrade
|
|
1394
|
+
/** The subprotocol token that carries the key on a WS upgrade. */
|
|
1222
1395
|
realtimeSubprotocol(): string;
|
|
1396
|
+
/**
|
|
1397
|
+
* The full subprotocol list to open a PyAI WebSocket with:
|
|
1398
|
+
* `[marker, credential]`. Always pass BOTH — see
|
|
1399
|
+
* {@link REALTIME_SUBPROTOCOL_MARKER}. Pass `token` to use an ephemeral
|
|
1400
|
+
* session token (from `omni.createSession`) instead of the secret key.
|
|
1401
|
+
*/
|
|
1402
|
+
realtimeSubprotocols(token?: string): string[];
|
|
1223
1403
|
/** Build the Hear streaming-STT WebSocket URL (`/v1/audio/transcriptions/stream`). */
|
|
1224
1404
|
hearStreamURL(opts?: HearStreamOptions): string;
|
|
1225
1405
|
/** Build the AMD detection WebSocket URL (`/v1/amd/stream`). */
|
package/dist/index.js
CHANGED
|
@@ -62,10 +62,16 @@ export const HearFrameType = {
|
|
|
62
62
|
/** Server-side fault frame. */
|
|
63
63
|
Error: "error",
|
|
64
64
|
};
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* WebSocket close codes used across the PyAI realtime/streaming surfaces.
|
|
67
|
+
* Browser callers may initiate only Normal or values in 3000-4999; the
|
|
68
|
+
* standards-reserved values below are server close observations.
|
|
69
|
+
*/
|
|
66
70
|
export const WSCloseCode = {
|
|
67
71
|
/** Normal closure. */
|
|
68
72
|
Normal: 1000,
|
|
73
|
+
/** Browser-safe private application code for a malformed peer protocol frame. */
|
|
74
|
+
ProtocolViolation: 4002,
|
|
69
75
|
/** Auth/policy: bad key, missing scope, or revoked token. */
|
|
70
76
|
PolicyViolation: 1008,
|
|
71
77
|
/** Engine/internal error. */
|
|
@@ -92,7 +98,22 @@ export const ErrorCode = {
|
|
|
92
98
|
IdempotencyConflict: "idempotency_conflict",
|
|
93
99
|
NotFound: "not_found",
|
|
94
100
|
NumberInUse: "number_in_use",
|
|
101
|
+
UnsupportedToolTransport: "unsupported_tool_transport",
|
|
95
102
|
};
|
|
103
|
+
/**
|
|
104
|
+
* The non-secret marker every PyAI realtime client offers ALONGSIDE its
|
|
105
|
+
* `pyai-key.<KEY>` credential token.
|
|
106
|
+
*
|
|
107
|
+
* A browser cannot set `Authorization` on a WebSocket, so the key rides in
|
|
108
|
+
* `Sec-WebSocket-Protocol`. RFC 6455 makes the server echo the subprotocol it
|
|
109
|
+
* SELECTS, so a server that selects the credential publishes the credential —
|
|
110
|
+
* which api.pyai.com did until 2026-09-07. The edge now echoes only this
|
|
111
|
+
* marker. Offering it is not optional for Node clients: `ws` throws
|
|
112
|
+
* "Server sent no subprotocol" when it offered subprotocols and the 101
|
|
113
|
+
* selected none, and RFC 6455 lets the server select only a value the client
|
|
114
|
+
* actually offered.
|
|
115
|
+
*/
|
|
116
|
+
export const REALTIME_SUBPROTOCOL_MARKER = "pyai.v1";
|
|
96
117
|
/**
|
|
97
118
|
* A live Hear streaming-STT session. Hides the frame protocol: stream audio
|
|
98
119
|
* with {@link HearStream.sendAudio}, get `onPartial`/`onFinal`/`onError`
|
|
@@ -114,7 +135,7 @@ export class HearStream {
|
|
|
114
135
|
if (!WS) {
|
|
115
136
|
throw new Error("No global WebSocket available; pass options.webSocket (e.g. the `ws` package) to transcriptions.stream()");
|
|
116
137
|
}
|
|
117
|
-
this.ws = new WS(url, [subprotocol]);
|
|
138
|
+
this.ws = new WS(url, [REALTIME_SUBPROTOCOL_MARKER, subprotocol]);
|
|
118
139
|
this.ws.onopen = () => {
|
|
119
140
|
opts.onOpen?.();
|
|
120
141
|
};
|
|
@@ -205,7 +226,7 @@ export class AmdStream {
|
|
|
205
226
|
if (!WS) {
|
|
206
227
|
throw new Error("No global WebSocket available; pass options.webSocket (e.g. the `ws` package) to amd.stream()");
|
|
207
228
|
}
|
|
208
|
-
this.ws = new WS(url, [subprotocol]);
|
|
229
|
+
this.ws = new WS(url, [REALTIME_SUBPROTOCOL_MARKER, subprotocol]);
|
|
209
230
|
this.ws.onopen = () => opts.onOpen?.();
|
|
210
231
|
this.ws.onmessage = (ev) => this.handleMessage(ev.data);
|
|
211
232
|
this.ws.onerror = (ev) => opts.onError?.(ev instanceof Error ? ev : new Error("WebSocket error"));
|
|
@@ -277,6 +298,8 @@ export const OmniEvent = {
|
|
|
277
298
|
Flush: "flush",
|
|
278
299
|
/** Engine requests a client-loop tool invocation. */
|
|
279
300
|
ToolCall: "tool_call",
|
|
301
|
+
/** A write tool is waiting for the caller to confirm; it has not run. */
|
|
302
|
+
ToolConfirmationRequired: "tool_confirmation_required",
|
|
280
303
|
/** Session is closing; see close code. */
|
|
281
304
|
SessionEnd: "session_end",
|
|
282
305
|
/** Server fault frame. */
|
|
@@ -413,7 +436,7 @@ export class OmniConnection {
|
|
|
413
436
|
if (!WS) {
|
|
414
437
|
throw new Error("No global WebSocket available; pass options.webSocket (e.g. the `ws` package) to omni.connect()");
|
|
415
438
|
}
|
|
416
|
-
this.ws = new WS(url, [subprotocol]);
|
|
439
|
+
this.ws = new WS(url, [REALTIME_SUBPROTOCOL_MARKER, subprotocol]);
|
|
417
440
|
this.ws.onopen = () => {
|
|
418
441
|
if (opts.configure) {
|
|
419
442
|
try {
|
|
@@ -679,12 +702,41 @@ export class PyAI {
|
|
|
679
702
|
q.set("gender", params.gender);
|
|
680
703
|
if (params.region)
|
|
681
704
|
q.set("region", params.region);
|
|
705
|
+
if (params.language)
|
|
706
|
+
q.set("language", params.language);
|
|
707
|
+
if (params.tier)
|
|
708
|
+
q.set("tier", params.tier);
|
|
709
|
+
if (params.q)
|
|
710
|
+
q.set("q", params.q);
|
|
682
711
|
const qs = q.toString();
|
|
683
712
|
const page = await this.getJson(`/v1/voices${qs ? `?${qs}` : ""}`);
|
|
684
713
|
return { ...page, data: page.data.map(normalizeVoice) };
|
|
685
714
|
},
|
|
686
715
|
get: async (id) => normalizeVoice(await this.getJson(`/v1/voices/${encodeURIComponent(id)}`)),
|
|
687
716
|
};
|
|
717
|
+
// --- Hear organization settings ----------------------------------------
|
|
718
|
+
hear = {
|
|
719
|
+
vocabulary: {
|
|
720
|
+
/** Read organization-owned vocabulary. Scope `hear:configure`. */
|
|
721
|
+
get: () => this.getJson("/v1/hear/vocabulary"),
|
|
722
|
+
/** Replace vocabulary and activation profiles. Scope `hear:configure`. */
|
|
723
|
+
set: (input) => this.putJson("/v1/hear/vocabulary", {
|
|
724
|
+
terms: input.terms,
|
|
725
|
+
enabled_for: input.enabledFor,
|
|
726
|
+
}),
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
// --- managed Agents ----------------------------------------------------
|
|
730
|
+
agents = {
|
|
731
|
+
list: () => this.getJson("/v1/agents"),
|
|
732
|
+
get: (agentId) => this.getJson(`/v1/agents/${encodeURIComponent(agentId)}`),
|
|
733
|
+
create: (input) => this.postJson("/v1/agents", input),
|
|
734
|
+
update: (agentId, patch) => this.postJson(`/v1/agents/${encodeURIComponent(agentId)}`, patch),
|
|
735
|
+
delete: async (agentId) => {
|
|
736
|
+
const response = await this.deleteReq(`/v1/agents/${encodeURIComponent(agentId)}`);
|
|
737
|
+
return (await response.json());
|
|
738
|
+
},
|
|
739
|
+
};
|
|
688
740
|
// --- audio --------------------------------------------------------------
|
|
689
741
|
audio = {
|
|
690
742
|
/** Text-to-speech. Returns the raw audio bytes (default WAV). */
|
|
@@ -700,10 +752,10 @@ export class PyAI {
|
|
|
700
752
|
/**
|
|
701
753
|
* Text-to-speech, streamed. Resolves as soon as the response headers arrive
|
|
702
754
|
* with the body as a `ReadableStream` of audio bytes, so you can start
|
|
703
|
-
* playback or forward the audio at the first chunk
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
755
|
+
* playback or forward the audio at the first chunk. Use `pcm`, `wav`,
|
|
756
|
+
* or G.711 for streaming; `mp3` and `opus` are buffered server-side.
|
|
757
|
+
* Consume the stream immediately and cancel its reader when stopping early.
|
|
758
|
+
* HTTP connection reuse and protocol negotiation are controlled by fetch.
|
|
707
759
|
*/
|
|
708
760
|
speechStream: async (params) => {
|
|
709
761
|
assertActiveSpeechParams(params);
|
|
@@ -724,6 +776,16 @@ export class PyAI {
|
|
|
724
776
|
form.set("model", params.model ?? "pyai-hear");
|
|
725
777
|
if (params.language)
|
|
726
778
|
form.set("language", params.language);
|
|
779
|
+
if (params.numerals !== undefined)
|
|
780
|
+
form.set("numerals", String(params.numerals));
|
|
781
|
+
if (params.smart_format !== undefined)
|
|
782
|
+
form.set("smart_format", String(params.smart_format));
|
|
783
|
+
if (params.dictation !== undefined)
|
|
784
|
+
form.set("dictation", String(params.dictation));
|
|
785
|
+
if (params.drop_fillers !== undefined)
|
|
786
|
+
form.set("drop_fillers", String(params.drop_fillers));
|
|
787
|
+
if (params.vocabulary?.length)
|
|
788
|
+
form.set("vocabulary", params.vocabulary.join(","));
|
|
727
789
|
if (params.response_format)
|
|
728
790
|
form.set("response_format", params.response_format);
|
|
729
791
|
if (params.seed !== undefined)
|
|
@@ -985,7 +1047,7 @@ export class PyAI {
|
|
|
985
1047
|
* realtime. **Call this from your server** with a secret key holding
|
|
986
1048
|
* `omni:session`; never ship the secret key to a page. Hand the returned
|
|
987
1049
|
* `token` to the browser, which connects with
|
|
988
|
-
* `new WebSocket(session.url, ["pyai-key." + session.token])`. The token
|
|
1050
|
+
* `new WebSocket(session.url, ["pyai.v1", "pyai-key." + session.token])`. The token
|
|
989
1051
|
* expires after `ttlSeconds` (default 60s) and only works from
|
|
990
1052
|
* `allowedOrigins`. Scope `omni:session`.
|
|
991
1053
|
*/
|
|
@@ -1035,14 +1097,29 @@ export class PyAI {
|
|
|
1035
1097
|
const qs = q.toString();
|
|
1036
1098
|
return `${wsBase}/v1/omni${qs ? `?${qs}` : ""}`;
|
|
1037
1099
|
}
|
|
1038
|
-
/** The subprotocol that carries the key on a WS upgrade
|
|
1100
|
+
/** The subprotocol token that carries the key on a WS upgrade. */
|
|
1039
1101
|
realtimeSubprotocol() {
|
|
1040
1102
|
return `pyai-key.${this.apiKey}`;
|
|
1041
1103
|
}
|
|
1104
|
+
/**
|
|
1105
|
+
* The full subprotocol list to open a PyAI WebSocket with:
|
|
1106
|
+
* `[marker, credential]`. Always pass BOTH — see
|
|
1107
|
+
* {@link REALTIME_SUBPROTOCOL_MARKER}. Pass `token` to use an ephemeral
|
|
1108
|
+
* session token (from `omni.createSession`) instead of the secret key.
|
|
1109
|
+
*/
|
|
1110
|
+
realtimeSubprotocols(token) {
|
|
1111
|
+
return [
|
|
1112
|
+
REALTIME_SUBPROTOCOL_MARKER,
|
|
1113
|
+
token ? `pyai-key.${token}` : this.realtimeSubprotocol(),
|
|
1114
|
+
];
|
|
1115
|
+
}
|
|
1042
1116
|
/** Build the Hear streaming-STT WebSocket URL (`/v1/audio/transcriptions/stream`). */
|
|
1043
1117
|
hearStreamURL(opts = {}) {
|
|
1044
1118
|
const wsBase = this.baseURL.replace(/^http/, "ws");
|
|
1045
1119
|
const q = new URLSearchParams(opts.query ?? {});
|
|
1120
|
+
// `context` is private to PyAI's adapter-to-server hop. Never expose or
|
|
1121
|
+
// forward it from the public SDK escape hatch.
|
|
1122
|
+
q.delete("context");
|
|
1046
1123
|
q.set("protocol", "pyai-hear-v1");
|
|
1047
1124
|
if (opts.model)
|
|
1048
1125
|
q.set("model", opts.model);
|
|
@@ -1056,6 +1133,15 @@ export class PyAI {
|
|
|
1056
1133
|
q.set("interim_results", String(opts.interimResults));
|
|
1057
1134
|
if (opts.numerals !== undefined)
|
|
1058
1135
|
q.set("numerals", String(opts.numerals));
|
|
1136
|
+
if (opts.smartFormat !== undefined)
|
|
1137
|
+
q.set("smart_format", String(opts.smartFormat));
|
|
1138
|
+
if (opts.dictation !== undefined)
|
|
1139
|
+
q.set("dictation", String(opts.dictation));
|
|
1140
|
+
if (opts.dropFillers !== undefined)
|
|
1141
|
+
q.set("drop_fillers", String(opts.dropFillers));
|
|
1142
|
+
if (opts.vocabulary?.length) {
|
|
1143
|
+
q.set("vocabulary", JSON.stringify(opts.vocabulary));
|
|
1144
|
+
}
|
|
1059
1145
|
if (opts.endpointingMs !== undefined)
|
|
1060
1146
|
q.set("endpointing_ms", String(opts.endpointingMs));
|
|
1061
1147
|
const qs = q.toString();
|
|
@@ -1080,8 +1166,8 @@ export class PyAI {
|
|
|
1080
1166
|
connectRealtime(opts = {}) {
|
|
1081
1167
|
const WS = globalThis.WebSocket;
|
|
1082
1168
|
if (!WS)
|
|
1083
|
-
throw new Error("No global WebSocket; use realtimeURL()/
|
|
1084
|
-
return new WS(this.realtimeURL(opts),
|
|
1169
|
+
throw new Error("No global WebSocket; use realtimeURL()/realtimeSubprotocols() with a WS library");
|
|
1170
|
+
return new WS(this.realtimeURL(opts), this.realtimeSubprotocols());
|
|
1085
1171
|
}
|
|
1086
1172
|
}
|
|
1087
1173
|
export default PyAI;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyai/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Official TypeScript/JavaScript SDK for PyAI, speech-to-text (Hear), text-to-speech (Speak), realtime voice agents (Omni), and call compliance (Trace).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,20 @@ export interface Voice {
|
|
|
41
41
|
name?: string;
|
|
42
42
|
gender?: string;
|
|
43
43
|
region?: string;
|
|
44
|
+
language?: string;
|
|
45
|
+
/** Customer-facing quality tier. */
|
|
46
|
+
tier?: "standard" | "natural";
|
|
47
|
+
/** Permanent convenience inputs accepted on the advertised surfaces. */
|
|
48
|
+
aliases?: string[];
|
|
49
|
+
/** Product surfaces on which this stock voice can be selected. */
|
|
50
|
+
available_on?: Array<"speak" | "omni">;
|
|
51
|
+
/** Accepted Speak delivery modes. Empty means the voice has no Speak surface. */
|
|
52
|
+
synthesis_modes?: Array<"streaming" | "async">;
|
|
53
|
+
/** Voice-specific amount on top of the selected product's base rate. */
|
|
54
|
+
pricing?: {
|
|
55
|
+
included_in_base_price: boolean;
|
|
56
|
+
additional_price_usd_per_minute: number;
|
|
57
|
+
};
|
|
44
58
|
[k: string]: unknown;
|
|
45
59
|
}
|
|
46
60
|
|
|
@@ -104,6 +118,13 @@ export interface SpeechParams {
|
|
|
104
118
|
input: string;
|
|
105
119
|
voice?: string;
|
|
106
120
|
model?: SpeakModel;
|
|
121
|
+
/**
|
|
122
|
+
* Delivery mode. The API defaults to true and every catalog voice accepts
|
|
123
|
+
* both values. Set false when you want one complete buffered body with a
|
|
124
|
+
* `Content-Length`. A voice whose serving fleet has no streaming lane is
|
|
125
|
+
* rendered buffered either way and says so with `x-pyai-stream: buffered`.
|
|
126
|
+
*/
|
|
127
|
+
stream?: boolean;
|
|
107
128
|
/**
|
|
108
129
|
* Output container/codec, resampled+encoded server-side. One of
|
|
109
130
|
* {@link SpeechFormat}, anything else is a `400 unsupported_format`. Omit for
|
|
@@ -150,8 +171,62 @@ export interface CreateJobParams {
|
|
|
150
171
|
diarize?: boolean;
|
|
151
172
|
channel?: boolean;
|
|
152
173
|
numerals?: boolean;
|
|
174
|
+
smart_format?: boolean;
|
|
175
|
+
dictation?: boolean;
|
|
176
|
+
drop_fillers?: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Per-job names, brands, products, or distinctive terms. PyAI keeps up to
|
|
179
|
+
* five valid entries after trimming, deduplication, and common-word
|
|
180
|
+
* filtering.
|
|
181
|
+
*/
|
|
182
|
+
vocabulary?: string[];
|
|
153
183
|
output_formats?: Array<"json" | "srt" | "vtt">;
|
|
154
184
|
webhook_url?: string;
|
|
185
|
+
call_id?: string;
|
|
186
|
+
pack_id?: string;
|
|
187
|
+
call_direction?: "inbound" | "outbound";
|
|
188
|
+
customer_name?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type HearVocabularyProfile = "batch" | "hear_stream";
|
|
192
|
+
|
|
193
|
+
export interface HearVocabularyInput {
|
|
194
|
+
/** Organization-owned names, brands, products, or distinctive terms. */
|
|
195
|
+
terms: string[];
|
|
196
|
+
/** Stored terms remain inert unless a use case is selected here. */
|
|
197
|
+
enabledFor: HearVocabularyProfile[];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface HearVocabulary {
|
|
201
|
+
object: "hear.vocabulary";
|
|
202
|
+
/** Sanitized stored list, at most five terms. */
|
|
203
|
+
terms: string[];
|
|
204
|
+
/** Use cases that may add stored suggestions. */
|
|
205
|
+
enabled_for: HearVocabularyProfile[];
|
|
206
|
+
/** Unix milliseconds, or null before the first save. */
|
|
207
|
+
updated_at: number | null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface AgentConfig {
|
|
211
|
+
name?: string;
|
|
212
|
+
persona_system_prompt?: string | null;
|
|
213
|
+
greeting?: string | null;
|
|
214
|
+
voice_id?: string | null;
|
|
215
|
+
language?: "en" | "fr" | "es" | "de" | "hi" | null;
|
|
216
|
+
/**
|
|
217
|
+
* Opt-in speech-recognition vocabulary. PyAI sanitizes and keeps at most
|
|
218
|
+
* five terms. An empty list or null turns it off.
|
|
219
|
+
*/
|
|
220
|
+
vocabulary?: string[] | null;
|
|
221
|
+
[key: string]: unknown;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface Agent {
|
|
225
|
+
object: "agent";
|
|
226
|
+
agent_id: string;
|
|
227
|
+
name: string;
|
|
228
|
+
vocabulary: string[];
|
|
229
|
+
[key: string]: unknown;
|
|
155
230
|
}
|
|
156
231
|
|
|
157
232
|
export interface RealtimeOptions {
|
|
@@ -226,10 +301,16 @@ export const HearFrameType = {
|
|
|
226
301
|
} as const;
|
|
227
302
|
export type HearFrameType = (typeof HearFrameType)[keyof typeof HearFrameType];
|
|
228
303
|
|
|
229
|
-
/**
|
|
304
|
+
/**
|
|
305
|
+
* WebSocket close codes used across the PyAI realtime/streaming surfaces.
|
|
306
|
+
* Browser callers may initiate only Normal or values in 3000-4999; the
|
|
307
|
+
* standards-reserved values below are server close observations.
|
|
308
|
+
*/
|
|
230
309
|
export const WSCloseCode = {
|
|
231
310
|
/** Normal closure. */
|
|
232
311
|
Normal: 1000,
|
|
312
|
+
/** Browser-safe private application code for a malformed peer protocol frame. */
|
|
313
|
+
ProtocolViolation: 4002,
|
|
233
314
|
/** Auth/policy: bad key, missing scope, or revoked token. */
|
|
234
315
|
PolicyViolation: 1008,
|
|
235
316
|
/** Engine/internal error. */
|
|
@@ -258,6 +339,7 @@ export const ErrorCode = {
|
|
|
258
339
|
IdempotencyConflict: "idempotency_conflict",
|
|
259
340
|
NotFound: "not_found",
|
|
260
341
|
NumberInUse: "number_in_use",
|
|
342
|
+
UnsupportedToolTransport: "unsupported_tool_transport",
|
|
261
343
|
} as const;
|
|
262
344
|
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
263
345
|
|
|
@@ -366,29 +448,61 @@ export interface WebSocketLike {
|
|
|
366
448
|
onclose: ((ev: { code: number; reason: string }) => void) | null;
|
|
367
449
|
}
|
|
368
450
|
|
|
451
|
+
/**
|
|
452
|
+
* The non-secret marker every PyAI realtime client offers ALONGSIDE its
|
|
453
|
+
* `pyai-key.<KEY>` credential token.
|
|
454
|
+
*
|
|
455
|
+
* A browser cannot set `Authorization` on a WebSocket, so the key rides in
|
|
456
|
+
* `Sec-WebSocket-Protocol`. RFC 6455 makes the server echo the subprotocol it
|
|
457
|
+
* SELECTS, so a server that selects the credential publishes the credential —
|
|
458
|
+
* which api.pyai.com did until 2026-09-07. The edge now echoes only this
|
|
459
|
+
* marker. Offering it is not optional for Node clients: `ws` throws
|
|
460
|
+
* "Server sent no subprotocol" when it offered subprotocols and the 101
|
|
461
|
+
* selected none, and RFC 6455 lets the server select only a value the client
|
|
462
|
+
* actually offered.
|
|
463
|
+
*/
|
|
464
|
+
export const REALTIME_SUBPROTOCOL_MARKER = "pyai.v1";
|
|
465
|
+
|
|
369
466
|
export type WebSocketCtor = new (url: string, protocols?: string | string[]) => WebSocketLike;
|
|
370
467
|
|
|
371
468
|
export interface HearStreamOptions {
|
|
372
469
|
/** Streaming STT model. Server default `pyai-hear`. */
|
|
373
470
|
model?: string;
|
|
374
|
-
/**
|
|
375
|
-
|
|
376
|
-
* and does not enable language detection. Other values receive
|
|
377
|
-
* `400 unsupported_language`.
|
|
378
|
-
*/
|
|
379
|
-
language?: "en";
|
|
471
|
+
/** Omit or use auto for automatic detection; an explicit code pins recognition. */
|
|
472
|
+
language?: "auto" | "en" | "es" | "fr" | "de" | "hi" | "it" | "pt" | "nl";
|
|
380
473
|
/** Input PCM sample rate in Hz. Default 16000 server-side. */
|
|
381
474
|
sampleRate?: number;
|
|
382
475
|
/** Audio frame encoding. Default "pcm16". */
|
|
383
|
-
encoding?: "pcm16"
|
|
476
|
+
encoding?: "pcm16";
|
|
384
477
|
/** Emit eager partial hypotheses. Default true server-side. */
|
|
385
478
|
interimResults?: boolean;
|
|
386
479
|
/**
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
480
|
+
* Tri-state number formatting on **final** transcripts. `true` forces digits,
|
|
481
|
+
* `false` keeps spoken form, omitted keeps the live engine default (ITN on).
|
|
482
|
+
* Never applied to interim partials. Independent of `smartFormat`.
|
|
390
483
|
*/
|
|
391
484
|
numerals?: boolean;
|
|
485
|
+
/**
|
|
486
|
+
* Opt-in English punctuation and sentence capitalization on **final**
|
|
487
|
+
* transcripts only. Interim partials are never formatted. Default false.
|
|
488
|
+
*/
|
|
489
|
+
smartFormat?: boolean;
|
|
490
|
+
/**
|
|
491
|
+
* Spoken punctuation commands (`period`, `comma`, `new paragraph`,
|
|
492
|
+
* `question mark`) on finals only. Separate from `smartFormat`. Off by default.
|
|
493
|
+
*/
|
|
494
|
+
dictation?: boolean;
|
|
495
|
+
/**
|
|
496
|
+
* Strip `um` / `uh` / `umm` / `uhh` / `er` on finals. Off by default.
|
|
497
|
+
* Do not enable on legal or compliance audio by default.
|
|
498
|
+
*/
|
|
499
|
+
dropFillers?: boolean;
|
|
500
|
+
/**
|
|
501
|
+
* Per-session names, brands, products, or other distinctive terms. PyAI
|
|
502
|
+
* sanitizes up to five effective terms. Request terms come first, then stored
|
|
503
|
+
* `hear_stream` suggestions fill remaining slots. The list is fixed at open.
|
|
504
|
+
*/
|
|
505
|
+
vocabulary?: string[];
|
|
392
506
|
/**
|
|
393
507
|
* Minimum trailing-pause length before an utterance may end (50-5000 ms).
|
|
394
508
|
* Turn detection may wait longer, bounded at `max(endpointingMs, 1500)`.
|
|
@@ -454,7 +568,7 @@ export class HearStream {
|
|
|
454
568
|
"No global WebSocket available; pass options.webSocket (e.g. the `ws` package) to transcriptions.stream()",
|
|
455
569
|
);
|
|
456
570
|
}
|
|
457
|
-
this.ws = new WS(url, [subprotocol]);
|
|
571
|
+
this.ws = new WS(url, [REALTIME_SUBPROTOCOL_MARKER, subprotocol]);
|
|
458
572
|
this.ws.onopen = () => {
|
|
459
573
|
opts.onOpen?.();
|
|
460
574
|
};
|
|
@@ -553,7 +667,7 @@ export class AmdStream {
|
|
|
553
667
|
"No global WebSocket available; pass options.webSocket (e.g. the `ws` package) to amd.stream()",
|
|
554
668
|
);
|
|
555
669
|
}
|
|
556
|
-
this.ws = new WS(url, [subprotocol]);
|
|
670
|
+
this.ws = new WS(url, [REALTIME_SUBPROTOCOL_MARKER, subprotocol]);
|
|
557
671
|
this.ws.onopen = () => opts.onOpen?.();
|
|
558
672
|
this.ws.onmessage = (ev) => this.handleMessage(ev.data);
|
|
559
673
|
this.ws.onerror = (ev) => opts.onError?.(ev instanceof Error ? ev : new Error("WebSocket error"));
|
|
@@ -629,6 +743,8 @@ export const OmniEvent = {
|
|
|
629
743
|
Flush: "flush",
|
|
630
744
|
/** Engine requests a client-loop tool invocation. */
|
|
631
745
|
ToolCall: "tool_call",
|
|
746
|
+
/** A write tool is waiting for the caller to confirm; it has not run. */
|
|
747
|
+
ToolConfirmationRequired: "tool_confirmation_required",
|
|
632
748
|
/** Session is closing; see close code. */
|
|
633
749
|
SessionEnd: "session_end",
|
|
634
750
|
/** Server fault frame. */
|
|
@@ -762,8 +878,6 @@ export interface OmniToolDef {
|
|
|
762
878
|
name: string;
|
|
763
879
|
description?: string;
|
|
764
880
|
parameters?: Record<string, unknown>;
|
|
765
|
-
/** When set, engine-POST mode. Omit for client-loop (default). */
|
|
766
|
-
endpoint?: string;
|
|
767
881
|
}
|
|
768
882
|
|
|
769
883
|
export interface OmniToolCallFrame {
|
|
@@ -801,7 +915,9 @@ export interface OmniConfigure {
|
|
|
801
915
|
* staged, see the Language support reference.
|
|
802
916
|
*/
|
|
803
917
|
language?: "en" | "fr" | "es" | "de" | "hi";
|
|
804
|
-
/** Function calling
|
|
918
|
+
/** Function calling: hosted catalog names, client-loop schemas, or names of
|
|
919
|
+
* server tools already registered with POST /v1/tools. An inline `endpoint`
|
|
920
|
+
* is rejected with `unsupported_tool_transport`. */
|
|
805
921
|
tools?: OmniToolDef[];
|
|
806
922
|
/** Forward-compatible: any other key the engine honors. */
|
|
807
923
|
[k: string]: unknown;
|
|
@@ -894,7 +1010,7 @@ export class OmniConnection {
|
|
|
894
1010
|
"No global WebSocket available; pass options.webSocket (e.g. the `ws` package) to omni.connect()",
|
|
895
1011
|
);
|
|
896
1012
|
}
|
|
897
|
-
this.ws = new WS(url, [subprotocol]);
|
|
1013
|
+
this.ws = new WS(url, [REALTIME_SUBPROTOCOL_MARKER, subprotocol]);
|
|
898
1014
|
this.ws.onopen = () => {
|
|
899
1015
|
if (opts.configure) {
|
|
900
1016
|
try {
|
|
@@ -1359,8 +1475,41 @@ export interface RecapCallSummary {
|
|
|
1359
1475
|
completed_at?: number | null;
|
|
1360
1476
|
}
|
|
1361
1477
|
|
|
1478
|
+
export interface RecapActionItem {
|
|
1479
|
+
owner?: string | null;
|
|
1480
|
+
task: string;
|
|
1481
|
+
due?: string | null;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
export interface RecapTalkRatio {
|
|
1485
|
+
agent: number;
|
|
1486
|
+
customer: number;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
export interface RecapSignal {
|
|
1490
|
+
kind: string;
|
|
1491
|
+
text: string;
|
|
1492
|
+
at_s?: number | null;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
export interface RecapRecord {
|
|
1496
|
+
format: "recap.record.v1";
|
|
1497
|
+
tldr: string | null;
|
|
1498
|
+
summary: string | null;
|
|
1499
|
+
action_items: RecapActionItem[];
|
|
1500
|
+
disposition: string | null;
|
|
1501
|
+
next_steps: string | null;
|
|
1502
|
+
talk_ratio: RecapTalkRatio | null;
|
|
1503
|
+
signals: RecapSignal[];
|
|
1504
|
+
fields: Record<string, unknown>;
|
|
1505
|
+
rep?: Record<string, unknown>;
|
|
1506
|
+
manager?: Record<string, unknown>;
|
|
1507
|
+
ops?: Record<string, unknown>;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1362
1510
|
export interface RecapCall extends RecapCallSummary {
|
|
1363
|
-
record?:
|
|
1511
|
+
record?: RecapRecord;
|
|
1512
|
+
transcript?: { format: "utterances.v1"; utterances: Array<{ speaker_role: "agent" | "customer"; text: string; offset_s: number; duration_s: number }> };
|
|
1364
1513
|
error?: string | null;
|
|
1365
1514
|
crm_write_status?: string | null;
|
|
1366
1515
|
}
|
|
@@ -1574,10 +1723,19 @@ export class PyAI {
|
|
|
1574
1723
|
// --- voices -------------------------------------------------------------
|
|
1575
1724
|
|
|
1576
1725
|
voices = {
|
|
1577
|
-
list: async (params: {
|
|
1726
|
+
list: async (params: {
|
|
1727
|
+
gender?: string;
|
|
1728
|
+
region?: string;
|
|
1729
|
+
language?: string;
|
|
1730
|
+
tier?: "standard" | "natural";
|
|
1731
|
+
q?: string;
|
|
1732
|
+
} = {}): Promise<ListResponse<Voice>> => {
|
|
1578
1733
|
const q = new URLSearchParams();
|
|
1579
1734
|
if (params.gender) q.set("gender", params.gender);
|
|
1580
1735
|
if (params.region) q.set("region", params.region);
|
|
1736
|
+
if (params.language) q.set("language", params.language);
|
|
1737
|
+
if (params.tier) q.set("tier", params.tier);
|
|
1738
|
+
if (params.q) q.set("q", params.q);
|
|
1581
1739
|
const qs = q.toString();
|
|
1582
1740
|
const page = await this.getJson<ListResponse<Record<string, unknown>>>(
|
|
1583
1741
|
`/v1/voices${qs ? `?${qs}` : ""}`,
|
|
@@ -1592,6 +1750,46 @@ export class PyAI {
|
|
|
1592
1750
|
),
|
|
1593
1751
|
};
|
|
1594
1752
|
|
|
1753
|
+
// --- Hear organization settings ----------------------------------------
|
|
1754
|
+
|
|
1755
|
+
hear = {
|
|
1756
|
+
vocabulary: {
|
|
1757
|
+
/** Read organization-owned vocabulary. Scope `hear:configure`. */
|
|
1758
|
+
get: (): Promise<HearVocabulary> =>
|
|
1759
|
+
this.getJson("/v1/hear/vocabulary"),
|
|
1760
|
+
/** Replace vocabulary and activation profiles. Scope `hear:configure`. */
|
|
1761
|
+
set: (input: HearVocabularyInput): Promise<HearVocabulary> =>
|
|
1762
|
+
this.putJson("/v1/hear/vocabulary", {
|
|
1763
|
+
terms: input.terms,
|
|
1764
|
+
enabled_for: input.enabledFor,
|
|
1765
|
+
}),
|
|
1766
|
+
},
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
// --- managed Agents ----------------------------------------------------
|
|
1770
|
+
|
|
1771
|
+
agents = {
|
|
1772
|
+
list: (): Promise<ListResponse<Agent>> => this.getJson("/v1/agents"),
|
|
1773
|
+
get: (agentId: string): Promise<Agent> =>
|
|
1774
|
+
this.getJson(`/v1/agents/${encodeURIComponent(agentId)}`),
|
|
1775
|
+
create: (input: AgentConfig & { name: string }): Promise<Agent> =>
|
|
1776
|
+
this.postJson("/v1/agents", input),
|
|
1777
|
+
update: (agentId: string, patch: AgentConfig): Promise<Agent> =>
|
|
1778
|
+
this.postJson(`/v1/agents/${encodeURIComponent(agentId)}`, patch),
|
|
1779
|
+
delete: async (
|
|
1780
|
+
agentId: string,
|
|
1781
|
+
): Promise<{ object: "agent.deleted"; agent_id: string; deleted: boolean }> => {
|
|
1782
|
+
const response = await this.deleteReq(
|
|
1783
|
+
`/v1/agents/${encodeURIComponent(agentId)}`,
|
|
1784
|
+
);
|
|
1785
|
+
return (await response.json()) as {
|
|
1786
|
+
object: "agent.deleted";
|
|
1787
|
+
agent_id: string;
|
|
1788
|
+
deleted: boolean;
|
|
1789
|
+
};
|
|
1790
|
+
},
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1595
1793
|
// --- audio --------------------------------------------------------------
|
|
1596
1794
|
|
|
1597
1795
|
audio = {
|
|
@@ -1608,10 +1806,10 @@ export class PyAI {
|
|
|
1608
1806
|
/**
|
|
1609
1807
|
* Text-to-speech, streamed. Resolves as soon as the response headers arrive
|
|
1610
1808
|
* with the body as a `ReadableStream` of audio bytes, so you can start
|
|
1611
|
-
* playback or forward the audio at the first chunk
|
|
1612
|
-
*
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1809
|
+
* playback or forward the audio at the first chunk. Use `pcm`, `wav`,
|
|
1810
|
+
* or G.711 for streaming; `mp3` and `opus` are buffered server-side.
|
|
1811
|
+
* Consume the stream immediately and cancel its reader when stopping early.
|
|
1812
|
+
* HTTP connection reuse and protocol negotiation are controlled by fetch.
|
|
1615
1813
|
*/
|
|
1616
1814
|
speechStream: async (params: SpeechParams): Promise<ReadableStream<Uint8Array>> => {
|
|
1617
1815
|
assertActiveSpeechParams(params);
|
|
@@ -1629,11 +1827,13 @@ export class PyAI {
|
|
|
1629
1827
|
file: Blob;
|
|
1630
1828
|
filename?: string;
|
|
1631
1829
|
model?: string;
|
|
1632
|
-
/**
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1830
|
+
/** Optional language hint. Omission enables automatic detection. */
|
|
1831
|
+
language?: "en" | "es" | "fr" | "de" | "hi" | "it" | "pt" | "nl";
|
|
1832
|
+
numerals?: boolean;
|
|
1833
|
+
smart_format?: boolean;
|
|
1834
|
+
dictation?: boolean;
|
|
1835
|
+
drop_fillers?: boolean;
|
|
1836
|
+
vocabulary?: string[];
|
|
1637
1837
|
response_format?: "json" | "text" | "verbose_json";
|
|
1638
1838
|
/**
|
|
1639
1839
|
* Deterministic seed for reproducible eval runs. Forward-compatible:
|
|
@@ -1647,6 +1847,11 @@ export class PyAI {
|
|
|
1647
1847
|
form.set("file", params.file, params.filename ?? "audio.wav");
|
|
1648
1848
|
form.set("model", params.model ?? "pyai-hear");
|
|
1649
1849
|
if (params.language) form.set("language", params.language);
|
|
1850
|
+
if (params.numerals !== undefined) form.set("numerals", String(params.numerals));
|
|
1851
|
+
if (params.smart_format !== undefined) form.set("smart_format", String(params.smart_format));
|
|
1852
|
+
if (params.dictation !== undefined) form.set("dictation", String(params.dictation));
|
|
1853
|
+
if (params.drop_fillers !== undefined) form.set("drop_fillers", String(params.drop_fillers));
|
|
1854
|
+
if (params.vocabulary?.length) form.set("vocabulary", params.vocabulary.join(","));
|
|
1650
1855
|
if (params.response_format) form.set("response_format", params.response_format);
|
|
1651
1856
|
if (params.seed !== undefined) form.set("seed", String(params.seed));
|
|
1652
1857
|
if (params.temperature !== undefined) form.set("temperature", String(params.temperature));
|
|
@@ -1921,7 +2126,7 @@ export class PyAI {
|
|
|
1921
2126
|
* realtime. **Call this from your server** with a secret key holding
|
|
1922
2127
|
* `omni:session`; never ship the secret key to a page. Hand the returned
|
|
1923
2128
|
* `token` to the browser, which connects with
|
|
1924
|
-
* `new WebSocket(session.url, ["pyai-key." + session.token])`. The token
|
|
2129
|
+
* `new WebSocket(session.url, ["pyai.v1", "pyai-key." + session.token])`. The token
|
|
1925
2130
|
* expires after `ttlSeconds` (default 60s) and only works from
|
|
1926
2131
|
* `allowedOrigins`. Scope `omni:session`.
|
|
1927
2132
|
*/
|
|
@@ -1973,15 +2178,31 @@ export class PyAI {
|
|
|
1973
2178
|
return `${wsBase}/v1/omni${qs ? `?${qs}` : ""}`;
|
|
1974
2179
|
}
|
|
1975
2180
|
|
|
1976
|
-
/** The subprotocol that carries the key on a WS upgrade
|
|
2181
|
+
/** The subprotocol token that carries the key on a WS upgrade. */
|
|
1977
2182
|
realtimeSubprotocol(): string {
|
|
1978
2183
|
return `pyai-key.${this.apiKey}`;
|
|
1979
2184
|
}
|
|
1980
2185
|
|
|
2186
|
+
/**
|
|
2187
|
+
* The full subprotocol list to open a PyAI WebSocket with:
|
|
2188
|
+
* `[marker, credential]`. Always pass BOTH — see
|
|
2189
|
+
* {@link REALTIME_SUBPROTOCOL_MARKER}. Pass `token` to use an ephemeral
|
|
2190
|
+
* session token (from `omni.createSession`) instead of the secret key.
|
|
2191
|
+
*/
|
|
2192
|
+
realtimeSubprotocols(token?: string): string[] {
|
|
2193
|
+
return [
|
|
2194
|
+
REALTIME_SUBPROTOCOL_MARKER,
|
|
2195
|
+
token ? `pyai-key.${token}` : this.realtimeSubprotocol(),
|
|
2196
|
+
];
|
|
2197
|
+
}
|
|
2198
|
+
|
|
1981
2199
|
/** Build the Hear streaming-STT WebSocket URL (`/v1/audio/transcriptions/stream`). */
|
|
1982
2200
|
hearStreamURL(opts: HearStreamOptions = {}): string {
|
|
1983
2201
|
const wsBase = this.baseURL.replace(/^http/, "ws");
|
|
1984
2202
|
const q = new URLSearchParams(opts.query ?? {});
|
|
2203
|
+
// `context` is private to PyAI's adapter-to-server hop. Never expose or
|
|
2204
|
+
// forward it from the public SDK escape hatch.
|
|
2205
|
+
q.delete("context");
|
|
1985
2206
|
q.set("protocol", "pyai-hear-v1");
|
|
1986
2207
|
if (opts.model) q.set("model", opts.model);
|
|
1987
2208
|
if (opts.language) q.set("language", opts.language);
|
|
@@ -1989,6 +2210,12 @@ export class PyAI {
|
|
|
1989
2210
|
if (opts.encoding) q.set("encoding", opts.encoding);
|
|
1990
2211
|
if (opts.interimResults !== undefined) q.set("interim_results", String(opts.interimResults));
|
|
1991
2212
|
if (opts.numerals !== undefined) q.set("numerals", String(opts.numerals));
|
|
2213
|
+
if (opts.smartFormat !== undefined) q.set("smart_format", String(opts.smartFormat));
|
|
2214
|
+
if (opts.dictation !== undefined) q.set("dictation", String(opts.dictation));
|
|
2215
|
+
if (opts.dropFillers !== undefined) q.set("drop_fillers", String(opts.dropFillers));
|
|
2216
|
+
if (opts.vocabulary?.length) {
|
|
2217
|
+
q.set("vocabulary", JSON.stringify(opts.vocabulary));
|
|
2218
|
+
}
|
|
1992
2219
|
if (opts.endpointingMs !== undefined) q.set("endpointing_ms", String(opts.endpointingMs));
|
|
1993
2220
|
const qs = q.toString();
|
|
1994
2221
|
return `${wsBase}/v1/audio/transcriptions/stream${qs ? `?${qs}` : ""}`;
|
|
@@ -2011,8 +2238,8 @@ export class PyAI {
|
|
|
2011
2238
|
*/
|
|
2012
2239
|
connectRealtime(opts: RealtimeOptions = {}): WebSocket {
|
|
2013
2240
|
const WS = (globalThis as { WebSocket?: typeof WebSocket }).WebSocket;
|
|
2014
|
-
if (!WS) throw new Error("No global WebSocket; use realtimeURL()/
|
|
2015
|
-
return new WS(this.realtimeURL(opts),
|
|
2241
|
+
if (!WS) throw new Error("No global WebSocket; use realtimeURL()/realtimeSubprotocols() with a WS library");
|
|
2242
|
+
return new WS(this.realtimeURL(opts), this.realtimeSubprotocols());
|
|
2016
2243
|
}
|
|
2017
2244
|
}
|
|
2018
2245
|
|