@voctiv/agent-sdk 0.2.1 → 0.2.3
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 +487 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types/media-channel.d.ts +18 -18
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# @voctiv/agent-sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for scripts executed by the `ScriptEngine` scripting runtime.
|
|
4
|
+
|
|
5
|
+
The package exports the `defineScript()` identity helper and the public runtime types for voice channels, SIP calls, ASR, TTS, VAD, Smart Turn, LLM, dialog context, logging, and Voctiv legacy platform compatibility APIs.
|
|
6
|
+
|
|
7
|
+
Runtime behavior is provided by `apps/api`. The SDK itself does not open SIP calls, run ASR/TTS, or talk to platform services; it describes the objects injected into your script by the host.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @voctiv/agent-sdk rxjs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`rxjs` is a peer dependency because the runtime API exposes observables for ASR, SIP, channel events, queues, and LLM streams.
|
|
16
|
+
|
|
17
|
+
## Basic Script
|
|
18
|
+
|
|
19
|
+
Scripts export a function created with `defineScript()`. The runtime loads the module and calls it with `{ channel, logger, context, platform }`.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { defineScript } from '@voctiv/agent-sdk';
|
|
23
|
+
import { filter, map, merge } from 'rxjs';
|
|
24
|
+
|
|
25
|
+
const TTS_QUEUE = 1;
|
|
26
|
+
|
|
27
|
+
export default defineScript(async ({ channel, logger, context }) => {
|
|
28
|
+
channel.sip.answer(); // No-op on WS/headless channels.
|
|
29
|
+
channel.sendMessage({ event: 'status', payload: 'ready' });
|
|
30
|
+
|
|
31
|
+
const asr = await channel.createAsr({
|
|
32
|
+
language: context.language || 'ru-RU',
|
|
33
|
+
vad: { preSpeechFrames: 20, postSpeechFrames: 4 },
|
|
34
|
+
smartTurn: { enabled: true, triggerFrames: 3, confirmMs: 50 },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Barge-in: user speech stops only the agent TTS queue.
|
|
38
|
+
merge(asr.speechStart$, asr.partial$.pipe(filter((p) => !!p.text?.trim()))).subscribe(() => {
|
|
39
|
+
channel.audio.stop(TTS_QUEUE);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await channel.audio.say('Hi! Say anything and I will answer briefly.', {
|
|
43
|
+
queue: TTS_QUEUE,
|
|
44
|
+
alias: 'greeting',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
asr.result$.pipe(filter((text) => !!text.trim())).subscribe((userText) => {
|
|
48
|
+
logger.log('User said', { userText });
|
|
49
|
+
|
|
50
|
+
const reply$ = channel.llm
|
|
51
|
+
.stream(`Reply briefly to the user: "${userText}"`, {
|
|
52
|
+
agentUuid: context.agentUuid,
|
|
53
|
+
dialogUuid: context.dialogUuid,
|
|
54
|
+
})
|
|
55
|
+
.pipe(map((chunk) => chunk.content));
|
|
56
|
+
|
|
57
|
+
void channel.audio.say(reply$, {
|
|
58
|
+
queue: TTS_QUEUE,
|
|
59
|
+
alias: 'reply',
|
|
60
|
+
ttsStrategy: 'sentence',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
channel.events.terminated$.subscribe(() => {
|
|
66
|
+
asr.destroy();
|
|
67
|
+
resolve({ output: { dialogUuid: context.dialogUuid } });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## What The SDK Contains
|
|
74
|
+
|
|
75
|
+
`defineScript(fn)` marks the default export as the script entry point. It returns the same function and exists to give TypeScript the correct `ScriptContext` shape.
|
|
76
|
+
|
|
77
|
+
`ScriptContext` is the top-level object passed to a script:
|
|
78
|
+
|
|
79
|
+
- `channel` is the media channel for SIP, WS, ASR, TTS, audio playback, LLM, and structured data messages.
|
|
80
|
+
- `logger` writes structured script logs and can stream logs to a debug endpoint.
|
|
81
|
+
- `context` contains dialog identity, caller/called numbers, language, flags, params, entry point, persisted env, and runtime budget.
|
|
82
|
+
- `platform` exposes legacy platform operations: NLU, dialog state, outbound call scheduling, messaging, and phrase records.
|
|
83
|
+
|
|
84
|
+
`MediaChannel` is the main real-time API:
|
|
85
|
+
|
|
86
|
+
- `channel.type` is `"sip"` for telephony and `"ws"` for WebSocket/script-manager sessions. Headless sessions currently expose a synthetic `"ws"` channel; check `context.headless` to detect them.
|
|
87
|
+
- `channel.params` is the merged runtime parameter map. Treat unknown keys as host-specific.
|
|
88
|
+
- `channel.createAsr()` creates an ASR handle.
|
|
89
|
+
- `channel.audio` controls TTS, raw playback, pre-synthesis, and mixer queues.
|
|
90
|
+
- `channel.sip` controls SIP state, pre-answer media, DTMF, hold/mute/hangup, outbound calls, and bridging.
|
|
91
|
+
- `channel.llm` talks to the Omni LLM backend.
|
|
92
|
+
- `channel.events` exposes speech, interrupt, termination, and WS data message observables.
|
|
93
|
+
- `channel.textInput` injects synthetic ASR results for tests and debug clients.
|
|
94
|
+
|
|
95
|
+
## SIP And Pre-Answer Media
|
|
96
|
+
|
|
97
|
+
SIP sessions expose call state through `channel.sip.state`, `state$`, `progress$`, `early$`, and `answered$`.
|
|
98
|
+
|
|
99
|
+
The important states are:
|
|
100
|
+
|
|
101
|
+
- `ringing`: INVITE is in progress, but no media is available yet.
|
|
102
|
+
- `early`: RTP is ready before the final 200 OK answer. ASR, TTS, playback, and DTMF work in this state.
|
|
103
|
+
- `active`: final 200 OK has been received or sent.
|
|
104
|
+
- `terminated`: the call ended and no more audio is possible.
|
|
105
|
+
|
|
106
|
+
### Outbound Pre-Answer
|
|
107
|
+
|
|
108
|
+
For outbound calls, early media starts when the remote side sends a provisional response with SDP, usually `183 Session Progress`. This is useful for IVRs that speak before answering.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const bLeg = await channel.sip.makeCall({
|
|
112
|
+
sipUri: 'sip:+12025551234@trunk.example.com',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await bLeg.sip.waitForEarly();
|
|
116
|
+
|
|
117
|
+
const asr = await bLeg.createAsr({ language: 'en-US' });
|
|
118
|
+
asr.result$.subscribe((text) => {
|
|
119
|
+
if (/press one/i.test(text)) {
|
|
120
|
+
bLeg.sip.sendDtmf('1');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`waitForEarly()` resolves when the call reaches either `early` or `active`. If a carrier skips early media and answers directly, it resolves on the final answer.
|
|
126
|
+
|
|
127
|
+
### Inbound Pre-Answer
|
|
128
|
+
|
|
129
|
+
For inbound calls, call `channel.sip.sendProgress()` to send `183 Session Progress` with SDP. This enters `early` state and enables full-duplex audio before the final answer.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { firstValueFrom } from 'rxjs';
|
|
133
|
+
|
|
134
|
+
channel.sip.sendProgress();
|
|
135
|
+
await channel.sip.waitForEarly();
|
|
136
|
+
|
|
137
|
+
const asr = await channel.createAsr({ language: 'en-US' });
|
|
138
|
+
await channel.audio.say('Please say your account number.');
|
|
139
|
+
|
|
140
|
+
const account = await firstValueFrom(asr.result$);
|
|
141
|
+
|
|
142
|
+
channel.sip.answer();
|
|
143
|
+
await channel.audio.say(`Thank you. Looking up account ${account}.`);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The API does not mark the call as answered until `answer()` sends final `200 OK`. External billing still depends on carrier policy.
|
|
147
|
+
|
|
148
|
+
### Audio Auto-Wait
|
|
149
|
+
|
|
150
|
+
On SIP channels, `channel.audio.say()` and `channel.audio.play()` automatically wait until RTP is ready (`early` or `active`). You only need explicit `waitForEarly()` / `waitForAnswer()` when your script logic depends on the state transition.
|
|
151
|
+
|
|
152
|
+
If the call terminates before media becomes available, deferred audio resolves as a no-op.
|
|
153
|
+
|
|
154
|
+
### SIP Controls
|
|
155
|
+
|
|
156
|
+
`channel.sip` also supports:
|
|
157
|
+
|
|
158
|
+
- `answer()` for inbound final answer.
|
|
159
|
+
- `sendDtmf(digit, duration?)` for IVR navigation.
|
|
160
|
+
- `sendInfo(contentType, body)` for SIP INFO messages.
|
|
161
|
+
- `hold()` / `unhold()` for SIP hold.
|
|
162
|
+
- `mute()` / `unmute()` for local outgoing audio suppression.
|
|
163
|
+
- `hangup()` to terminate the call.
|
|
164
|
+
- `makeCall()` to create an outbound SIP B-leg from the main SIP channel.
|
|
165
|
+
- `bridge(other)` to cross-connect two SIP channels.
|
|
166
|
+
|
|
167
|
+
`makeCall()` and `bridge()` are only supported by the main SIP channel. Worker-isolated, WS, and headless channels do not create nested SIP legs.
|
|
168
|
+
|
|
169
|
+
## ASR, VAD, And Smart Turn
|
|
170
|
+
|
|
171
|
+
Create ASR with `channel.createAsr(config?)`.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const asr = await channel.createAsr({
|
|
175
|
+
vendor: 'Y',
|
|
176
|
+
name: 'main-yandex-key',
|
|
177
|
+
language: 'ru-RU',
|
|
178
|
+
vad: {
|
|
179
|
+
positiveThreshold: 0.55,
|
|
180
|
+
negativeThreshold: 0.35,
|
|
181
|
+
preSpeechFrames: 12,
|
|
182
|
+
postSpeechFrames: 12,
|
|
183
|
+
},
|
|
184
|
+
smartTurn: {
|
|
185
|
+
enabled: true,
|
|
186
|
+
silenceTimeoutMs: 1200,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`AsrHandle` exposes:
|
|
192
|
+
|
|
193
|
+
- `result$`: finalized utterances.
|
|
194
|
+
- `partial$`: streaming partial hypotheses.
|
|
195
|
+
- `speechStart$` / `speechEnd$`: VAD speech boundaries.
|
|
196
|
+
- `interrupt$`: barge-in / interrupt events where the host supports them.
|
|
197
|
+
- `vadProbability$`: normalized VAD probability when available.
|
|
198
|
+
- `pause()` / `resume()` to stop or resume forwarding new audio frames.
|
|
199
|
+
- `finalize()` to force the current utterance to flush.
|
|
200
|
+
- `destroy()` to close connector streams and subscriptions.
|
|
201
|
+
|
|
202
|
+
SIP sessions use the call-level telephony VAD when it is available. WS sessions create one VAD/SmartTurn instance for the socket session on the first `createAsr()` call. Headless sessions return an inert ASR handle with empty observables.
|
|
203
|
+
|
|
204
|
+
If ASR connector creation fails, SIP/WS return a degraded handle. VAD observables still mirror the channel where possible, but no real STT results are emitted.
|
|
205
|
+
|
|
206
|
+
## ASR Credentials And Vendors
|
|
207
|
+
|
|
208
|
+
`AsrConfig.vendor` is an engine hint, for example `"Y"`, `"D"`, `"yandex"`, or `"neuro_v3"`, resolved by the host vendor alias mapping.
|
|
209
|
+
|
|
210
|
+
In Voctiv legacy compatibility mode, ASR credentials can be selected by logic-executor `key_storage.name`:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const asr = await channel.createAsr({
|
|
214
|
+
name: 'main-asr-key',
|
|
215
|
+
language: 'ru-RU',
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The runtime looks in `channel.params.authentication_data.legacyAsrKeysByName[name]` for the current dialog agent and company. If `name` is omitted, `channel.params.defaultAsrName` may be used. Vendor-specific overrides go into `data`; primitives are stringified and objects/arrays are JSON-serialized before connector config is built.
|
|
220
|
+
|
|
221
|
+
## TTS, Playback, And Mixer Queues
|
|
222
|
+
|
|
223
|
+
`channel.audio.say(textOrObservable, options?)` synthesizes text and plays it through the mixer.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
await channel.audio.say('Please wait while I check that.', {
|
|
227
|
+
queue: 0,
|
|
228
|
+
alias: 'main-response',
|
|
229
|
+
ttsVendor: 'E',
|
|
230
|
+
ttsStrategy: 'sentence',
|
|
231
|
+
ttsConfig: {
|
|
232
|
+
voice_id: 'voice-id',
|
|
233
|
+
output_format: 'pcm_16000',
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`channel.audio.play(source, options?)` plays raw audio from a URL/path or a `LegacyPhraseRecord`.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
await channel.audio.play('/opt/prompts/welcome.wav', {
|
|
242
|
+
queue: 1,
|
|
243
|
+
alias: 'welcome-earcon',
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`channel.audio.presay(text, options?)` pre-synthesizes TTS into the host TTS cache. If the cache is not available, the runtime logs a warning and resolves without throwing.
|
|
248
|
+
|
|
249
|
+
`channel.audio.preload(source)` decodes a raw audio source through the audio player. It does not synthesize TTS and does not populate the TTS cache used by `presay()`.
|
|
250
|
+
|
|
251
|
+
### TTS Strategies
|
|
252
|
+
|
|
253
|
+
`ttsStrategy` controls how text is chunked:
|
|
254
|
+
|
|
255
|
+
- `sentence`: split on sentence boundaries and synthesize each sentence. This is the default.
|
|
256
|
+
- `streaming`: send chunks incrementally for streaming-capable vendors.
|
|
257
|
+
- `full`: accumulate the whole input and synthesize it as one segment after the input completes.
|
|
258
|
+
|
|
259
|
+
When using an `Observable<string>` input, WS clients also receive text progress events for streamed chunks.
|
|
260
|
+
|
|
261
|
+
### Mixer Queues
|
|
262
|
+
|
|
263
|
+
The mixer has queues `0` through `4`. Use separate queues for main speech, earcons, hold music, or background audio.
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
const music = channel.audio.queue(2);
|
|
267
|
+
music.volume = 0.25;
|
|
268
|
+
|
|
269
|
+
await channel.audio.play('/opt/audio/hold.wav', {
|
|
270
|
+
queue: 2,
|
|
271
|
+
alias: 'hold-music',
|
|
272
|
+
loop: true,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
channel.audio.stop(2);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`PlayOptions.volume` changes the whole queue volume, not just one item. `stop(queue)` clears a queue and aborts in-flight sentence TTS for that queue. `stopAll()` clears every queue.
|
|
279
|
+
|
|
280
|
+
For sentence-split TTS, internal queue item aliases are suffixed as `alias-0`, `alias-1`, and so on. Raw `play()` and direct streaming TTS use the alias exactly.
|
|
281
|
+
|
|
282
|
+
## TTS Credentials And Saved Phrases
|
|
283
|
+
|
|
284
|
+
In Voctiv legacy compatibility mode, TTS credentials can be selected by `PlayOptions.name` or `ttsConfig.name`.
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
await channel.audio.say('Здравствуйте!', {
|
|
288
|
+
name: 'main-tts-key',
|
|
289
|
+
ttsConfig: {
|
|
290
|
+
voice: 'alena',
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The runtime looks in `channel.params.authentication_data.legacyTtsKeysByName[name]`. If `name` is omitted, `channel.params.defaultTtsName` may be used.
|
|
296
|
+
|
|
297
|
+
`legacySavePhrase` stores synthesized audio under the Voctiv record phrase storage root and inserts phrase metadata so it can later be loaded with `platform.getRecords()`.
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
await channel.audio.say('Welcome back.', {
|
|
301
|
+
legacySavePhrase: {
|
|
302
|
+
phraseName: 'welcome_back',
|
|
303
|
+
flag: context.flag,
|
|
304
|
+
language: context.language,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const records = await platform.getRecords?.({
|
|
309
|
+
phraseName: 'welcome_back',
|
|
310
|
+
flag: context.flag,
|
|
311
|
+
language: context.language,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (records?.[0]) {
|
|
315
|
+
await channel.audio.play(records[0]);
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
This requires legacy compatibility mode, a trusted LE agent id/UUID, TTS cache, and `LEGACY_V3_RECORD_PHRASE_ROOT`.
|
|
320
|
+
|
|
321
|
+
## LLM API
|
|
322
|
+
|
|
323
|
+
`channel.llm` talks to the Omni LLM backend.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const answer = await channel.llm.ask('Summarize the user request', {
|
|
327
|
+
role: 'assistant',
|
|
328
|
+
hidden: true,
|
|
329
|
+
agentUuid: context.agentUuid,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
await channel.audio.say(answer);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
For streaming:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
const stream$ = channel.llm.stream('Answer briefly', {
|
|
339
|
+
role: 'assistant',
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await channel.audio.say(
|
|
343
|
+
stream$.pipe(map((chunk) => chunk.content)),
|
|
344
|
+
{ ttsStrategy: 'streaming' },
|
|
345
|
+
);
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
`channel.llm.extract(options?)` runs structured extraction via Omni. `makePersistentStream(options?)` opens a long-lived Socket.IO stream and lets you send multiple turns without reconnecting.
|
|
349
|
+
|
|
350
|
+
## Platform API
|
|
351
|
+
|
|
352
|
+
`platform` exposes Voctiv legacy-compatible operations.
|
|
353
|
+
|
|
354
|
+
`platform.nlu.extract(utterance, options?)` calls NLU v3 `/infer`. The runtime sends `phrase`, `context`, and `agent_id`. If `options.context` is omitted, current dialog params are serialized and used as NLU context.
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
const result = await platform.nlu.extract('I want to reschedule', {
|
|
358
|
+
intents: ['reschedule', 'cancel'],
|
|
359
|
+
entities: ['date', 'time'],
|
|
360
|
+
use_synonyms: true,
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Legacy platform APIs require `context.legacyV3Compat === true` in the current `apps/api` runtime. This includes NLU, outbound calls, dialog writes, messaging sends, and phrase records.
|
|
365
|
+
|
|
366
|
+
### Dialog State
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
platform.dialog.entryPoint = 'on_recall';
|
|
370
|
+
platform.dialog.result = 'done';
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Setters update the local value immediately and ask the platform DB to persist asynchronously. They are not awaitable and should not be used as transactional writes.
|
|
374
|
+
|
|
375
|
+
### Outbound Calls
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
await platform.call('+12025551234', {
|
|
379
|
+
date: new Date(Date.now() + 60_000),
|
|
380
|
+
entryPoint: 'on_callback',
|
|
381
|
+
recallCount: 2,
|
|
382
|
+
recallDelay: 300,
|
|
383
|
+
priority: 10,
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
This creates a row in the legacy `call` table. The dialer picks it up and originates the SIP call.
|
|
388
|
+
|
|
389
|
+
### Messaging
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
await platform.messaging.send({
|
|
393
|
+
src: 'bot',
|
|
394
|
+
destination: '+12025551234',
|
|
395
|
+
text: 'Your appointment is confirmed.',
|
|
396
|
+
});
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Outbound messages are transported through legacy Redis streams. `platform.messaging.message$` currently replays the inbound message that started a headless messaging script; it is not a live subscription to all future Redis messages.
|
|
400
|
+
|
|
401
|
+
## Dialog Context And Persisted Env
|
|
402
|
+
|
|
403
|
+
`context` includes identity, telephony fields, params, routing metadata, and runtime helpers.
|
|
404
|
+
|
|
405
|
+
Important fields:
|
|
406
|
+
|
|
407
|
+
- `context.dialogUuid`: current dialog UUID.
|
|
408
|
+
- `context.callerId` / `context.msisdn`: caller identity.
|
|
409
|
+
- `context.destinationNumber`: called number.
|
|
410
|
+
- `context.language` / `context.lang`: language selected for the run.
|
|
411
|
+
- `context.flag`: business flag.
|
|
412
|
+
- `context.initialData`: shallow snapshot of params at script start.
|
|
413
|
+
- `context.dialogParams`: live param map for the run.
|
|
414
|
+
- `context.entryPoint`: current routing entry point.
|
|
415
|
+
- `context.headless`: true for offline/queue/messaging sessions without a real media channel.
|
|
416
|
+
- `context.runTime`: async execution budget helper.
|
|
417
|
+
- `context.env$`: persisted dialog environment as an RxJS `BehaviorSubject`.
|
|
418
|
+
|
|
419
|
+
Use `env$` for persisted script state:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
const current = context.env$?.getValue() ?? {};
|
|
423
|
+
context.env$?.next({
|
|
424
|
+
...current,
|
|
425
|
+
lastIntent: 'reschedule',
|
|
426
|
+
});
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Do not return `env` from the script. The runtime snapshots `context.env$` after completion and attaches it to the persisted result.
|
|
430
|
+
|
|
431
|
+
## Logging And Debugging
|
|
432
|
+
|
|
433
|
+
Use `logger.log()`, `warn()`, `error()`, and `debug()` for structured logs.
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
logger.log('ASR result received', { text });
|
|
437
|
+
logger.warn('Low confidence intent', { confidence });
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
`logger.enableDebug(endpoint)` streams logs from the current script instance to a remote debug endpoint. `logger.breakpoint(label, snapshot?)` pauses only when an active debug session is connected; otherwise it resolves immediately.
|
|
441
|
+
|
|
442
|
+
## WS And Headless Behavior
|
|
443
|
+
|
|
444
|
+
WS channels behave like active media channels:
|
|
445
|
+
|
|
446
|
+
- `channel.sip.state` is effectively active.
|
|
447
|
+
- `sendDtmf()` emits `dtmf-send` to the WS client.
|
|
448
|
+
- `sendMessage()` emits a structured `data` event.
|
|
449
|
+
- ASR reads socket audio frames or synthetic text input.
|
|
450
|
+
|
|
451
|
+
Headless channels are for offline, queue, or messaging sessions:
|
|
452
|
+
|
|
453
|
+
- Audio methods are no-ops that log warnings.
|
|
454
|
+
- SIP methods are mostly no-ops.
|
|
455
|
+
- `createAsr()` returns an inert handle.
|
|
456
|
+
- LLM and platform APIs still work.
|
|
457
|
+
|
|
458
|
+
Use `context.headless` to branch when a script must behave differently without a real media channel.
|
|
459
|
+
|
|
460
|
+
## Text Input For Tests
|
|
461
|
+
|
|
462
|
+
`channel.textInput` injects synthetic ASR output into a live ASR handle.
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
const asr = await channel.createAsr();
|
|
466
|
+
|
|
467
|
+
channel.textInput.pushPartial(asr.id, 'hello', false);
|
|
468
|
+
channel.textInput.pushResult(asr.id, 'hello world');
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
This is mainly for WS debug clients and automated tests. Unknown ASR ids are ignored.
|
|
472
|
+
|
|
473
|
+
## Package Notes
|
|
474
|
+
|
|
475
|
+
The package is published as CommonJS with TypeScript declarations in `dist`.
|
|
476
|
+
|
|
477
|
+
Build locally with:
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
npm run build
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
The package exports only the public SDK entry point:
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
import { defineScript, type MediaChannel, type AsrHandle } from '@voctiv/agent-sdk';
|
|
487
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @packageDocumentation
|
|
3
|
-
* **Agent scripting SDK** for `
|
|
3
|
+
* **Agent scripting SDK** for `ScriptEngine`: typed **`defineScript`** context,
|
|
4
4
|
* {@link import('./types/media-channel').MediaChannel}, ASR/TTS, LLM, SIP, and Voctiv platform (LE-compat) helpers.
|
|
5
5
|
* These exports are types plus the `defineScript` identity helper; behavior is provided by
|
|
6
6
|
* the `apps/api` scripting runtime that loads and executes your script.
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* @packageDocumentation
|
|
4
|
-
* **Agent scripting SDK** for `
|
|
4
|
+
* **Agent scripting SDK** for `ScriptEngine`: typed **`defineScript`** context,
|
|
5
5
|
* {@link import('./types/media-channel').MediaChannel}, ASR/TTS, LLM, SIP, and Voctiv platform (LE-compat) helpers.
|
|
6
6
|
* These exports are types plus the `defineScript` identity helper; behavior is provided by
|
|
7
7
|
* the `apps/api` scripting runtime that loads and executes your script.
|
|
@@ -205,8 +205,8 @@ export interface ChannelLlm {
|
|
|
205
205
|
* |---|---|---|
|
|
206
206
|
* | **`idle`** | Call object exists but no SIP signalling has started yet. | Initial state. |
|
|
207
207
|
* | **`ringing`** | INVITE sent (outbound) or received (inbound); remote party is ringing. No media yet. | Automatic after INVITE. |
|
|
208
|
-
* | **`early`** | RTP pipeline is up — you can **send and receive audio**, run ASR, play TTS. This is the "pre-answer" phase
|
|
209
|
-
* | **`active`** | 200 OK received/sent — the call is fully established.
|
|
208
|
+
* | **`early`** | RTP pipeline is up — you can **send and receive audio**, run ASR, play TTS. This is the "pre-answer" phase before final 200 OK. | **Outbound:** remote sends 183 Session Progress with SDP. **Inbound:** your script calls `sip.sendProgress()`. |
|
|
209
|
+
* | **`active`** | 200 OK received/sent — the call is fully established. | **Outbound:** remote answers. **Inbound:** your script calls `sip.answer()`. |
|
|
210
210
|
* | **`holding`** | Local hold is active. | Your script calls `sip.hold()`. |
|
|
211
211
|
* | **`terminated`** | Call ended (BYE, CANCEL, or error). No further audio is possible. | Either side hangs up, or network error. |
|
|
212
212
|
*
|
|
@@ -260,9 +260,9 @@ export interface SipProgressEvent {
|
|
|
260
260
|
* #### Inbound calls
|
|
261
261
|
*
|
|
262
262
|
* Call `sip.sendProgress()` to send **183 Session Progress** to the caller. The call
|
|
263
|
-
* enters `"early"
|
|
264
|
-
*
|
|
265
|
-
* later to complete the answer.
|
|
263
|
+
* enters `"early"` and audio flows in both directions before final 200 OK. The API
|
|
264
|
+
* does not mark the call answered until `sip.answer()`; external billing still depends
|
|
265
|
+
* on carrier policy. Call `sip.answer()` later to complete the answer.
|
|
266
266
|
*
|
|
267
267
|
* ---
|
|
268
268
|
*
|
|
@@ -286,18 +286,18 @@ export interface SipProgressEvent {
|
|
|
286
286
|
* await call.audio.say('Hello! We are calling about your order.');
|
|
287
287
|
* ```
|
|
288
288
|
*
|
|
289
|
-
* #### Inbound — collect data before
|
|
289
|
+
* #### Inbound — collect data before the final answer
|
|
290
290
|
*
|
|
291
291
|
* ```ts
|
|
292
292
|
* // Inbound call arrives — state is 'ringing'
|
|
293
293
|
* channel.sip.sendProgress();
|
|
294
|
-
* // State is now 'early' — full duplex audio
|
|
294
|
+
* // State is now 'early' — full duplex audio before final 200 OK
|
|
295
295
|
*
|
|
296
296
|
* const asr = await channel.createAsr();
|
|
297
297
|
* await channel.audio.say('Please say your account number.');
|
|
298
298
|
* const account = await firstValueFrom(asr.result$);
|
|
299
299
|
*
|
|
300
|
-
* // Now
|
|
300
|
+
* // Now send final answer
|
|
301
301
|
* channel.sip.answer();
|
|
302
302
|
* await channel.audio.say(`Thank you! Looking up account ${account}…`);
|
|
303
303
|
* ```
|
|
@@ -415,7 +415,7 @@ export interface ChannelSip {
|
|
|
415
415
|
* - `audio.say()` / `audio.play()` will be heard by the remote party
|
|
416
416
|
* - `createAsr()` will receive remote audio
|
|
417
417
|
* - `dtmf$` will deliver in-band DTMF
|
|
418
|
-
* -
|
|
418
|
+
* - The API has not marked the call answered yet; carrier billing policy may vary
|
|
419
419
|
*
|
|
420
420
|
* If the call is answered without any early media (outbound: no 183 with SDP;
|
|
421
421
|
* inbound: `answer()` called directly), `early$` **may not emit at all** —
|
|
@@ -560,10 +560,10 @@ export interface ChannelSip {
|
|
|
560
560
|
*
|
|
561
561
|
* No-op if the call is already answered or if this is an outbound call. Also no-op
|
|
562
562
|
* on WS/headless channels. After answering, the state transitions to `"active"` and
|
|
563
|
-
*
|
|
563
|
+
* the API records the call answer time.
|
|
564
564
|
*
|
|
565
565
|
* If the call is in `"early"` state (after `sendProgress()`), `answer()` promotes it
|
|
566
|
-
* to `"active"` — audio was already flowing
|
|
566
|
+
* to `"active"` — audio was already flowing and the final answer is now sent.
|
|
567
567
|
*
|
|
568
568
|
* ```ts
|
|
569
569
|
* // Simple: answer immediately
|
|
@@ -573,9 +573,9 @@ export interface ChannelSip {
|
|
|
573
573
|
*
|
|
574
574
|
* ```ts
|
|
575
575
|
* // Advanced: pre-answer → answer
|
|
576
|
-
* channel.sip.sendProgress(); // early media
|
|
576
|
+
* channel.sip.sendProgress(); // early media before final 200 OK
|
|
577
577
|
* await channel.audio.say('One moment…');
|
|
578
|
-
* channel.sip.answer(); //
|
|
578
|
+
* channel.sip.answer(); // final 200 OK
|
|
579
579
|
* await channel.audio.say('How can I help?');
|
|
580
580
|
* ```
|
|
581
581
|
*/
|
|
@@ -588,8 +588,8 @@ export interface ChannelSip {
|
|
|
588
588
|
* - The call state transitions to `"early"` and `early$` emits.
|
|
589
589
|
* - `audio.say()`, `audio.play()`, `createAsr()`, `dtmf$` all work —
|
|
590
590
|
* exactly the same as in the `"active"` state.
|
|
591
|
-
* - The remote party hears your audio
|
|
592
|
-
*
|
|
591
|
+
* - The remote party hears your audio before the final 200 OK. The API still treats
|
|
592
|
+
* the call as not answered; external billing depends on carrier policy.
|
|
593
593
|
*
|
|
594
594
|
* Call `sip.answer()` later to send the final 200 OK and transition to `"active"`.
|
|
595
595
|
*
|
|
@@ -601,11 +601,11 @@ export interface ChannelSip {
|
|
|
601
601
|
* ```ts
|
|
602
602
|
* // Inbound call arrives — state is 'ringing'
|
|
603
603
|
* channel.sip.sendProgress();
|
|
604
|
-
* // State is now 'early' — audio flows
|
|
604
|
+
* // State is now 'early' — audio flows before final 200 OK
|
|
605
605
|
*
|
|
606
606
|
* await channel.audio.say('Please hold while we connect you…');
|
|
607
607
|
*
|
|
608
|
-
* // Now answer
|
|
608
|
+
* // Now answer with final 200 OK
|
|
609
609
|
* channel.sip.answer();
|
|
610
610
|
* await channel.audio.say('Hello! How can I help?');
|
|
611
611
|
* ```
|
|
@@ -617,7 +617,7 @@ export interface ChannelSip {
|
|
|
617
617
|
* const asr = await channel.createAsr();
|
|
618
618
|
* await channel.audio.say('Hi! What is your account number?');
|
|
619
619
|
* const result = await firstValueFrom(asr.result$);
|
|
620
|
-
* // Collected account number
|
|
620
|
+
* // Collected account number before sending final 200 OK
|
|
621
621
|
* channel.sip.answer();
|
|
622
622
|
* ```
|
|
623
623
|
*/
|
package/package.json
CHANGED