@convai/web-sdk 1.3.2-beta.0 → 1.4.0-beta.1
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 +258 -6
- package/dist/core/AudioManager.d.ts +4 -0
- package/dist/core/AudioManager.d.ts.map +1 -1
- package/dist/core/AudioManager.js +26 -0
- package/dist/core/AudioManager.js.map +1 -1
- package/dist/core/ConvaiClient.d.ts +34 -0
- package/dist/core/ConvaiClient.d.ts.map +1 -1
- package/dist/core/ConvaiClient.js +302 -225
- package/dist/core/ConvaiClient.js.map +1 -1
- package/dist/core/MessageHandler.d.ts +10 -0
- package/dist/core/MessageHandler.d.ts.map +1 -1
- package/dist/core/MessageHandler.js +86 -22
- package/dist/core/MessageHandler.js.map +1 -1
- package/dist/core/WebSocketSession.d.ts +45 -0
- package/dist/core/WebSocketSession.d.ts.map +1 -0
- package/dist/core/WebSocketSession.js +133 -0
- package/dist/core/WebSocketSession.js.map +1 -0
- package/dist/core/types.d.ts +77 -49
- package/dist/core/types.d.ts.map +1 -1
- package/dist/react/components/ConvaiWidget.d.ts.map +1 -1
- package/dist/react/components/ConvaiWidget.js +25 -14
- package/dist/react/components/ConvaiWidget.js.map +1 -1
- package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
- package/dist/react/hooks/useConvaiClient.js +16 -0
- package/dist/react/hooks/useConvaiClient.js.map +1 -1
- package/dist/vanilla/AudioRenderer.d.ts +6 -0
- package/dist/vanilla/AudioRenderer.d.ts.map +1 -1
- package/dist/vanilla/AudioRenderer.js +21 -0
- package/dist/vanilla/AudioRenderer.js.map +1 -1
- package/dist/vanilla/types.d.ts +3 -1
- package/dist/vanilla/types.d.ts.map +1 -1
- package/package.json +3 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Room, RoomEvent, DefaultReconnectPolicy, } from "livekit-client";
|
|
1
|
+
import { Room, RoomEvent, DefaultReconnectPolicy, setLogLevel, } from "livekit-client";
|
|
2
|
+
setLogLevel("warn");
|
|
2
3
|
import { AudioManager } from "./AudioManager";
|
|
3
4
|
import { VideoManager } from "./VideoManager";
|
|
4
5
|
import { ScreenShareManager } from "./ScreenShareManager";
|
|
@@ -6,6 +7,7 @@ import { MessageHandler } from "./MessageHandler";
|
|
|
6
7
|
import { EventEmitter } from "./EventEmitter";
|
|
7
8
|
import { MemoryManager } from "./MemoryManager";
|
|
8
9
|
import { ConnectionStateHandler } from "./ConnectionStateHandler";
|
|
10
|
+
import { WebSocketSession } from "./WebSocketSession";
|
|
9
11
|
/**
|
|
10
12
|
* Custom reconnect policy that disables automatic reconnection
|
|
11
13
|
*/
|
|
@@ -64,6 +66,13 @@ export class ConvaiClient extends EventEmitter {
|
|
|
64
66
|
this._storedConfig = null;
|
|
65
67
|
this._endUserId = null;
|
|
66
68
|
this._endUserMetadata = null;
|
|
69
|
+
this._wsSession = null;
|
|
70
|
+
this._activeTransport = null;
|
|
71
|
+
this._logRtviMessages = true;
|
|
72
|
+
// RTVI client-ready handshake (race-proof bot-ready delivery)
|
|
73
|
+
this._clientReadyRetryTimer = null;
|
|
74
|
+
this._clientReadyTimeoutTimer = null;
|
|
75
|
+
this._clientReadyMsgId = null;
|
|
67
76
|
// Conversation session tracking
|
|
68
77
|
this._conversationSessionId = 0;
|
|
69
78
|
this._conversationStartTime = 0;
|
|
@@ -72,6 +81,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
72
81
|
// Store config if provided
|
|
73
82
|
if (config) {
|
|
74
83
|
this._storedConfig = config;
|
|
84
|
+
this._logRtviMessages = config.logRtviMessages !== false;
|
|
75
85
|
}
|
|
76
86
|
// Initialize room with no reconnect policy
|
|
77
87
|
this._room = new Room({
|
|
@@ -104,6 +114,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
104
114
|
this._videoManager = new VideoManager(this._room);
|
|
105
115
|
this._screenShareManager = new ScreenShareManager(this._room);
|
|
106
116
|
this._messageHandler = new MessageHandler(this._room);
|
|
117
|
+
this._messageHandler.setRtviMessageLogging(this._logRtviMessages);
|
|
107
118
|
this._connectionStateHandler = new ConnectionStateHandler(this.updateState.bind(this), (ready) => (this._isBotReady = ready), () => {
|
|
108
119
|
this._messageHandler.reset();
|
|
109
120
|
this._endUserId = null;
|
|
@@ -177,7 +188,13 @@ export class ConvaiClient extends EventEmitter {
|
|
|
177
188
|
this._room.on(RoomEvent.ConnectionStateChanged, this._connectionStateHandler.handleConnectionStateChanged.bind(this._connectionStateHandler));
|
|
178
189
|
// Message handler events
|
|
179
190
|
this._messageHandler.on("botReady", () => {
|
|
191
|
+
// Server may send multiple bot-ready frames (one per client-ready
|
|
192
|
+
// we retried). Only surface the first to consumers per connect.
|
|
193
|
+
if (this._isBotReady) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
180
196
|
this._isBotReady = true;
|
|
197
|
+
this._stopClientReadyHandshake();
|
|
181
198
|
this.updateState({ isConnected: true });
|
|
182
199
|
this.emit("botReady");
|
|
183
200
|
});
|
|
@@ -280,6 +297,15 @@ export class ConvaiClient extends EventEmitter {
|
|
|
280
297
|
this._messageHandler.on("userMuteStopped", () => {
|
|
281
298
|
this.emit("userMuteStopped");
|
|
282
299
|
});
|
|
300
|
+
this._messageHandler.on("actionResponse", (data) => {
|
|
301
|
+
this.emit("actionResponse", data);
|
|
302
|
+
});
|
|
303
|
+
this._messageHandler.on("serverResponse", (response) => {
|
|
304
|
+
this.emit("serverResponse", response);
|
|
305
|
+
});
|
|
306
|
+
this._messageHandler.on("interactionCreated", (data) => {
|
|
307
|
+
this.emit("interactionCreated", data);
|
|
308
|
+
});
|
|
283
309
|
}
|
|
284
310
|
/**
|
|
285
311
|
* Setup MessageHandler to respond to conversation start events
|
|
@@ -318,6 +344,31 @@ export class ConvaiClient extends EventEmitter {
|
|
|
318
344
|
this.emit("stateChange", this._state);
|
|
319
345
|
}
|
|
320
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Send a data message via the active transport.
|
|
349
|
+
* For LiveKit: publishes via the data channel.
|
|
350
|
+
* For WebSocket: sends via PipecatClient.sendClientMessage (wrapped in {t,d} envelope).
|
|
351
|
+
*/
|
|
352
|
+
publishMessage(type, data) {
|
|
353
|
+
const message = { type, data };
|
|
354
|
+
if (this._activeTransport === "websocket" && this._wsSession?.isConnected) {
|
|
355
|
+
this._wsSession.sendMessage(type, data);
|
|
356
|
+
}
|
|
357
|
+
else if (this._room?.localParticipant &&
|
|
358
|
+
this._room.state !== "disconnected") {
|
|
359
|
+
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
360
|
+
this._room.localParticipant.publishData(encodedData, { reliable: true });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Returns true when a message can be sent on the active transport.
|
|
365
|
+
*/
|
|
366
|
+
isTransportReady() {
|
|
367
|
+
if (this._activeTransport === "websocket") {
|
|
368
|
+
return this._wsSession?.isConnected ?? false;
|
|
369
|
+
}
|
|
370
|
+
return (!!this._room?.localParticipant && this._room.state !== "disconnected");
|
|
371
|
+
}
|
|
321
372
|
/**
|
|
322
373
|
* Connect to a Convai character
|
|
323
374
|
*/
|
|
@@ -336,9 +387,11 @@ export class ConvaiClient extends EventEmitter {
|
|
|
336
387
|
}
|
|
337
388
|
// Add default URL if not provided
|
|
338
389
|
const configWithDefaults = {
|
|
339
|
-
url: "https://realtime-api
|
|
390
|
+
url: "https://realtime-api.convai.com",
|
|
340
391
|
...finalConfig,
|
|
341
392
|
};
|
|
393
|
+
this._logRtviMessages = configWithDefaults.logRtviMessages !== false;
|
|
394
|
+
this._messageHandler.setRtviMessageLogging(this._logRtviMessages);
|
|
342
395
|
const hasApiKey = Boolean(configWithDefaults.apiKey);
|
|
343
396
|
const hasAuthToken = Boolean(configWithDefaults.authToken);
|
|
344
397
|
if ((!hasApiKey && !hasAuthToken) || !configWithDefaults.characterId) {
|
|
@@ -354,6 +407,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
354
407
|
// Determine connection type based on enableVideo
|
|
355
408
|
const connType = configWithDefaults.enableVideo ? "video" : "audio";
|
|
356
409
|
this._connectionType = connType;
|
|
410
|
+
const transportType = configWithDefaults.transport ?? "livekit";
|
|
411
|
+
this._activeTransport = transportType;
|
|
357
412
|
// Prepare request body with required parameters
|
|
358
413
|
const characterSessionIdToSend = configWithDefaults.characterSessionId ?? this._characterSessionId;
|
|
359
414
|
const requestBody = {
|
|
@@ -364,7 +419,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
364
419
|
...(configWithDefaults.endUserMetadata != null && {
|
|
365
420
|
end_user_metadata: configWithDefaults.endUserMetadata,
|
|
366
421
|
}),
|
|
367
|
-
transport:
|
|
422
|
+
transport: transportType,
|
|
368
423
|
connection_type: connType,
|
|
369
424
|
blendshape_provider: configWithDefaults.enableLipsync
|
|
370
425
|
? "neurosync"
|
|
@@ -377,7 +432,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
377
432
|
output_fps: 90,
|
|
378
433
|
},
|
|
379
434
|
llm_provider: "dynamic",
|
|
380
|
-
...(configWithDefaults.enableEmotion
|
|
435
|
+
...(configWithDefaults.enableEmotion === true && {
|
|
381
436
|
emotion_config: {
|
|
382
437
|
provider: configWithDefaults.emotionConfig?.provider ?? "llm",
|
|
383
438
|
min_word_threshold: configWithDefaults.emotionConfig?.min_word_threshold ?? 3,
|
|
@@ -395,19 +450,14 @@ export class ConvaiClient extends EventEmitter {
|
|
|
395
450
|
character_session_id: characterSessionIdToSend,
|
|
396
451
|
}),
|
|
397
452
|
...(configWithDefaults.actionConfig && {
|
|
398
|
-
action_config:
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
...(configWithDefaults.currentAttentionObject && {
|
|
407
|
-
current_attention_object: configWithDefaults.currentAttentionObject,
|
|
408
|
-
}),
|
|
409
|
-
...(configWithDefaults.sceneDescription && {
|
|
410
|
-
scene_description: configWithDefaults.sceneDescription,
|
|
453
|
+
action_config: {
|
|
454
|
+
actions: configWithDefaults.actionConfig.actions,
|
|
455
|
+
objects: configWithDefaults.actionConfig.objects,
|
|
456
|
+
characters: configWithDefaults.actionConfig.characters,
|
|
457
|
+
...(configWithDefaults.actionConfig.current_attention_object && {
|
|
458
|
+
current_attention_object: configWithDefaults.actionConfig.current_attention_object,
|
|
459
|
+
}),
|
|
460
|
+
},
|
|
411
461
|
}),
|
|
412
462
|
...(configWithDefaults.dynamicInfo && {
|
|
413
463
|
dynamic_info: {
|
|
@@ -429,18 +479,19 @@ export class ConvaiClient extends EventEmitter {
|
|
|
429
479
|
}),
|
|
430
480
|
},
|
|
431
481
|
};
|
|
432
|
-
const
|
|
482
|
+
const connectHeaders = {
|
|
433
483
|
"Content-Type": "application/json",
|
|
434
484
|
};
|
|
435
485
|
if (configWithDefaults.authToken) {
|
|
436
|
-
|
|
486
|
+
connectHeaders["API-AUTH-TOKEN"] = configWithDefaults.authToken;
|
|
437
487
|
}
|
|
438
488
|
else if (configWithDefaults.apiKey) {
|
|
439
|
-
|
|
489
|
+
connectHeaders["X-API-Key"] = configWithDefaults.apiKey;
|
|
440
490
|
}
|
|
491
|
+
// HTTP POST /connect — same request for both transports
|
|
441
492
|
const response = await fetch(`${configWithDefaults.url}/connect`, {
|
|
442
493
|
method: "POST",
|
|
443
|
-
headers,
|
|
494
|
+
headers: connectHeaders,
|
|
444
495
|
body: JSON.stringify(requestBody),
|
|
445
496
|
});
|
|
446
497
|
if (!response.ok) {
|
|
@@ -466,7 +517,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
466
517
|
throw new Error(errorMessage);
|
|
467
518
|
}
|
|
468
519
|
const connectionData = await response.json();
|
|
469
|
-
// Capture
|
|
520
|
+
// Capture session identifiers from /connect response
|
|
470
521
|
if (connectionData.character_session_id) {
|
|
471
522
|
this._characterSessionId = connectionData.character_session_id;
|
|
472
523
|
this._storedConfig = {
|
|
@@ -474,39 +525,87 @@ export class ConvaiClient extends EventEmitter {
|
|
|
474
525
|
characterSessionId: connectionData.character_session_id,
|
|
475
526
|
};
|
|
476
527
|
}
|
|
477
|
-
// Capture end_user_id and end_user_metadata from connection response
|
|
478
528
|
if (connectionData.end_user_id) {
|
|
479
529
|
this._endUserId = connectionData.end_user_id;
|
|
480
530
|
}
|
|
481
531
|
if (connectionData.end_user_metadata) {
|
|
482
532
|
this._endUserMetadata = connectionData.end_user_metadata;
|
|
483
533
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
channelCount: this._audioSettings.channelCount,
|
|
534
|
+
if (transportType === "websocket") {
|
|
535
|
+
// ── WebSocket (Pipecat) transport path ────────────────────────────
|
|
536
|
+
// Server returns the WebSocket URL in room_url (same field as LiveKit).
|
|
537
|
+
const wsUrl = connectionData.room_url;
|
|
538
|
+
if (!wsUrl) {
|
|
539
|
+
throw new Error("No WebSocket URL returned from /connect endpoint (expected room_url)");
|
|
540
|
+
}
|
|
541
|
+
// Always enable mic at construction so WavMediaManager initializes the
|
|
542
|
+
// audio stream during connect(). If startWithAudioOn is false, we mute
|
|
543
|
+
// after the connection is established (enableMic(false) works once the
|
|
544
|
+
// stream exists, but not before it is initialized).
|
|
545
|
+
this._wsSession = new WebSocketSession((payload) => this._messageHandler.handleDataReceivedPublic(payload), true);
|
|
546
|
+
this._wsSession.on("botAudioTrack", (track) => {
|
|
547
|
+
this.emit("botAudioTrack", track);
|
|
499
548
|
});
|
|
549
|
+
this._wsSession.on("disconnected", () => {
|
|
550
|
+
this._connectionStateHandler.handleDisconnected();
|
|
551
|
+
});
|
|
552
|
+
this._audioManager.setWebSocketSession(this._wsSession);
|
|
553
|
+
// Wait for WebSocket transport to connect (fires before bot-ready)
|
|
554
|
+
await new Promise((resolve, reject) => {
|
|
555
|
+
const onConnected = () => {
|
|
556
|
+
this._wsSession.off("connected", onConnected);
|
|
557
|
+
this._wsSession.off("error", onError);
|
|
558
|
+
resolve();
|
|
559
|
+
};
|
|
560
|
+
const onError = (err) => {
|
|
561
|
+
this._wsSession.off("connected", onConnected);
|
|
562
|
+
this._wsSession.off("error", onError);
|
|
563
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
564
|
+
};
|
|
565
|
+
this._wsSession.on("connected", onConnected);
|
|
566
|
+
this._wsSession.on("error", onError);
|
|
567
|
+
// connectWithUrl calls initDevices() then connect({ wsUrl }) — matches sandbox flow
|
|
568
|
+
this._wsSession.connectWithUrl(wsUrl).catch((err) => {
|
|
569
|
+
this._wsSession.off("connected", onConnected);
|
|
570
|
+
this._wsSession.off("error", onError);
|
|
571
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
// For WebSocket, mic is already streaming after connect.
|
|
575
|
+
// Only mute if the caller explicitly sets startWithAudioOn: false.
|
|
576
|
+
// Defaulting to on matches the sandbox behavior and ensures the bot
|
|
577
|
+
// sees audio when it initializes (avoids server "tap mic to talk" prompt).
|
|
578
|
+
if (configWithDefaults.startWithAudioOn === false) {
|
|
579
|
+
this._wsSession.enableMic(false);
|
|
580
|
+
this._audioManager.syncWsAudioState(false);
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
this._audioManager.syncWsAudioState(true);
|
|
584
|
+
}
|
|
500
585
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
586
|
+
else {
|
|
587
|
+
// ── LiveKit transport path (default) ─────────────────────────────
|
|
588
|
+
await this._room.connect(connectionData.room_url, connectionData.token, {
|
|
589
|
+
rtcConfig: {
|
|
590
|
+
iceTransportPolicy: "relay",
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
if (configWithDefaults.startWithAudioOn) {
|
|
594
|
+
await this._room.localParticipant.setMicrophoneEnabled(true, {
|
|
595
|
+
echoCancellation: this._audioSettings.echoCancellation,
|
|
596
|
+
noiseSuppression: this._audioSettings.noiseSuppression,
|
|
597
|
+
autoGainControl: this._audioSettings.autoGainControl,
|
|
598
|
+
sampleRate: this._audioSettings.sampleRate,
|
|
599
|
+
channelCount: this._audioSettings.channelCount,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
if (configWithDefaults.enableVideo &&
|
|
603
|
+
configWithDefaults.startWithVideoOn) {
|
|
604
|
+
await this._room.localParticipant.setCameraEnabled(true);
|
|
605
|
+
}
|
|
606
|
+
this._audioManager.syncStateFromRoom({ emit: true });
|
|
607
|
+
this._participantSid = this._room.localParticipant.sid;
|
|
505
608
|
}
|
|
506
|
-
// Ensure audio manager mirrors the actual microphone permission state
|
|
507
|
-
this._audioManager.syncStateFromRoom({ emit: true });
|
|
508
|
-
// Capture participant SID
|
|
509
|
-
this._participantSid = this._room.localParticipant.sid;
|
|
510
609
|
// Apply custom mapper to blendshape queue if provided
|
|
511
610
|
if (configWithDefaults.blendshapeConfig?.customMapper) {
|
|
512
611
|
this.blendshapeQueue.setMapper(configWithDefaults.blendshapeConfig.customMapper);
|
|
@@ -531,6 +630,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
531
630
|
}
|
|
532
631
|
}
|
|
533
632
|
this.emit("connect");
|
|
633
|
+
this._startClientReadyHandshake();
|
|
534
634
|
}
|
|
535
635
|
catch (error) {
|
|
536
636
|
this.updateState({
|
|
@@ -541,11 +641,82 @@ export class ConvaiClient extends EventEmitter {
|
|
|
541
641
|
throw error;
|
|
542
642
|
}
|
|
543
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* RTVI client-ready handshake. Server-side gates bot-ready emission on
|
|
646
|
+
* receipt of client-ready (and re-emits bot-ready on every client-ready),
|
|
647
|
+
* so we can retry until we hear bot-ready back. Closes the race where the
|
|
648
|
+
* bot publishes bot-ready before the client data-channel is subscribed.
|
|
649
|
+
*/
|
|
650
|
+
_startClientReadyHandshake() {
|
|
651
|
+
// RTVI client-ready handshake is LiveKit-only. WebSocket transport
|
|
652
|
+
// uses its own pipecat ready signalling.
|
|
653
|
+
if (this._activeTransport !== "livekit") {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
this._stopClientReadyHandshake();
|
|
657
|
+
this._clientReadyMsgId =
|
|
658
|
+
typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
659
|
+
? crypto.randomUUID()
|
|
660
|
+
: `client-ready-${Date.now()}-${Math.random()}`;
|
|
661
|
+
const send = () => {
|
|
662
|
+
if (this._isBotReady)
|
|
663
|
+
return;
|
|
664
|
+
if (!this._room || this._room.state !== "connected")
|
|
665
|
+
return;
|
|
666
|
+
try {
|
|
667
|
+
const msg = {
|
|
668
|
+
id: this._clientReadyMsgId,
|
|
669
|
+
label: "rtvi-ai",
|
|
670
|
+
type: "client-ready",
|
|
671
|
+
data: {
|
|
672
|
+
version: "1.0.0",
|
|
673
|
+
about: { library: "convai-web-sdk" },
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
const encoded = new TextEncoder().encode(JSON.stringify(msg));
|
|
677
|
+
this._room.localParticipant.publishData(encoded, { reliable: true });
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
// swallow; next retry will try again
|
|
681
|
+
}
|
|
682
|
+
this._clientReadyRetryTimer = setTimeout(send, ConvaiClient.CLIENT_READY_RETRY_MS);
|
|
683
|
+
};
|
|
684
|
+
send();
|
|
685
|
+
this._clientReadyTimeoutTimer = setTimeout(() => {
|
|
686
|
+
if (this._isBotReady)
|
|
687
|
+
return;
|
|
688
|
+
this._stopClientReadyHandshake();
|
|
689
|
+
const err = new Error(`bot-ready not received within ${ConvaiClient.CLIENT_READY_TIMEOUT_MS}ms`);
|
|
690
|
+
this.emit("error", err);
|
|
691
|
+
}, ConvaiClient.CLIENT_READY_TIMEOUT_MS);
|
|
692
|
+
}
|
|
693
|
+
_stopClientReadyHandshake() {
|
|
694
|
+
if (this._clientReadyRetryTimer) {
|
|
695
|
+
clearTimeout(this._clientReadyRetryTimer);
|
|
696
|
+
this._clientReadyRetryTimer = null;
|
|
697
|
+
}
|
|
698
|
+
if (this._clientReadyTimeoutTimer) {
|
|
699
|
+
clearTimeout(this._clientReadyTimeoutTimer);
|
|
700
|
+
this._clientReadyTimeoutTimer = null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
544
703
|
/**
|
|
545
704
|
* Disconnect from the current character session
|
|
546
705
|
*/
|
|
547
706
|
async disconnect() {
|
|
548
|
-
|
|
707
|
+
this._stopClientReadyHandshake();
|
|
708
|
+
if (this._activeTransport === "websocket" && this._wsSession) {
|
|
709
|
+
try {
|
|
710
|
+
await this._wsSession.disconnect();
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
// ignore disconnect errors
|
|
714
|
+
}
|
|
715
|
+
this._wsSession = null;
|
|
716
|
+
this._connectionStateHandler.resetConnectionState();
|
|
717
|
+
this.clearAllManagers();
|
|
718
|
+
}
|
|
719
|
+
else if (this._room && this._room.state !== "disconnected") {
|
|
549
720
|
try {
|
|
550
721
|
await this._room.disconnect();
|
|
551
722
|
this._connectionStateHandler.resetConnectionState();
|
|
@@ -563,6 +734,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
563
734
|
*/
|
|
564
735
|
clearAllManagers() {
|
|
565
736
|
this._connectionType = null;
|
|
737
|
+
this._activeTransport = null;
|
|
738
|
+
this._wsSession = null;
|
|
566
739
|
this._apiKey = null;
|
|
567
740
|
this._authToken = null;
|
|
568
741
|
this._characterId = null;
|
|
@@ -570,6 +743,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
570
743
|
this._endUserId = null;
|
|
571
744
|
this._endUserMetadata = null;
|
|
572
745
|
this._memoryManager = null; // Clear memory manager on disconnect
|
|
746
|
+
this._audioManager.setWebSocketSession(null);
|
|
573
747
|
this._audioManager.reset();
|
|
574
748
|
this._videoManager.reset();
|
|
575
749
|
this._screenShareManager.reset();
|
|
@@ -596,14 +770,10 @@ export class ConvaiClient extends EventEmitter {
|
|
|
596
770
|
* Send a text message to the character
|
|
597
771
|
*/
|
|
598
772
|
sendUserTextMessage(text) {
|
|
599
|
-
if (!this.
|
|
600
|
-
this._room.state === "disconnected" ||
|
|
601
|
-
!this._room.localParticipant) {
|
|
773
|
+
if (!this.isTransportReady())
|
|
602
774
|
return;
|
|
603
|
-
|
|
604
|
-
if (!text || !text.trim()) {
|
|
775
|
+
if (!text || !text.trim())
|
|
605
776
|
return;
|
|
606
|
-
}
|
|
607
777
|
try {
|
|
608
778
|
// Reset blendshape queue if mid-conversation, but don't interrupt the server
|
|
609
779
|
if (this.blendshapeQueue.isConversationActive() ||
|
|
@@ -619,16 +789,9 @@ export class ConvaiClient extends EventEmitter {
|
|
|
619
789
|
userMessage: text.trim(),
|
|
620
790
|
timestamp: this._conversationStartTime,
|
|
621
791
|
});
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
text: text.trim(),
|
|
626
|
-
participant_sid: this._participantSid || this._room.localParticipant.sid,
|
|
627
|
-
},
|
|
628
|
-
};
|
|
629
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
630
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
631
|
-
reliable: true,
|
|
792
|
+
this.publishMessage("user_text_message", {
|
|
793
|
+
text: text.trim(),
|
|
794
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
632
795
|
});
|
|
633
796
|
}
|
|
634
797
|
catch (error) {
|
|
@@ -639,32 +802,24 @@ export class ConvaiClient extends EventEmitter {
|
|
|
639
802
|
* Send a trigger message to invoke specific character actions
|
|
640
803
|
*/
|
|
641
804
|
sendTriggerMessage(triggerName, triggerMessage) {
|
|
642
|
-
if (this.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
participant_sid: this._participantSid || this._room.localParticipant.sid,
|
|
661
|
-
},
|
|
662
|
-
};
|
|
663
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
664
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
665
|
-
reliable: true,
|
|
666
|
-
});
|
|
667
|
-
}
|
|
805
|
+
if (!this.isTransportReady())
|
|
806
|
+
return;
|
|
807
|
+
// Reset blendshape queue if mid-conversation, but don't interrupt the server
|
|
808
|
+
if (this.blendshapeQueue.isConversationActive() || this._state.isSpeaking) {
|
|
809
|
+
this.blendshapeQueue.interrupt();
|
|
810
|
+
}
|
|
811
|
+
this._conversationSessionId++;
|
|
812
|
+
this._conversationStartTime = Date.now();
|
|
813
|
+
this.emit("conversationStart", {
|
|
814
|
+
sessionId: this._conversationSessionId,
|
|
815
|
+
userMessage: `[trigger:${triggerName ?? ""}]`,
|
|
816
|
+
timestamp: this._conversationStartTime,
|
|
817
|
+
});
|
|
818
|
+
this.publishMessage("trigger-message", {
|
|
819
|
+
...(triggerName && { trigger_name: triggerName }),
|
|
820
|
+
...(triggerMessage && { trigger_message: triggerMessage }),
|
|
821
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
822
|
+
});
|
|
668
823
|
}
|
|
669
824
|
/**
|
|
670
825
|
* If the bot is currently speaking or a conversation is active, send an
|
|
@@ -676,17 +831,10 @@ export class ConvaiClient extends EventEmitter {
|
|
|
676
831
|
const botSpeaking = this._state.isSpeaking;
|
|
677
832
|
if (!queueActive && !botSpeaking)
|
|
678
833
|
return;
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const message = {
|
|
684
|
-
type: "interrupt-bot",
|
|
685
|
-
data: {
|
|
686
|
-
participant_sid: this._participantSid || this._room.localParticipant.sid,
|
|
687
|
-
},
|
|
688
|
-
};
|
|
689
|
-
this._room.localParticipant.publishData(new TextEncoder().encode(JSON.stringify(message)), { reliable: true });
|
|
834
|
+
if (this.isTransportReady()) {
|
|
835
|
+
this.publishMessage("interrupt-bot", {
|
|
836
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
837
|
+
});
|
|
690
838
|
}
|
|
691
839
|
// End the blendshape queue so the lipsync player finishes cleanly
|
|
692
840
|
this.blendshapeQueue.interrupt();
|
|
@@ -695,26 +843,20 @@ export class ConvaiClient extends EventEmitter {
|
|
|
695
843
|
* Send an interrupt message to stop the bot's current response
|
|
696
844
|
*/
|
|
697
845
|
sendInterruptMessage() {
|
|
698
|
-
if (!this.
|
|
699
|
-
this._room.state === "disconnected" ||
|
|
700
|
-
!this._room.localParticipant) {
|
|
846
|
+
if (!this.isTransportReady())
|
|
701
847
|
return;
|
|
702
|
-
}
|
|
703
848
|
try {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
data: {
|
|
707
|
-
participant_sid: this._participantSid || this._room.localParticipant.sid,
|
|
708
|
-
},
|
|
709
|
-
};
|
|
710
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
711
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
712
|
-
reliable: true,
|
|
849
|
+
this.publishMessage("interrupt-bot", {
|
|
850
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
713
851
|
});
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
852
|
+
// Blendshape fade-out only for LiveKit (WebSocket transport has no lipsync;
|
|
853
|
+
// calling interrupt() would set conversationEnded=true and block the next
|
|
854
|
+
// bot-started-speaking from ever firing isSpeaking).
|
|
855
|
+
if (this._activeTransport !== "websocket") {
|
|
856
|
+
if (this.blendshapeQueue.isConversationActive() ||
|
|
857
|
+
this._state.isSpeaking) {
|
|
858
|
+
this.blendshapeQueue.interrupt();
|
|
859
|
+
}
|
|
718
860
|
}
|
|
719
861
|
}
|
|
720
862
|
catch (error) {
|
|
@@ -725,39 +867,23 @@ export class ConvaiClient extends EventEmitter {
|
|
|
725
867
|
* Update template keys in the character's context
|
|
726
868
|
*/
|
|
727
869
|
updateTemplateKeys(templateKeys) {
|
|
728
|
-
if (this.
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
template_keys: templateKeys,
|
|
735
|
-
participant_sid: this._participantSid || this._room.localParticipant.sid,
|
|
736
|
-
},
|
|
737
|
-
};
|
|
738
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
739
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
740
|
-
reliable: true,
|
|
741
|
-
});
|
|
742
|
-
}
|
|
870
|
+
if (!this.isTransportReady() || Object.keys(templateKeys).length === 0)
|
|
871
|
+
return;
|
|
872
|
+
this.publishMessage("update-template-keys", {
|
|
873
|
+
template_keys: templateKeys,
|
|
874
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
875
|
+
});
|
|
743
876
|
}
|
|
744
877
|
/**
|
|
745
878
|
* Update dynamic information about the current context
|
|
746
879
|
*/
|
|
747
880
|
updateDynamicInfo(dynamicInfo) {
|
|
748
|
-
if (this.
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
},
|
|
755
|
-
};
|
|
756
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
757
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
758
|
-
reliable: true,
|
|
759
|
-
});
|
|
760
|
-
}
|
|
881
|
+
if (!this.isTransportReady() || !dynamicInfo?.trim())
|
|
882
|
+
return;
|
|
883
|
+
this.publishMessage("update-dynamic-info", {
|
|
884
|
+
dynamic_info: { text: dynamicInfo },
|
|
885
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
886
|
+
});
|
|
761
887
|
}
|
|
762
888
|
/**
|
|
763
889
|
* Update the bot's temporary runtime context in a unified way.
|
|
@@ -786,19 +912,15 @@ export class ConvaiClient extends EventEmitter {
|
|
|
786
912
|
* ```
|
|
787
913
|
*/
|
|
788
914
|
updateContext(options) {
|
|
789
|
-
if (!this.
|
|
915
|
+
if (!this.isTransportReady())
|
|
790
916
|
return;
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
917
|
+
const hasText = !!options.text?.trim();
|
|
918
|
+
const hasAttention = options.current_attention_object !== undefined;
|
|
919
|
+
// text is required unless mode is "reset" or only updating attention
|
|
920
|
+
if (options.mode !== "reset" && !hasText && !hasAttention) {
|
|
921
|
+
console.warn("[ConvaiClient] updateContext: text or current_attention_object is required unless mode is 'reset'");
|
|
795
922
|
return;
|
|
796
923
|
}
|
|
797
|
-
// When run_llm is "false" the server won't respond, so no conversation
|
|
798
|
-
// lifecycle is needed. For "true", "auto", or unset the server may respond,
|
|
799
|
-
// so we treat it the same as sendUserTextMessage: interrupt any active turn
|
|
800
|
-
// and start a new conversation so the blendshape queue is ready to accept
|
|
801
|
-
// the incoming frames.
|
|
802
924
|
const willTriggerLlm = options.run_llm !== "false";
|
|
803
925
|
if (willTriggerLlm) {
|
|
804
926
|
if (this.blendshapeQueue.isConversationActive() ||
|
|
@@ -813,70 +935,38 @@ export class ConvaiClient extends EventEmitter {
|
|
|
813
935
|
timestamp: this._conversationStartTime,
|
|
814
936
|
});
|
|
815
937
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
participant_sid: this._participantSid || this._room.localParticipant.sid,
|
|
823
|
-
},
|
|
824
|
-
};
|
|
825
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
826
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
827
|
-
reliable: true,
|
|
938
|
+
this.publishMessage("context-update", {
|
|
939
|
+
...(hasText && { text: options.text.trim() }),
|
|
940
|
+
...(options.mode && { mode: options.mode }),
|
|
941
|
+
...(options.run_llm && { run_llm: options.run_llm }),
|
|
942
|
+
...(hasAttention && { current_attention_object: options.current_attention_object }),
|
|
943
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
828
944
|
});
|
|
829
945
|
}
|
|
946
|
+
/**
|
|
947
|
+
* Send descriptive scene updates. Use when environment description changes
|
|
948
|
+
* and the bot should know what is visible or nearby. Does not modify action_config.
|
|
949
|
+
*/
|
|
950
|
+
updateSceneMetadata(items) {
|
|
951
|
+
if (!this.isTransportReady())
|
|
952
|
+
return;
|
|
953
|
+
this.publishMessage("update-scene-metadata", { scene_metadata: items });
|
|
954
|
+
}
|
|
830
955
|
/**
|
|
831
956
|
* Toggle text-to-speech on or off
|
|
832
957
|
*/
|
|
833
958
|
toggleTts(enabled) {
|
|
834
|
-
if (!this.
|
|
835
|
-
this._room.state === "disconnected" ||
|
|
836
|
-
!this._room.localParticipant) {
|
|
959
|
+
if (!this.isTransportReady())
|
|
837
960
|
return;
|
|
838
|
-
}
|
|
839
|
-
try {
|
|
840
|
-
const message = {
|
|
841
|
-
type: "tts-toggle",
|
|
842
|
-
data: {
|
|
843
|
-
enabled: enabled,
|
|
844
|
-
},
|
|
845
|
-
};
|
|
846
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
847
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
848
|
-
reliable: true,
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
catch (error) {
|
|
852
|
-
throw error;
|
|
853
|
-
}
|
|
961
|
+
this.publishMessage("tts-toggle", { enabled });
|
|
854
962
|
}
|
|
855
963
|
/**
|
|
856
964
|
* Toggle speech-to-text on or off
|
|
857
965
|
*/
|
|
858
966
|
toggleStt(enabled) {
|
|
859
|
-
if (!this.
|
|
860
|
-
this._room.state === "disconnected" ||
|
|
861
|
-
!this._room.localParticipant) {
|
|
967
|
+
if (!this.isTransportReady())
|
|
862
968
|
return;
|
|
863
|
-
}
|
|
864
|
-
try {
|
|
865
|
-
const message = {
|
|
866
|
-
lable: "rtvi-ai",
|
|
867
|
-
type: "stt-toggle",
|
|
868
|
-
data: {
|
|
869
|
-
muted: !enabled,
|
|
870
|
-
},
|
|
871
|
-
};
|
|
872
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
873
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
874
|
-
reliable: true,
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
catch (error) {
|
|
878
|
-
throw error;
|
|
879
|
-
}
|
|
969
|
+
this.publishMessage("stt-toggle", { muted: !enabled });
|
|
880
970
|
}
|
|
881
971
|
/**
|
|
882
972
|
* Reset the server-side idle timer.
|
|
@@ -884,26 +974,13 @@ export class ConvaiClient extends EventEmitter {
|
|
|
884
974
|
* the session from being disconnected due to inactivity.
|
|
885
975
|
*/
|
|
886
976
|
resetIdleTimer() {
|
|
887
|
-
if (!this.
|
|
888
|
-
this._room.state === "disconnected" ||
|
|
889
|
-
!this._room.localParticipant) {
|
|
977
|
+
if (!this.isTransportReady())
|
|
890
978
|
return;
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
type: "reset-idle-timer",
|
|
895
|
-
data: {
|
|
896
|
-
participant_sid: this._participantSid || this._room.localParticipant.sid,
|
|
897
|
-
},
|
|
898
|
-
};
|
|
899
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
900
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
901
|
-
reliable: true,
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
catch (error) {
|
|
905
|
-
throw error;
|
|
906
|
-
}
|
|
979
|
+
this.publishMessage("reset-idle-timer", {
|
|
980
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
981
|
+
});
|
|
907
982
|
}
|
|
908
983
|
}
|
|
984
|
+
ConvaiClient.CLIENT_READY_RETRY_MS = 500;
|
|
985
|
+
ConvaiClient.CLIENT_READY_TIMEOUT_MS = 45000;
|
|
909
986
|
//# sourceMappingURL=ConvaiClient.js.map
|