@nuancelabs/js-sdk 0.2.2 → 0.5.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/index.d.ts +55 -7
- package/index.js +163 -18
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -216,8 +216,8 @@ export declare type ConversationEvent =
|
|
|
216
216
|
/**
|
|
217
217
|
* How long the live session has left, when it is capped.
|
|
218
218
|
*
|
|
219
|
-
* Emitted
|
|
220
|
-
* countdown should
|
|
219
|
+
* Emitted when the time left is learned or corrected, so it steps rather
|
|
220
|
+
* than ticks; a caller showing a countdown should count down from the latest.
|
|
221
221
|
*/
|
|
222
222
|
| {
|
|
223
223
|
type: "time-remaining";
|
|
@@ -318,6 +318,26 @@ export declare interface CreateNuanceConversationOptions {
|
|
|
318
318
|
* ```
|
|
319
319
|
*/
|
|
320
320
|
token?: () => Promise<string>;
|
|
321
|
+
/**
|
|
322
|
+
* Who the avatar is in the conversation, such as your own
|
|
323
|
+
* {@link SessionPersonality.system_prompt}. Left out, it is the default
|
|
324
|
+
* personality.
|
|
325
|
+
*
|
|
326
|
+
* Only for a conversation the SDK creates with your API key (see
|
|
327
|
+
* {@link CreateNuanceConversationOptions.headers}). With a
|
|
328
|
+
* {@link CreateNuanceConversationOptions.token}, set it when your server
|
|
329
|
+
* mints the token instead: the token fixes it, and a conversation that sets
|
|
330
|
+
* its own is refused.
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```ts
|
|
334
|
+
* createNuanceConversation({
|
|
335
|
+
* headers: () => ({ Authorization: `Bearer ${process.env.NUANCE_API_KEY}` }),
|
|
336
|
+
* personality: { system_prompt: "You are Sam, at Acme Bank..." },
|
|
337
|
+
* });
|
|
338
|
+
* ```
|
|
339
|
+
*/
|
|
340
|
+
personality?: PersonalityChoice;
|
|
321
341
|
/**
|
|
322
342
|
* Capture constraints for the user's camera.
|
|
323
343
|
*
|
|
@@ -344,9 +364,6 @@ export declare interface CreateNuanceConversationOptions {
|
|
|
344
364
|
}
|
|
345
365
|
|
|
346
366
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
367
|
/**
|
|
351
368
|
* Raised by {@link Conversation.connect} when the server refuses a new session
|
|
352
369
|
* because the account has not completed onboarding or accepted the current
|
|
@@ -405,7 +422,12 @@ export declare type MediaErrorReason = "permission-denied" | "device-not-found"
|
|
|
405
422
|
*/
|
|
406
423
|
export declare type NetworkQuality = "good" | "poor" | "lost";
|
|
407
424
|
|
|
408
|
-
|
|
425
|
+
/**
|
|
426
|
+
* Who the avatar is in a conversation created with
|
|
427
|
+
* {@link CreateNuanceConversationOptions.personality}.
|
|
428
|
+
*/
|
|
429
|
+
export declare interface PersonalityChoice extends SessionPersonality {
|
|
430
|
+
}
|
|
409
431
|
|
|
410
432
|
/**
|
|
411
433
|
* Raised by {@link Conversation.connect} when the server refuses a new session
|
|
@@ -428,11 +450,37 @@ export declare type SessionEndReason = "time-limit" | "ended" | "policy-violatio
|
|
|
428
450
|
|
|
429
451
|
|
|
430
452
|
|
|
453
|
+
/** Who the avatar is in a conversation. */
|
|
454
|
+
export declare interface SessionPersonality {
|
|
455
|
+
/**
|
|
456
|
+
* Your own prompt: who the avatar is, where it works, and what it knows
|
|
457
|
+
* about the person and the situation, written as you would brief a person
|
|
458
|
+
* before a call, in plain text.
|
|
459
|
+
*/
|
|
460
|
+
system_prompt?: string;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* What your server sends to `POST /session-tokens` with its API key. Every
|
|
465
|
+
* field is fixed for the conversation the token starts; send `{}` for the
|
|
466
|
+
* defaults.
|
|
467
|
+
*/
|
|
468
|
+
export declare interface SessionTokenRequest {
|
|
469
|
+
/** Who the avatar is in the conversation. Leave it out for the default personality. */
|
|
470
|
+
personality?: SessionPersonality;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** What `POST /session-tokens` returns. */
|
|
474
|
+
export declare interface SessionTokenResponse {
|
|
475
|
+
/** The session token, `st_…`: pass it to {@link CreateNuanceConversationOptions.token}. */
|
|
476
|
+
token: string;
|
|
477
|
+
/** When the token stops being able to start a conversation (UTC, ISO 8601). */
|
|
478
|
+
expires_at: string;
|
|
479
|
+
}
|
|
431
480
|
|
|
432
481
|
/** When the avatar starts speaking: see {@link CreateNuanceConversationOptions.startOn}. */
|
|
433
482
|
export declare type StartOn = "first-frame" | "full-quality";
|
|
434
483
|
|
|
435
|
-
|
|
436
484
|
/**
|
|
437
485
|
* The local user participant in a conversation.
|
|
438
486
|
*/
|
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@ function isRecord(value) {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
// src/core/sdk-version.ts
|
|
7
|
-
var SDK_HEADER = `js/${"0.
|
|
7
|
+
var SDK_HEADER = `js/${"0.5.0"}`;
|
|
8
8
|
|
|
9
9
|
// src/utils.ts
|
|
10
10
|
function normalizeHost(host) {
|
|
@@ -56,6 +56,9 @@ class NuanceApiClient {
|
|
|
56
56
|
async heartbeatConversation(conversationId) {
|
|
57
57
|
return await this.request(`/conversation/${conversationId}/heartbeat`, { method: "POST", token: this.tokens.get(conversationId) });
|
|
58
58
|
}
|
|
59
|
+
async conversationEvents(conversationId, after, signal) {
|
|
60
|
+
return await this.request(`/conversation/${conversationId}/events?after=${after}`, { token: this.tokens.get(conversationId), signal });
|
|
61
|
+
}
|
|
59
62
|
async abandonConversation(conversationId) {
|
|
60
63
|
return await this.request(`/conversation/${conversationId}/abandon`, { method: "POST", token: this.tokens.get(conversationId) });
|
|
61
64
|
}
|
|
@@ -71,7 +74,8 @@ class NuanceApiClient {
|
|
|
71
74
|
const response = await fetch(`${this.host}${path}`, {
|
|
72
75
|
method: options?.method ?? "GET",
|
|
73
76
|
headers,
|
|
74
|
-
body: options?.body !== undefined ? JSON.stringify(options.body) : undefined
|
|
77
|
+
body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
78
|
+
signal: options?.signal
|
|
75
79
|
});
|
|
76
80
|
if (!response.ok) {
|
|
77
81
|
let detail = response.statusText;
|
|
@@ -2137,7 +2141,8 @@ class LiveKitConversation {
|
|
|
2137
2141
|
var DEFAULT_HOST = "https://api.nuancelabs.ai";
|
|
2138
2142
|
var QUEUE_HEARTBEAT_INTERVAL_MS = 5000;
|
|
2139
2143
|
var HEARTBEAT_INTERVAL_MS = 5000;
|
|
2140
|
-
var
|
|
2144
|
+
var EVENTS_RETRY_MS = 5000;
|
|
2145
|
+
var ADMISSION_FAILED_REQUESTS = 6;
|
|
2141
2146
|
var BUSY_RETRY_LIMIT_MS = 60000;
|
|
2142
2147
|
var BUSY_RETRY_DEFAULT_MS = 3000;
|
|
2143
2148
|
var LOST_WINDOW_MS = 30000;
|
|
@@ -2204,6 +2209,7 @@ class NuanceConversation {
|
|
|
2204
2209
|
delegate = null;
|
|
2205
2210
|
delegateUnsubscribe = null;
|
|
2206
2211
|
stopBeats = null;
|
|
2212
|
+
stopEvents = null;
|
|
2207
2213
|
admitted = null;
|
|
2208
2214
|
failedBeats = 0;
|
|
2209
2215
|
resilient = false;
|
|
@@ -2292,10 +2298,8 @@ class NuanceConversation {
|
|
|
2292
2298
|
this._id = conversation.id;
|
|
2293
2299
|
this.held = conversation.id;
|
|
2294
2300
|
this.emit({ type: "session-created", id: conversation.id });
|
|
2295
|
-
|
|
2296
|
-
this.
|
|
2297
|
-
await this.admitted.promise;
|
|
2298
|
-
this.delegate = await this.createDelegate(conversation, mediaDevices, attempt);
|
|
2301
|
+
const grant = await this.waitForSlot(conversation.id, attempt);
|
|
2302
|
+
this.delegate = await this.createDelegate(conversation, grant, mediaDevices, attempt);
|
|
2299
2303
|
this.delegateUnsubscribe = this.delegate.onEvent((event) => {
|
|
2300
2304
|
if (event.type === "state-changed") {
|
|
2301
2305
|
this.setState(event.state);
|
|
@@ -2318,7 +2322,9 @@ class NuanceConversation {
|
|
|
2318
2322
|
this.emit(event);
|
|
2319
2323
|
});
|
|
2320
2324
|
await this.delegate.connect();
|
|
2321
|
-
|
|
2325
|
+
if (grant === null) {
|
|
2326
|
+
this.startHeartbeat(conversation.id, HEARTBEAT_INTERVAL_MS);
|
|
2327
|
+
}
|
|
2322
2328
|
} catch (error) {
|
|
2323
2329
|
if (attempt !== this.attempt) {
|
|
2324
2330
|
throw error;
|
|
@@ -2332,6 +2338,8 @@ class NuanceConversation {
|
|
|
2332
2338
|
this.attempt += 1;
|
|
2333
2339
|
this.clearLimit();
|
|
2334
2340
|
this.stopHeartbeat();
|
|
2341
|
+
this.stopEvents?.();
|
|
2342
|
+
this.stopEvents = null;
|
|
2335
2343
|
this.admitted?.reject(new Error("Disconnected while waiting for a slot"));
|
|
2336
2344
|
this.admitted = null;
|
|
2337
2345
|
this.delegateUnsubscribe?.();
|
|
@@ -2390,19 +2398,146 @@ class NuanceConversation {
|
|
|
2390
2398
|
}
|
|
2391
2399
|
});
|
|
2392
2400
|
}
|
|
2393
|
-
async
|
|
2401
|
+
async waitForSlot(conversationId, attempt) {
|
|
2402
|
+
const admission = await this.followEvents(conversationId, attempt);
|
|
2403
|
+
if (admission !== null) {
|
|
2404
|
+
this.followLive(conversationId, admission.after, attempt);
|
|
2405
|
+
return admission.grant;
|
|
2406
|
+
}
|
|
2407
|
+
this.admitted = Promise.withResolvers();
|
|
2408
|
+
this.startHeartbeat(conversationId, QUEUE_HEARTBEAT_INTERVAL_MS);
|
|
2409
|
+
await this.admitted.promise;
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
async followEvents(conversationId, attempt) {
|
|
2413
|
+
const abort = new AbortController;
|
|
2414
|
+
const stop = () => abort.abort();
|
|
2415
|
+
this.stopEvents = stop;
|
|
2416
|
+
let after = 0;
|
|
2417
|
+
let answered = false;
|
|
2418
|
+
let failures = 0;
|
|
2419
|
+
try {
|
|
2420
|
+
for (;; ) {
|
|
2421
|
+
let events;
|
|
2422
|
+
try {
|
|
2423
|
+
({ events } = await this.api.conversationEvents(conversationId, after, abort.signal));
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
if (attempt !== this.attempt)
|
|
2426
|
+
throw new DisconnectedError;
|
|
2427
|
+
if (!answered && error instanceof ApiError && error.status === 404) {
|
|
2428
|
+
return null;
|
|
2429
|
+
}
|
|
2430
|
+
failures += 1;
|
|
2431
|
+
if (failures >= ADMISSION_FAILED_REQUESTS) {
|
|
2432
|
+
throw new Error("Lost contact with the server while waiting", {
|
|
2433
|
+
cause: error
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
await this.pause(EVENTS_RETRY_MS, abort.signal);
|
|
2437
|
+
if (attempt !== this.attempt)
|
|
2438
|
+
throw new DisconnectedError;
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
if (attempt !== this.attempt)
|
|
2442
|
+
throw new DisconnectedError;
|
|
2443
|
+
answered = true;
|
|
2444
|
+
failures = 0;
|
|
2445
|
+
for (const event of events) {
|
|
2446
|
+
if (event.type === "admitted") {
|
|
2447
|
+
const grant = {
|
|
2448
|
+
server_url: event.livekit_url,
|
|
2449
|
+
participant_token: event.token
|
|
2450
|
+
};
|
|
2451
|
+
return { grant, after: event.id };
|
|
2452
|
+
}
|
|
2453
|
+
after = Math.max(after, event.id);
|
|
2454
|
+
if (event.type === "ended") {
|
|
2455
|
+
throw new TerminalStatusError(event.reason);
|
|
2456
|
+
}
|
|
2457
|
+
if (event.type === "queued" && event.position != null) {
|
|
2458
|
+
this.reportPlace(event.position, event.estimated_wait_seconds ?? null, null);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
} finally {
|
|
2463
|
+
if (this.stopEvents === stop)
|
|
2464
|
+
this.stopEvents = null;
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
async followLive(conversationId, after, attempt) {
|
|
2468
|
+
const abort = new AbortController;
|
|
2469
|
+
const stop = () => abort.abort();
|
|
2470
|
+
this.stopEvents = stop;
|
|
2471
|
+
let failures = 0;
|
|
2472
|
+
try {
|
|
2473
|
+
while (attempt === this.attempt && !this.ended) {
|
|
2474
|
+
let events;
|
|
2475
|
+
try {
|
|
2476
|
+
({ events } = await this.api.conversationEvents(conversationId, after, abort.signal));
|
|
2477
|
+
} catch {
|
|
2478
|
+
if (attempt !== this.attempt)
|
|
2479
|
+
return;
|
|
2480
|
+
failures += 1;
|
|
2481
|
+
await this.pause(EVENTS_RETRY_MS, abort.signal);
|
|
2482
|
+
continue;
|
|
2483
|
+
}
|
|
2484
|
+
if (attempt !== this.attempt)
|
|
2485
|
+
return;
|
|
2486
|
+
const receivedAt = Date.now();
|
|
2487
|
+
this.lostContact = failures > 0;
|
|
2488
|
+
failures = 0;
|
|
2489
|
+
for (const event of events) {
|
|
2490
|
+
after = Math.max(after, event.id);
|
|
2491
|
+
if (event.type === "started" && event.seconds_remaining != null) {
|
|
2492
|
+
this.capped = true;
|
|
2493
|
+
this.emit({
|
|
2494
|
+
type: "time-remaining",
|
|
2495
|
+
secondsRemaining: event.seconds_remaining
|
|
2496
|
+
});
|
|
2497
|
+
this.armLimit(event.seconds_remaining, receivedAt);
|
|
2498
|
+
}
|
|
2499
|
+
if (event.type === "ended") {
|
|
2500
|
+
this.endSession(this.endReason(event.reason));
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
} finally {
|
|
2506
|
+
if (this.stopEvents === stop)
|
|
2507
|
+
this.stopEvents = null;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
endReason(reason) {
|
|
2511
|
+
if (reason === "time_limit" || this.atLimit())
|
|
2512
|
+
return "time-limit";
|
|
2513
|
+
if (reason === "policy_violation")
|
|
2514
|
+
return "policy-violation";
|
|
2515
|
+
if (this.cutOff())
|
|
2516
|
+
return "connection-lost";
|
|
2517
|
+
return "ended";
|
|
2518
|
+
}
|
|
2519
|
+
pause(ms, signal) {
|
|
2520
|
+
return new Promise((resolve) => {
|
|
2521
|
+
let stopTicker = () => {};
|
|
2522
|
+
const done = () => {
|
|
2523
|
+
stopTicker();
|
|
2524
|
+
signal.removeEventListener("abort", done);
|
|
2525
|
+
resolve();
|
|
2526
|
+
};
|
|
2527
|
+
stopTicker = this.ticker(done, ms);
|
|
2528
|
+
signal.addEventListener("abort", done);
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
async createDelegate(conversation, grant, mediaDevices, attempt) {
|
|
2394
2532
|
if (conversation.provider !== "livekit") {
|
|
2395
2533
|
throw new Error(`Unsupported provider ${conversation.provider}`);
|
|
2396
2534
|
}
|
|
2397
|
-
const
|
|
2535
|
+
const connection = grant ?? await this.connectRoom(conversation.id);
|
|
2398
2536
|
if (attempt !== this.attempt) {
|
|
2399
2537
|
throw new DisconnectedError;
|
|
2400
2538
|
}
|
|
2401
|
-
if (response.connection.kind !== "livekit") {
|
|
2402
|
-
throw new Error(`Expected LiveKit connection, received ${response.connection.kind}`);
|
|
2403
|
-
}
|
|
2404
2539
|
this.resilient = typeof this.resilientReconnect === "function" ? this.resilientReconnect() : this.resilientReconnect;
|
|
2405
|
-
return this.provider(
|
|
2540
|
+
return this.provider(connection, {
|
|
2406
2541
|
conversationId: conversation.id,
|
|
2407
2542
|
mediaDevices,
|
|
2408
2543
|
cameraEnabled: !this.sessionOverrides.disable_camera && mediaDevices.cameraEnabled !== false,
|
|
@@ -2411,6 +2546,13 @@ class NuanceConversation {
|
|
|
2411
2546
|
resilientReconnect: this.resilient
|
|
2412
2547
|
});
|
|
2413
2548
|
}
|
|
2549
|
+
async connectRoom(conversationId) {
|
|
2550
|
+
const response = await this.api.connectConversation(conversationId);
|
|
2551
|
+
if (response.connection.kind !== "livekit") {
|
|
2552
|
+
throw new Error(`Expected LiveKit connection, received ${response.connection.kind}`);
|
|
2553
|
+
}
|
|
2554
|
+
return response.connection;
|
|
2555
|
+
}
|
|
2414
2556
|
async createWhenNotBusy(request, attempt) {
|
|
2415
2557
|
const giveUpAt = Date.now() + BUSY_RETRY_LIMIT_MS;
|
|
2416
2558
|
for (;; ) {
|
|
@@ -2458,7 +2600,7 @@ class NuanceConversation {
|
|
|
2458
2600
|
return;
|
|
2459
2601
|
if (!(error instanceof TerminalStatusError)) {
|
|
2460
2602
|
this.failedBeats += 1;
|
|
2461
|
-
if (this.admitted && this.failedBeats >=
|
|
2603
|
+
if (this.admitted && this.failedBeats >= ADMISSION_FAILED_REQUESTS) {
|
|
2462
2604
|
this.admitted.reject(new Error("Lost contact with the server while waiting", {
|
|
2463
2605
|
cause: error
|
|
2464
2606
|
}));
|
|
@@ -2497,14 +2639,17 @@ class NuanceConversation {
|
|
|
2497
2639
|
if (state.status !== "queued" || state.position == null) {
|
|
2498
2640
|
return;
|
|
2499
2641
|
}
|
|
2642
|
+
this.reportPlace(state.position, state.estimated_wait_seconds ?? null, state.pool_slots ?? null);
|
|
2643
|
+
}
|
|
2644
|
+
reportPlace(position, estimate, poolSlots) {
|
|
2500
2645
|
if (this._state !== "queued") {
|
|
2501
2646
|
this.setState("queued");
|
|
2502
2647
|
}
|
|
2503
2648
|
this.emit({
|
|
2504
2649
|
type: "queue-changed",
|
|
2505
|
-
position
|
|
2506
|
-
estimatedWaitSeconds: this.smoothEstimate(
|
|
2507
|
-
poolSlots
|
|
2650
|
+
position,
|
|
2651
|
+
estimatedWaitSeconds: this.smoothEstimate(position, estimate),
|
|
2652
|
+
poolSlots
|
|
2508
2653
|
});
|
|
2509
2654
|
}
|
|
2510
2655
|
smoothEstimate(position, estimate) {
|