@kaltura-sdk/avatars 1.15.4 → 1.16.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.esm.js +304 -314
- package/package.json +4 -4
- package/src/lib/avatar-client-manager/machines/avatar-connection/actions/context/remove-stale-chat-entries-for-retry.d.ts +2 -0
- package/src/lib/avatar-client-manager/machines/avatar-connection/actions/context/test-utils/as-harness-action.d.ts +4 -0
- package/src/lib/avatar-client-manager/machines/avatar-connection/avatar-connection-machine.d.ts +3 -0
- package/src/lib/avatar-client-manager/machines/avatar-connection/states/connected-state.d.ts +2 -2
- package/src/lib/avatar-client-manager/machines/avatar-connection/states/connected-states/agent-recovery-monitor-state.d.ts +2 -2
- package/src/lib/avatar-client-manager/machines/avatar-connection/types/context.d.ts +7 -3
- package/src/lib/avatar-client-manager/machines/avatar-connection/types/events.d.ts +1 -0
- package/src/lib/avatar-client-manager/machines/avatar-connection/types/index.d.ts +1 -0
- package/src/lib/avatar-client-manager/machines/avatar-connection/types/input.d.ts +2 -2
- package/src/lib/avatar-client-manager/machines/avatar-connection/types/notifications.d.ts +19 -2
- package/src/lib/avatar-client-manager/machines/avatar-connection/types/schemas.d.ts +5 -0
- package/src/lib/avatar-client-manager/types.d.ts +2 -2
package/index.esm.js
CHANGED
|
@@ -2,10 +2,10 @@ import { UnisphereLoggerFactory, TinyDataStore, TinyPubSub, performanceMonitor }
|
|
|
2
2
|
import { getPartnerIdFromKs } from '@unisphere/runtime-js';
|
|
3
3
|
import { DisconnectReasons, getAvatarDefaultCapabilities, resolveActiveContextType, AvatarConnectionStatus } from '@unisphere/genie-types';
|
|
4
4
|
import { z as z$1 } from 'zod/mini';
|
|
5
|
+
import { AlertIconEnum } from '@unisphere/notifications-core';
|
|
5
6
|
import { initializeLogsCollector, EselfSession, MediaTrackState, sendLogs } from '@kaltura-sdk/rtc-avatar';
|
|
6
7
|
import { produce } from 'immer';
|
|
7
8
|
import * as yaml from 'js-yaml';
|
|
8
|
-
import { AlertIconEnum, NotificationTypes } from '@unisphere/notifications-core';
|
|
9
9
|
import { ApplicationPageTypes, ApplicationEventTypes, AnalyticsBucket } from '@unisphere/runtime';
|
|
10
10
|
import { executeClientToolCall, sendClientToolResponse } from '@local/genie-shared-utils';
|
|
11
11
|
|
|
@@ -6226,6 +6226,9 @@ const agentRawTextBaseDeltaSchema = z$1.object({
|
|
|
6226
6226
|
role: z$1.optional(z$1.string()),
|
|
6227
6227
|
type: z$1.optional(z$1.string()),
|
|
6228
6228
|
content: z$1.optional(z$1.union([z$1.string(), z$1.record(z$1.string(), z$1.unknown())])),
|
|
6229
|
+
// present on the bootstrap 'think' segment - the user's turn text, echoed
|
|
6230
|
+
// there instead of a dedicated 'human' segment type.
|
|
6231
|
+
human: z$1.optional(z$1.string()),
|
|
6229
6232
|
segmentNumber: z$1.optional(z$1.number()),
|
|
6230
6233
|
et: z$1.optional(z$1.number()),
|
|
6231
6234
|
segmentStart: z$1.optional(z$1.boolean()),
|
|
@@ -6260,6 +6263,48 @@ const stvSpeechChunkSchema = z$1.object({
|
|
|
6260
6263
|
durationMs: z$1.number(),
|
|
6261
6264
|
speechId: z$1.string()
|
|
6262
6265
|
});
|
|
6266
|
+
const unsupportedClientSchema = z$1.object({
|
|
6267
|
+
code: z$1.optional(z$1.string())
|
|
6268
|
+
});
|
|
6269
|
+
|
|
6270
|
+
// Codes for notifications that are not tied to a connection disconnect reason.
|
|
6271
|
+
// Keep these string-stable: consumers of `externalActions.showNotification`
|
|
6272
|
+
// match on `code` to look up their own localized copy.
|
|
6273
|
+
var AvatarNotificationCode;
|
|
6274
|
+
(function (AvatarNotificationCode) {
|
|
6275
|
+
AvatarNotificationCode["ScreenShareError"] = "ScreenShareError";
|
|
6276
|
+
AvatarNotificationCode["AgentRecoveryExhausted"] = "AgentRecoveryExhausted";
|
|
6277
|
+
AvatarNotificationCode["ConversationTimeWarning"] = "ConversationTimeWarning";
|
|
6278
|
+
})(AvatarNotificationCode || (AvatarNotificationCode = {}));
|
|
6279
|
+
const disconnectReasonVariantMap = {
|
|
6280
|
+
[DisconnectReasons.ConnectionError]: AlertIconEnum.Error,
|
|
6281
|
+
[DisconnectReasons.ExceededTierLimits]: AlertIconEnum.Warning,
|
|
6282
|
+
[DisconnectReasons.NoAgentsAvailable]: AlertIconEnum.Warning,
|
|
6283
|
+
[DisconnectReasons.UserMediaInitializationFailed]: AlertIconEnum.Error,
|
|
6284
|
+
[DisconnectReasons.UserRequested]: AlertIconEnum.Success,
|
|
6285
|
+
[DisconnectReasons.WebsocketConnectionFailed]: AlertIconEnum.Error,
|
|
6286
|
+
[DisconnectReasons.WebsocketDisconnected]: AlertIconEnum.Warning,
|
|
6287
|
+
[DisconnectReasons.PeerRemoved]: AlertIconEnum.Warning,
|
|
6288
|
+
[DisconnectReasons.BadRequest]: AlertIconEnum.Error,
|
|
6289
|
+
[DisconnectReasons.ASRConnectionFailed]: AlertIconEnum.Warning,
|
|
6290
|
+
[DisconnectReasons.DestroyFailed]: AlertIconEnum.Error,
|
|
6291
|
+
[DisconnectReasons.ConversationEnded]: AlertIconEnum.Warning,
|
|
6292
|
+
[DisconnectReasons.UnknownReason]: AlertIconEnum.Error,
|
|
6293
|
+
[DisconnectReasons.DevicesPermissionDenied]: AlertIconEnum.Warning,
|
|
6294
|
+
[DisconnectReasons.AgentResponseTimeout]: AlertIconEnum.Warning,
|
|
6295
|
+
[DisconnectReasons.UnsupportedClient]: AlertIconEnum.Error,
|
|
6296
|
+
[DisconnectReasons.JoinRoomTimeout]: AlertIconEnum.Warning,
|
|
6297
|
+
[DisconnectReasons.ConnectionTimeout]: AlertIconEnum.Warning,
|
|
6298
|
+
[DisconnectReasons.PlayerConnectionFailed]: AlertIconEnum.Error
|
|
6299
|
+
};
|
|
6300
|
+
const defaultDisconnectVariant = AlertIconEnum.Error;
|
|
6301
|
+
function isMappedAvatarDisconnectReason(reason) {
|
|
6302
|
+
return !!disconnectReasonVariantMap[reason];
|
|
6303
|
+
}
|
|
6304
|
+
function getAvatarDisconnectNotificationVariant(reason) {
|
|
6305
|
+
var _a;
|
|
6306
|
+
return (_a = disconnectReasonVariantMap[reason]) !== null && _a !== void 0 ? _a : defaultDisconnectVariant;
|
|
6307
|
+
}
|
|
6263
6308
|
|
|
6264
6309
|
const AvatarConnectionMachineId = 'AvatarClient';
|
|
6265
6310
|
|
|
@@ -6983,13 +7028,13 @@ const agentRecoveryMonitorState = {
|
|
|
6983
7028
|
actions: 'resetAgentRecoveryRetryCount'
|
|
6984
7029
|
}, {
|
|
6985
7030
|
guard: 'hasMoreRecoveryRetries',
|
|
6986
|
-
actions: ['incrementAgentRecoveryRetryCount', 'sendReprocessTurn']
|
|
7031
|
+
actions: ['removeStaleChatEntriesForRetry', 'incrementAgentRecoveryRetryCount', 'sendReprocessTurn']
|
|
6987
7032
|
}, {
|
|
6988
7033
|
guard: 'justExceededRecoveryRetries',
|
|
6989
7034
|
actions: ['incrementAgentRecoveryRetryCount', 'showAgentRecoveryExhaustedNotification']
|
|
6990
7035
|
}],
|
|
6991
7036
|
[AvatarConnectionMachineEventTypes.RequestAgentTurnRecovery]: {
|
|
6992
|
-
actions: 'sendReprocessTurn'
|
|
7037
|
+
actions: ['removeStaleChatEntriesForRetry', 'sendReprocessTurn']
|
|
6993
7038
|
}
|
|
6994
7039
|
},
|
|
6995
7040
|
invoke: {
|
|
@@ -19259,97 +19304,6 @@ const setDisconnectData = assign$1(({
|
|
|
19259
19304
|
});
|
|
19260
19305
|
});
|
|
19261
19306
|
|
|
19262
|
-
const avatarConnectionNotificationsMap = {
|
|
19263
|
-
[DisconnectReasons.ConnectionError]: {
|
|
19264
|
-
text: 'There was a problem connecting to the server. Please try again later.',
|
|
19265
|
-
variant: AlertIconEnum.Error
|
|
19266
|
-
},
|
|
19267
|
-
[DisconnectReasons.ExceededTierLimits]: {
|
|
19268
|
-
text: 'You have exceeded your usage limits. Please upgrade your plan or try again later.',
|
|
19269
|
-
variant: AlertIconEnum.Warning
|
|
19270
|
-
},
|
|
19271
|
-
[DisconnectReasons.NoAgentsAvailable]: {
|
|
19272
|
-
text: 'No agents are currently available. Please try again later.',
|
|
19273
|
-
variant: AlertIconEnum.Warning
|
|
19274
|
-
},
|
|
19275
|
-
[DisconnectReasons.UserMediaInitializationFailed]: {
|
|
19276
|
-
text: 'Unable to access your microphone or camera. Please check your device settings.',
|
|
19277
|
-
variant: AlertIconEnum.Error
|
|
19278
|
-
},
|
|
19279
|
-
[DisconnectReasons.UserRequested]: {
|
|
19280
|
-
text: 'You have ended the session.',
|
|
19281
|
-
variant: AlertIconEnum.Success
|
|
19282
|
-
},
|
|
19283
|
-
[DisconnectReasons.WebsocketConnectionFailed]: {
|
|
19284
|
-
text: 'WebSocket connection failed. Please check your internet connection and try again.',
|
|
19285
|
-
variant: AlertIconEnum.Error
|
|
19286
|
-
},
|
|
19287
|
-
[DisconnectReasons.WebsocketDisconnected]: {
|
|
19288
|
-
text: 'The connection was lost. Please try reconnecting.',
|
|
19289
|
-
variant: AlertIconEnum.Warning
|
|
19290
|
-
},
|
|
19291
|
-
[DisconnectReasons.PeerRemoved]: {
|
|
19292
|
-
text: 'The session has ended as the peer has disconnected.',
|
|
19293
|
-
variant: AlertIconEnum.Warning
|
|
19294
|
-
},
|
|
19295
|
-
[DisconnectReasons.BadRequest]: {
|
|
19296
|
-
text: 'There was a bad request error. Please verify your settings and try again.',
|
|
19297
|
-
variant: AlertIconEnum.Error
|
|
19298
|
-
},
|
|
19299
|
-
[DisconnectReasons.ASRConnectionFailed]: {
|
|
19300
|
-
text: 'Voice Activity Detection connection failed. Some features may not work properly.',
|
|
19301
|
-
variant: AlertIconEnum.Warning
|
|
19302
|
-
},
|
|
19303
|
-
[DisconnectReasons.DestroyFailed]: {
|
|
19304
|
-
text: 'An error occurred while ending the session. Please try again later.',
|
|
19305
|
-
variant: AlertIconEnum.Error
|
|
19306
|
-
},
|
|
19307
|
-
[DisconnectReasons.ConversationEnded]: {
|
|
19308
|
-
text: 'The conversation has ended.',
|
|
19309
|
-
variant: AlertIconEnum.Warning
|
|
19310
|
-
},
|
|
19311
|
-
[DisconnectReasons.UnknownReason]: {
|
|
19312
|
-
text: 'An unknown error occurred. Please try again later or contact support.',
|
|
19313
|
-
variant: AlertIconEnum.Error
|
|
19314
|
-
},
|
|
19315
|
-
[DisconnectReasons.DevicesPermissionDenied]: {
|
|
19316
|
-
text: 'Microphone access was denied. Please allow microphone permissions and try again.',
|
|
19317
|
-
variant: AlertIconEnum.Warning
|
|
19318
|
-
},
|
|
19319
|
-
[DisconnectReasons.AgentResponseTimeout]: {
|
|
19320
|
-
text: 'The agent took too long to respond. Please try again later.',
|
|
19321
|
-
variant: AlertIconEnum.Warning
|
|
19322
|
-
},
|
|
19323
|
-
[DisconnectReasons.UnsupportedClient]: {
|
|
19324
|
-
text: 'The client you are using is not supported. Please contact support.',
|
|
19325
|
-
variant: AlertIconEnum.Error
|
|
19326
|
-
},
|
|
19327
|
-
[DisconnectReasons.JoinRoomTimeout]: {
|
|
19328
|
-
text: 'Joining the room timed out. Please check your connection and try again.',
|
|
19329
|
-
variant: AlertIconEnum.Warning
|
|
19330
|
-
},
|
|
19331
|
-
[DisconnectReasons.ConnectionTimeout]: {
|
|
19332
|
-
text: 'The connection timed out. Please check your internet connection and try again.',
|
|
19333
|
-
variant: AlertIconEnum.Warning
|
|
19334
|
-
},
|
|
19335
|
-
[DisconnectReasons.PlayerConnectionFailed]: {
|
|
19336
|
-
text: 'A playback error occurred while connecting to the avatar. Please try again.',
|
|
19337
|
-
variant: AlertIconEnum.Error
|
|
19338
|
-
}
|
|
19339
|
-
};
|
|
19340
|
-
const defaultDisconnectNotification = {
|
|
19341
|
-
text: 'An unknown error occurred. Please try again later or contact support.',
|
|
19342
|
-
variant: AlertIconEnum.Error
|
|
19343
|
-
};
|
|
19344
|
-
function isMappedAvatarDisconnectReason(reason) {
|
|
19345
|
-
return !!avatarConnectionNotificationsMap[reason];
|
|
19346
|
-
}
|
|
19347
|
-
function getAvatarConnectionNotification(reason) {
|
|
19348
|
-
return Object.assign({
|
|
19349
|
-
type: NotificationTypes.Alert
|
|
19350
|
-
}, avatarConnectionNotificationsMap[reason] || defaultDisconnectNotification);
|
|
19351
|
-
}
|
|
19352
|
-
|
|
19353
19307
|
const showDisconnectNotification = ({
|
|
19354
19308
|
context
|
|
19355
19309
|
}) => {
|
|
@@ -19377,7 +19331,10 @@ const showDisconnectNotification = ({
|
|
|
19377
19331
|
}
|
|
19378
19332
|
});
|
|
19379
19333
|
}
|
|
19380
|
-
const notification =
|
|
19334
|
+
const notification = {
|
|
19335
|
+
code: context.disconnectData.reason,
|
|
19336
|
+
variant: getAvatarDisconnectNotificationVariant(context.disconnectData.reason)
|
|
19337
|
+
};
|
|
19381
19338
|
try {
|
|
19382
19339
|
context.externalActions.showNotification(notification);
|
|
19383
19340
|
} catch (error) {
|
|
@@ -19428,6 +19385,7 @@ const onConversationEnded = fromCallback(({
|
|
|
19428
19385
|
});
|
|
19429
19386
|
});
|
|
19430
19387
|
|
|
19388
|
+
const USAGE_LIMIT_EXCEEDED_CODE = 'USAGE_LIMIT_EXCEEDED';
|
|
19431
19389
|
const source$g = createTinyPubsubEventsSource(websocketMessage, {
|
|
19432
19390
|
disconnect: {
|
|
19433
19391
|
schema: null
|
|
@@ -19451,7 +19409,7 @@ const source$g = createTinyPubsubEventsSource(websocketMessage, {
|
|
|
19451
19409
|
schema: null
|
|
19452
19410
|
},
|
|
19453
19411
|
unsupportedClient: {
|
|
19454
|
-
schema:
|
|
19412
|
+
schema: unsupportedClientSchema
|
|
19455
19413
|
}
|
|
19456
19414
|
}, {
|
|
19457
19415
|
queueWhilePending: true
|
|
@@ -19480,7 +19438,10 @@ const onError = fromCallback(({
|
|
|
19480
19438
|
throwToNoAgent: createHandler(DisconnectReasons.NoAgentsAvailable),
|
|
19481
19439
|
throwToExceededTier: createHandler(DisconnectReasons.ExceededTierLimits),
|
|
19482
19440
|
throwToBadRequest: createHandler(DisconnectReasons.BadRequest),
|
|
19483
|
-
unsupportedClient:
|
|
19441
|
+
unsupportedClient: event => {
|
|
19442
|
+
const reason = (event === null || event === void 0 ? void 0 : event.code) === USAGE_LIMIT_EXCEEDED_CODE ? DisconnectReasons.ExceededTierLimits : DisconnectReasons.UnsupportedClient;
|
|
19443
|
+
createHandler(reason)(event);
|
|
19444
|
+
}
|
|
19484
19445
|
}
|
|
19485
19446
|
});
|
|
19486
19447
|
});
|
|
@@ -20407,9 +20368,11 @@ const showTimeWarningNotification = ({
|
|
|
20407
20368
|
}) => {
|
|
20408
20369
|
assertEvent(event, AvatarConnectionMachineEventTypes.ConversationTimeWarningReceived);
|
|
20409
20370
|
const notification = {
|
|
20410
|
-
|
|
20411
|
-
text: `Your conversation will end in ${event.payload.remainingTime} seconds.`,
|
|
20371
|
+
code: AvatarNotificationCode.ConversationTimeWarning,
|
|
20412
20372
|
variant: AlertIconEnum.Warning,
|
|
20373
|
+
params: {
|
|
20374
|
+
remainingTime: event.payload.remainingTime
|
|
20375
|
+
},
|
|
20413
20376
|
options: {
|
|
20414
20377
|
duration: event.payload.remainingTime,
|
|
20415
20378
|
showClose: true
|
|
@@ -20660,9 +20623,51 @@ const appendAvatarChatEntry = assign$1(({
|
|
|
20660
20623
|
context,
|
|
20661
20624
|
event
|
|
20662
20625
|
}) => {
|
|
20626
|
+
var _a, _b;
|
|
20663
20627
|
assertEvent(event, AvatarConnectionMachineEventTypes.AvatarChatEntryReceived);
|
|
20628
|
+
// The avatar's FINAL segment responding means the human phase for this
|
|
20629
|
+
// turn is done - a pendingTextInput still anchored to this turnId (see
|
|
20630
|
+
// appendUserChatEntry) has nothing left to merge into; clear it so the
|
|
20631
|
+
// next askViaText starts a fresh entry instead of merging into a turn
|
|
20632
|
+
// that's already been answered. A non-final segment (avatar-filler,
|
|
20633
|
+
// spoken ahead of the main answer during a tool call) must NOT clear it
|
|
20634
|
+
// - the real turn is still open server-side and more sends can still
|
|
20635
|
+
// legitimately merge into it; closing early here caused a real bug: a
|
|
20636
|
+
// later send got a fresh local entry, but the server's own confirmation
|
|
20637
|
+
// for it still carried the still-open turn's turnId, landing two chat
|
|
20638
|
+
// entries on the same id - every later merge (by id) then grew both.
|
|
20639
|
+
//
|
|
20640
|
+
// Compares against `turnId`, NOT `id` - a real capture showed the
|
|
20641
|
+
// server can reuse the same turnId across multiple, already-answered
|
|
20642
|
+
// exchanges (e.g. repeated ambiguous input like "="), so `id` alone is
|
|
20643
|
+
// not a safe proxy for "this is the turn that's still open" once it's
|
|
20644
|
+
// also being used as a collision-safe chatLog entry id (see
|
|
20645
|
+
// on-chat-log.ts / appendUserChatEntry).
|
|
20646
|
+
const isFinalSegment = (_a = event.isFinalSegment) !== null && _a !== void 0 ? _a : true;
|
|
20647
|
+
const pendingTurnId = (_b = context.pendingTextInput) === null || _b === void 0 ? void 0 : _b.turnId;
|
|
20648
|
+
const pendingTextInput = isFinalSegment && pendingTurnId != null && pendingTurnId === event.entry.turnId ? null : context.pendingTextInput;
|
|
20649
|
+
return {
|
|
20650
|
+
chatLog: [...context.chatLog, event.entry],
|
|
20651
|
+
pendingTextInput
|
|
20652
|
+
};
|
|
20653
|
+
});
|
|
20654
|
+
|
|
20655
|
+
// A retry re-runs the same turn (same userInput -> same turnId) under a new
|
|
20656
|
+
// speechId/messageId, re-emitting both the user's `human` segment and the
|
|
20657
|
+
// avatar answer — both go stale, not just the avatar one.
|
|
20658
|
+
//
|
|
20659
|
+
// Matched on messageId, not turnId: the server can reuse the same turnId
|
|
20660
|
+
// across distinct, already-answered exchanges (e.g. repeated "="), so
|
|
20661
|
+
// turnId would wipe an unrelated earlier exchange too. messageId is
|
|
20662
|
+
// constant per exchange and never reused (confirmed against
|
|
20663
|
+
// kaltura_agent_executer.py), so it removes exactly the retried exchange.
|
|
20664
|
+
const removeStaleChatEntriesForRetry = assign$1(({
|
|
20665
|
+
context,
|
|
20666
|
+
event
|
|
20667
|
+
}) => {
|
|
20668
|
+
assertEvent(event, [AvatarConnectionMachineEventTypes.RequestAgentTurnRecovery, AvatarConnectionMachineEventTypes.AgentRecoveryTurnCompleted]);
|
|
20664
20669
|
return {
|
|
20665
|
-
chatLog:
|
|
20670
|
+
chatLog: context.chatLog.filter(entry => entry.messageId !== event.messageId)
|
|
20666
20671
|
};
|
|
20667
20672
|
});
|
|
20668
20673
|
|
|
@@ -20670,18 +20675,39 @@ const appendUserChatEntry = assign$1(({
|
|
|
20670
20675
|
context,
|
|
20671
20676
|
event
|
|
20672
20677
|
}) => {
|
|
20678
|
+
var _a;
|
|
20673
20679
|
assertEvent(event, AvatarConnectionMachineEventTypes.UserVoiceTurnReceived);
|
|
20674
|
-
const
|
|
20675
|
-
if (
|
|
20676
|
-
// Already added optimistically via text input — consume the pending entry and skip
|
|
20677
|
-
const pendingTextInputs = [...context.pendingTextInputs];
|
|
20678
|
-
pendingTextInputs.splice(idx, 1);
|
|
20680
|
+
const pending = context.pendingTextInput;
|
|
20681
|
+
if (!pending) {
|
|
20679
20682
|
return {
|
|
20680
|
-
|
|
20683
|
+
chatLog: [...context.chatLog, event.entry]
|
|
20681
20684
|
};
|
|
20682
20685
|
}
|
|
20686
|
+
// Only one send can be in flight at a time - appendUserTextEntry merges
|
|
20687
|
+
// any further sends into it instead of starting a second one - so this
|
|
20688
|
+
// turn is unambiguously that pending entry's confirmation. The server is
|
|
20689
|
+
// authoritative for what actually gets saved as message history, so its
|
|
20690
|
+
// entry always wins here, whether or not our locally-accumulated text
|
|
20691
|
+
// happens to match it exactly.
|
|
20692
|
+
const chatLog = pending.id ? context.chatLog.map(entry => entry.id === pending.id ? event.entry : entry) : [...context.chatLog, event.entry];
|
|
20693
|
+
// Stay anchored to this turn via its now-confirmed id instead of
|
|
20694
|
+
// clearing to null - the server re-sends this same "human" echo once per
|
|
20695
|
+
// rapid send under one still-open turnId, each carrying the full
|
|
20696
|
+
// cumulative text (see on-chat-log.ts), so a further send before the
|
|
20697
|
+
// avatar responds must keep merging into this same entry. What actually
|
|
20698
|
+
// closes this out is the avatar responding - see appendAvatarChatEntry.
|
|
20699
|
+
// `turnId` is tracked alongside `id` (not derived from it) - a real
|
|
20700
|
+
// capture showed the server can reuse the same turnId across multiple,
|
|
20701
|
+
// already-answered exchanges, so `event.entry.id` must stay collision-safe
|
|
20702
|
+
// (see on-chat-log.ts) while `turnId` is what appendAvatarChatEntry
|
|
20703
|
+
// actually compares against to decide its turn just closed.
|
|
20683
20704
|
return {
|
|
20684
|
-
chatLog
|
|
20705
|
+
chatLog,
|
|
20706
|
+
pendingTextInput: {
|
|
20707
|
+
id: event.entry.id,
|
|
20708
|
+
turnId: (_a = event.entry.turnId) !== null && _a !== void 0 ? _a : null,
|
|
20709
|
+
text: event.entry.text
|
|
20710
|
+
}
|
|
20685
20711
|
};
|
|
20686
20712
|
});
|
|
20687
20713
|
|
|
@@ -20694,12 +20720,70 @@ const appendUserTextEntry = assign$1(({
|
|
|
20694
20720
|
text,
|
|
20695
20721
|
internal
|
|
20696
20722
|
} = event;
|
|
20723
|
+
if (context.pendingTextInput) {
|
|
20724
|
+
// A send while the previous turn is still open mirrors the server's
|
|
20725
|
+
// own behavior: it merges rapid back-to-back input into one turn, so
|
|
20726
|
+
// append into the same pending message instead of starting a new one.
|
|
20727
|
+
// The server re-confirms this same turn once per send (see
|
|
20728
|
+
// appendUserChatEntry) rather than clearing pendingTextInput, so
|
|
20729
|
+
// further sends keep landing here until the avatar actually answers
|
|
20730
|
+
// (see appendAvatarChatEntry, which is what closes it out) or the turn
|
|
20731
|
+
// is interrupted.
|
|
20732
|
+
const {
|
|
20733
|
+
id,
|
|
20734
|
+
turnId,
|
|
20735
|
+
text: pendingText
|
|
20736
|
+
} = context.pendingTextInput;
|
|
20737
|
+
const mergedText = `${pendingText} ${text}`;
|
|
20738
|
+
if (id) {
|
|
20739
|
+
return {
|
|
20740
|
+
pendingTextInput: {
|
|
20741
|
+
id,
|
|
20742
|
+
turnId,
|
|
20743
|
+
text: mergedText
|
|
20744
|
+
},
|
|
20745
|
+
chatLog: context.chatLog.map(entry => entry.id === id ? Object.assign(Object.assign({}, entry), {
|
|
20746
|
+
text: mergedText
|
|
20747
|
+
}) : entry)
|
|
20748
|
+
};
|
|
20749
|
+
}
|
|
20750
|
+
if (internal) {
|
|
20751
|
+
// Still an internal-only accumulation - stays out of the chat log.
|
|
20752
|
+
return {
|
|
20753
|
+
pendingTextInput: {
|
|
20754
|
+
id: null,
|
|
20755
|
+
turnId: null,
|
|
20756
|
+
text: mergedText
|
|
20757
|
+
}
|
|
20758
|
+
};
|
|
20759
|
+
}
|
|
20760
|
+
// The pending message so far was internal (invisible) but this
|
|
20761
|
+
// follow-up isn't - it becomes visible now, under a fresh entry.
|
|
20762
|
+
const _entry = {
|
|
20763
|
+
id: crypto.randomUUID(),
|
|
20764
|
+
role: 'user',
|
|
20765
|
+
text: mergedText
|
|
20766
|
+
};
|
|
20767
|
+
return {
|
|
20768
|
+
chatLog: [...context.chatLog, _entry],
|
|
20769
|
+
pendingTextInput: {
|
|
20770
|
+
id: _entry.id,
|
|
20771
|
+
turnId: null,
|
|
20772
|
+
text: mergedText
|
|
20773
|
+
}
|
|
20774
|
+
};
|
|
20775
|
+
}
|
|
20697
20776
|
// raiseAskViaTextKickoff marks its auto-sent ask `internal` — it's a bootstrap prompt,
|
|
20698
|
-
// not something the user said, so keep it out of the visible chat log. Still
|
|
20699
|
-
// in
|
|
20777
|
+
// not something the user said, so keep it out of the visible chat log. Still tracked
|
|
20778
|
+
// in pendingTextInput so the server's turn echo is deduped the same way. No chatLog
|
|
20779
|
+
// entry backs it, so `id: null`.
|
|
20700
20780
|
if (internal) {
|
|
20701
20781
|
return {
|
|
20702
|
-
|
|
20782
|
+
pendingTextInput: {
|
|
20783
|
+
id: null,
|
|
20784
|
+
turnId: null,
|
|
20785
|
+
text
|
|
20786
|
+
}
|
|
20703
20787
|
};
|
|
20704
20788
|
}
|
|
20705
20789
|
const entry = {
|
|
@@ -20709,7 +20793,11 @@ const appendUserTextEntry = assign$1(({
|
|
|
20709
20793
|
};
|
|
20710
20794
|
return {
|
|
20711
20795
|
chatLog: [...context.chatLog, entry],
|
|
20712
|
-
|
|
20796
|
+
pendingTextInput: {
|
|
20797
|
+
id: entry.id,
|
|
20798
|
+
turnId: null,
|
|
20799
|
+
text
|
|
20800
|
+
}
|
|
20713
20801
|
};
|
|
20714
20802
|
});
|
|
20715
20803
|
|
|
@@ -20760,135 +20848,64 @@ const source$a = createTinyPubsubEventsSource(websocketMessage, {
|
|
|
20760
20848
|
agent_raw_text: {
|
|
20761
20849
|
schema: agentRawTextSchema
|
|
20762
20850
|
},
|
|
20763
|
-
agentTurnToTalk: {
|
|
20764
|
-
schema: agentTurnToTalkSchema
|
|
20765
|
-
},
|
|
20766
20851
|
generatingSpeech: {
|
|
20767
20852
|
schema: generatingSpeechSchema
|
|
20853
|
+
},
|
|
20854
|
+
agentTurnToTalk: {
|
|
20855
|
+
schema: agentTurnToTalkSchema
|
|
20768
20856
|
}
|
|
20769
20857
|
}, {
|
|
20770
20858
|
queueWhilePending: true
|
|
20771
20859
|
});
|
|
20772
|
-
//
|
|
20773
|
-
// =============
|
|
20774
|
-
// Builds the alternating user/avatar chat log from two independent, unordered
|
|
20775
|
-
// websocket event streams that share no common id:
|
|
20776
|
-
// - agentTurnToTalk: fires once per USER turn, carries the ASR transcript
|
|
20777
|
-
// (userTranscription). Empty userTranscription means a proactive/opening
|
|
20778
|
-
// turn (avatar speaks first, no preceding user input).
|
|
20779
|
-
// - agent_raw_text: streams the AVATAR's reply for a turn as chunked deltas
|
|
20780
|
-
// keyed by speechId, terminated by a delta with segmentEnd=true.
|
|
20781
|
-
// delta.type === 'avatar' or 'avatar-filler' chunks are chat text (filler
|
|
20782
|
-
// is spoken content too, e.g. "Let me check that for you..." streamed
|
|
20783
|
-
// ahead of the main answer while a tool call runs); 'think'/'tool'/'share'
|
|
20784
|
-
// chunks are filtered out here.
|
|
20860
|
+
// Builds alternating user/avatar chat log from agent_raw_text, keyed by turnId.
|
|
20785
20861
|
//
|
|
20786
|
-
//
|
|
20787
|
-
//
|
|
20788
|
-
//
|
|
20789
|
-
//
|
|
20790
|
-
//
|
|
20791
|
-
//
|
|
20792
|
-
//
|
|
20793
|
-
//
|
|
20794
|
-
//
|
|
20795
|
-
//
|
|
20796
|
-
//
|
|
20797
|
-
//
|
|
20798
|
-
//
|
|
20799
|
-
// avatar segmentEnd — either multiple segments under one speechId (e.g. a
|
|
20800
|
-
// tool call splits the reply into a chunk before and a chunk after the
|
|
20801
|
-
// tool result), or multiple speechIds sharing one turnId (a reprocessTurn
|
|
20802
|
-
// retry regenerates the whole answer under a new speechId but keeps the
|
|
20803
|
-
// same turnId). Only the FIRST resolved segment/attempt for a turnId
|
|
20804
|
-
// goes through the queue/counter above; later ones are known to already
|
|
20805
|
-
// belong to a claimed turn, so they're emitted immediately without
|
|
20806
|
-
// consuming another slot.
|
|
20862
|
+
// delta.type:
|
|
20863
|
+
// - 'think' with `human` — user's turn text, one-shot on the bootstrap
|
|
20864
|
+
// "preparing to answer..." segment (precedes the turn's avatar segments).
|
|
20865
|
+
// No `human` field = real thinking tokens, ignored (like 'tool'/'share').
|
|
20866
|
+
// Deduped per turnId:speechId. Entry id is that composite, not bare
|
|
20867
|
+
// turnId — server can reuse one turnId across several already-answered
|
|
20868
|
+
// exchanges (e.g. repeated "="), so speechId keeps entries from colliding.
|
|
20869
|
+
// - 'avatar' / 'avatar-filler' — chat text, chunked by speechId, closed by
|
|
20870
|
+
// segmentEnd=true. Filler is spoken content too (e.g. tool-call "let me
|
|
20871
|
+
// check..." lines).
|
|
20872
|
+
// - 'user-interruption' — turn aborted server-side (no literal 'abort'
|
|
20873
|
+
// type exists). Flushes whatever partial avatar text accumulated and
|
|
20874
|
+
// evicts that turnId:speechId buffer only.
|
|
20807
20875
|
//
|
|
20808
|
-
//
|
|
20809
|
-
//
|
|
20810
|
-
// which just push onto context.chatLog in the order events arrive here).
|
|
20876
|
+
// A retry reuses the same turnId under a new speechId; each speechId's
|
|
20877
|
+
// segmentEnd/user-interruption is tracked/flushed independently.
|
|
20811
20878
|
//
|
|
20812
|
-
//
|
|
20813
|
-
//
|
|
20814
|
-
//
|
|
20815
|
-
//
|
|
20879
|
+
// Every entry carries `messageId` (parsed.messageId) — constant for every
|
|
20880
|
+
// segment of one exchange, unique across exchanges even when turnId repeats
|
|
20881
|
+
// (confirmed against kaltura_agent_executer.py). remove-stale-chat-entries-
|
|
20882
|
+
// for-retry.ts matches on it to drop exactly one exchange on retry.
|
|
20816
20883
|
//
|
|
20817
|
-
//
|
|
20818
|
-
//
|
|
20819
|
-
//
|
|
20884
|
+
// Opening phrase (resumed thread, avatar speaks first): never sent as a
|
|
20885
|
+
// normal turnId-keyed segment. Server sends it via `init_response` (too
|
|
20886
|
+
// early — fires before this actor's setHandler(), so it's dropped by the
|
|
20887
|
+
// pubsub queue reset) and a mirrored `generatingSpeech` event, which we use
|
|
20888
|
+
// as the text source instead. `agentTurnToTalk` (userTranscription === '')
|
|
20889
|
+
// is the flush trigger — nothing else marks "greeting done, no real user
|
|
20890
|
+
// turn happened first". BLANK_TEXT_MARKER is the server's "no opening
|
|
20891
|
+
// phrase configured" sentinel, skipped.
|
|
20820
20892
|
const onChatLog = fromCallback(({
|
|
20821
20893
|
input,
|
|
20822
20894
|
sendBack
|
|
20823
20895
|
}) => {
|
|
20824
|
-
// Accumulate
|
|
20896
|
+
// Accumulate avatar text by `${turnId}:${speechId}` — each speechId
|
|
20897
|
+
// closes independently (e.g. a filler segment vs the main answer).
|
|
20825
20898
|
const pending = new Map();
|
|
20826
|
-
|
|
20827
|
-
// When a new speechId is seen, the previous turn's incomplete text is abandoned (interrupted).
|
|
20828
|
-
let currentSpeechId = null;
|
|
20829
|
-
// Avatar answers whose segmentEnd fired before their matching agentTurnToTalk arrived.
|
|
20830
|
-
// Completed avatar answers waiting for their paired agentTurnToTalk.
|
|
20831
|
-
const awaitingUserTurnQueue = [];
|
|
20832
|
-
// agentTurnToTalk events (with transcription) whose matching avatar answer hasn't
|
|
20833
|
-
// completed yet. Talk-vs-avatar-stream ordering isn't fixed — the ASR transcript can
|
|
20834
|
-
// beat or lag the avatar's own text stream on a per-turn basis (races harder when the
|
|
20835
|
-
// answer carries tool content) — so pairing is done via this queue+counter rather than
|
|
20836
|
-
// a single slot, which a later turn's speechId change would otherwise clobber.
|
|
20837
|
-
//
|
|
20838
|
-
// Known risk (backend, not fixable here — agentTurnToTalk carries no turnId/speechId
|
|
20839
|
-
// to correlate by id, only arrival order): both sides can drift out of 1:1 and desync
|
|
20840
|
-
// pairing for a later, unrelated turn instead of the intended one.
|
|
20841
|
-
// - Queue can strand an entry forever if its own agentTurnToTalk never fires — e.g.
|
|
20842
|
-
// conversation-manager's ttv.sayTurn throwing on the first chunk, or the
|
|
20843
|
-
// first-sentence punctuation gate in SpeechBuffer.append swallowing a short
|
|
20844
|
-
// punctuation-less reply. Raw text still gets forwarded; the talk pipeline doesn't run.
|
|
20845
|
-
// - Counter can stay inflated forever if agentTurnToTalk fires but its avatar segment
|
|
20846
|
-
// never reaches segmentEnd — e.g. onUserInterrupted not aborting AgentAdapter's loop,
|
|
20847
|
-
// dropping buffered avatar text without ever calling finish()/completeStream().
|
|
20848
|
-
// Real fix is backend: always run finish()/completeStream() on error/interrupt, and
|
|
20849
|
-
// populate turnId on agentTurnToTalk (schema already has the field, unused) so pairing
|
|
20850
|
-
// can be done by id instead of arrival order.
|
|
20851
|
-
let pendingUserTurnCount = 0;
|
|
20852
|
-
// turnIds whose first segment already claimed a pairing slot (queued or emitted).
|
|
20853
|
-
// Keyed by turnId, NOT speechId, because a single logical turn can produce more than
|
|
20854
|
-
// one speechId:
|
|
20855
|
-
// - Genie can stream a reply as multiple avatar segments under one speechId (e.g. a
|
|
20856
|
-
// tool call splits the answer into a segment before and a segment after the tool
|
|
20857
|
-
// result) — those segments share both speechId and turnId.
|
|
20858
|
-
// - A reprocessTurn retry (sendReprocessTurn, triggered by agentRecoveryMonitor or a
|
|
20859
|
-
// manual "retry" action) makes the backend regenerate the FULL answer under a NEW
|
|
20860
|
-
// speechId while keeping the SAME turnId — confirmed in logs: two distinct speechIds
|
|
20861
|
-
// sharing turnId "c83e031d-..." after a reprocessTurn. Guarding on speechId alone
|
|
20862
|
-
// would treat the retry's answer as an unrelated new turn.
|
|
20863
|
-
// Either way, the extra segment/retry belongs to a turn that's already paired, so it
|
|
20864
|
-
// must bypass the queue/counter entirely; otherwise it would consume the next,
|
|
20865
|
-
// unrelated user turn's pairing slot and desync every pairing for the rest of the
|
|
20866
|
-
// conversation. Intentionally never cleared: a later segment/retry for the same turnId
|
|
20867
|
-
// can arrive arbitrarily long after the first (slow tool call, slow retry, other turns
|
|
20868
|
-
// in between), so "already claimed" must be remembered for the rest of the session,
|
|
20869
|
-
// same lifetime as awaitingUserTurnQueue/pendingUserTurnCount above. Actor-scoped, dies
|
|
20870
|
-
// with it.
|
|
20871
|
-
const resolvedTurnIds = new Set();
|
|
20872
|
-
// Opening phrase accumulation: for resumed-thread sessions where init_response is not sent,
|
|
20873
|
-
// generatingSpeech chunks arrive before the first user turn — accumulate them keyed by speechId
|
|
20874
|
-
// and flush on the first proactive agentTurnToTalk (empty userTranscription).
|
|
20875
|
-
let openingPhraseSpeechId = null;
|
|
20876
|
-
const openingPhraseChunks = new Map();
|
|
20877
|
-
let openingPhraseEmitted = false;
|
|
20899
|
+
const seenHumanTurnKeys = new Set();
|
|
20878
20900
|
let avatarEntrySequence = 0;
|
|
20879
20901
|
const createAvatarEntryId = (messageId, speechId) => {
|
|
20880
20902
|
avatarEntrySequence += 1;
|
|
20881
20903
|
return `${messageId}:${speechId}:${avatarEntrySequence}`;
|
|
20882
20904
|
};
|
|
20883
|
-
|
|
20884
|
-
|
|
20885
|
-
|
|
20886
|
-
|
|
20887
|
-
type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
|
|
20888
|
-
entry
|
|
20889
|
-
});
|
|
20890
|
-
}
|
|
20891
|
-
};
|
|
20905
|
+
// Opening phrase accumulation — see top-of-file comment.
|
|
20906
|
+
let openingPhraseSpeechId = null;
|
|
20907
|
+
let openingPhraseText = '';
|
|
20908
|
+
let openingPhraseSettled = false;
|
|
20892
20909
|
return createTinyPubsubEventsHandler({
|
|
20893
20910
|
source: source$a,
|
|
20894
20911
|
logger: input.logger,
|
|
@@ -20900,115 +20917,89 @@ const onChatLog = fromCallback(({
|
|
|
20900
20917
|
const result = agentRawTextDeltaSchema.safeParse(delta);
|
|
20901
20918
|
if (!result.success) return;
|
|
20902
20919
|
const parsed = result.data;
|
|
20903
|
-
|
|
20904
|
-
|
|
20905
|
-
|
|
20906
|
-
|
|
20907
|
-
if (parsed.type !== 'avatar' && parsed.type !== 'avatar-filler') return;
|
|
20908
|
-
const speechId = payload.speechId;
|
|
20909
|
-
// New speech started — abandon prior speech's incomplete accumulation.
|
|
20910
|
-
// Does NOT touch awaitingUserTurnQueue/pendingUserTurnCount: those are completed,
|
|
20911
|
-
// already-paired-or-waiting turns and must survive across speechId changes.
|
|
20912
|
-
if (speechId !== currentSpeechId) {
|
|
20913
|
-
pending.delete(currentSpeechId !== null && currentSpeechId !== void 0 ? currentSpeechId : '');
|
|
20914
|
-
currentSpeechId = speechId;
|
|
20915
|
-
}
|
|
20916
|
-
if (typeof parsed.content === 'string' && parsed.content) {
|
|
20917
|
-
pending.set(speechId, ((_a = pending.get(speechId)) !== null && _a !== void 0 ? _a : '') + parsed.content);
|
|
20918
|
-
}
|
|
20919
|
-
if (parsed.segmentEnd) {
|
|
20920
|
-
const text = (_b = pending.get(speechId)) !== null && _b !== void 0 ? _b : '';
|
|
20921
|
-
pending.delete(speechId);
|
|
20922
|
-
if (!text) return;
|
|
20923
|
-
const entry = {
|
|
20924
|
-
id: createAvatarEntryId(parsed.messageId, speechId),
|
|
20925
|
-
role: 'avatar',
|
|
20926
|
-
text,
|
|
20927
|
-
turnId: speechId
|
|
20928
|
-
};
|
|
20929
|
-
if (resolvedTurnIds.has(payload.turnId)) {
|
|
20930
|
-
// Not this turn's first segment/attempt — its pairing slot is already claimed
|
|
20931
|
-
// (multi-segment reply, or a reprocessTurn retry answering the same turnId).
|
|
20920
|
+
const key = `${payload.turnId}:${payload.speechId}`;
|
|
20921
|
+
if (parsed.type === 'think') {
|
|
20922
|
+
if (parsed.human && !seenHumanTurnKeys.has(key)) {
|
|
20923
|
+
seenHumanTurnKeys.add(key);
|
|
20932
20924
|
sendBack({
|
|
20933
|
-
type: AvatarConnectionMachineEventTypes.
|
|
20934
|
-
entry
|
|
20925
|
+
type: AvatarConnectionMachineEventTypes.UserVoiceTurnReceived,
|
|
20926
|
+
entry: {
|
|
20927
|
+
id: key,
|
|
20928
|
+
role: 'user',
|
|
20929
|
+
text: parsed.human,
|
|
20930
|
+
turnId: payload.turnId,
|
|
20931
|
+
messageId: parsed.messageId
|
|
20932
|
+
}
|
|
20935
20933
|
});
|
|
20936
|
-
return;
|
|
20937
20934
|
}
|
|
20938
|
-
|
|
20939
|
-
|
|
20940
|
-
|
|
20941
|
-
|
|
20935
|
+
return;
|
|
20936
|
+
}
|
|
20937
|
+
if (parsed.type === 'user-interruption') {
|
|
20938
|
+
const text = pending.get(key);
|
|
20939
|
+
pending.delete(key);
|
|
20940
|
+
if (text) {
|
|
20942
20941
|
sendBack({
|
|
20943
20942
|
type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
|
|
20944
|
-
entry
|
|
20943
|
+
entry: {
|
|
20944
|
+
id: createAvatarEntryId(parsed.messageId, payload.speechId),
|
|
20945
|
+
role: 'avatar',
|
|
20946
|
+
text,
|
|
20947
|
+
turnId: payload.turnId,
|
|
20948
|
+
messageId: parsed.messageId
|
|
20949
|
+
}
|
|
20945
20950
|
});
|
|
20946
|
-
} else {
|
|
20947
|
-
// agentTurnToTalk hasn't arrived yet — queue until it does
|
|
20948
|
-
awaitingUserTurnQueue.push(entry);
|
|
20949
20951
|
}
|
|
20952
|
+
return;
|
|
20953
|
+
}
|
|
20954
|
+
if (parsed.type !== 'avatar' && parsed.type !== 'avatar-filler') return;
|
|
20955
|
+
if (typeof parsed.content === 'string' && parsed.content) {
|
|
20956
|
+
pending.set(key, ((_a = pending.get(key)) !== null && _a !== void 0 ? _a : '') + parsed.content);
|
|
20957
|
+
}
|
|
20958
|
+
if (parsed.segmentEnd) {
|
|
20959
|
+
const text = (_b = pending.get(key)) !== null && _b !== void 0 ? _b : '';
|
|
20960
|
+
pending.delete(key);
|
|
20961
|
+
if (!text) return;
|
|
20962
|
+
sendBack({
|
|
20963
|
+
type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
|
|
20964
|
+
entry: {
|
|
20965
|
+
id: createAvatarEntryId(parsed.messageId, payload.speechId),
|
|
20966
|
+
role: 'avatar',
|
|
20967
|
+
text,
|
|
20968
|
+
turnId: payload.turnId,
|
|
20969
|
+
messageId: parsed.messageId
|
|
20970
|
+
},
|
|
20971
|
+
// Filler doesn't close an open pendingTextInput (see append-avatar-chat-entry.ts).
|
|
20972
|
+
isFinalSegment: parsed.type !== 'avatar-filler'
|
|
20973
|
+
});
|
|
20950
20974
|
}
|
|
20951
20975
|
} catch (_c) {
|
|
20952
20976
|
// invalid JSON delta — ignore
|
|
20953
20977
|
}
|
|
20954
20978
|
},
|
|
20955
20979
|
generatingSpeech: payload => {
|
|
20956
|
-
|
|
20957
|
-
|
|
20958
|
-
// Server sends this literal sentinel instead of real speech when no opening phrase
|
|
20959
|
-
// is configured — must not leak into the chat log as visible avatar text.
|
|
20980
|
+
if (openingPhraseSettled) return;
|
|
20981
|
+
// No opening phrase configured — must not leak into the chat log.
|
|
20960
20982
|
if (payload.text === BLANK_TEXT_MARKER) return;
|
|
20961
|
-
if (openingPhraseSpeechId !== null && openingPhraseSpeechId !== payload.speechId)
|
|
20983
|
+
if (openingPhraseSpeechId !== null && openingPhraseSpeechId !== payload.speechId) {
|
|
20984
|
+
return;
|
|
20985
|
+
}
|
|
20962
20986
|
openingPhraseSpeechId = payload.speechId;
|
|
20963
|
-
|
|
20987
|
+
openingPhraseText += payload.text;
|
|
20964
20988
|
},
|
|
20965
20989
|
agentTurnToTalk: payload => {
|
|
20966
|
-
|
|
20967
|
-
|
|
20968
|
-
|
|
20969
|
-
|
|
20970
|
-
} = payload;
|
|
20971
|
-
if (!userTranscription) {
|
|
20972
|
-
// Proactive turn — flush opening phrase built from generatingSpeech chunks if present
|
|
20973
|
-
if (!openingPhraseEmitted && openingPhraseSpeechId) {
|
|
20974
|
-
const text = (_a = openingPhraseChunks.get(openingPhraseSpeechId)) !== null && _a !== void 0 ? _a : '';
|
|
20975
|
-
openingPhraseChunks.clear();
|
|
20976
|
-
openingPhraseEmitted = true;
|
|
20977
|
-
if (text) {
|
|
20978
|
-
sendBack({
|
|
20979
|
-
type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
|
|
20980
|
-
entry: {
|
|
20981
|
-
id: openingPhraseSpeechId,
|
|
20982
|
-
role: 'avatar',
|
|
20983
|
-
text,
|
|
20984
|
-
turnId: 'init_response'
|
|
20985
|
-
}
|
|
20986
|
-
});
|
|
20987
|
-
}
|
|
20988
|
-
}
|
|
20989
|
-
// Also emit any queued avatar entry left over from the agent_raw_text path
|
|
20990
|
-
emitQueuedAvatarEntry();
|
|
20991
|
-
return;
|
|
20992
|
-
}
|
|
20993
|
-
openingPhraseEmitted = true;
|
|
20994
|
-
openingPhraseChunks.clear();
|
|
20995
|
-
// Emit user first, then pair with its avatar answer — whichever side finished
|
|
20996
|
-
// streaming first (queue has it) or hasn't yet (bump the counter for agent_raw_text
|
|
20997
|
-
// to consume once its segmentEnd arrives)
|
|
20990
|
+
// Only the first occurrence decides the opening — later turns also fire this.
|
|
20991
|
+
if (openingPhraseSettled) return;
|
|
20992
|
+
openingPhraseSettled = true;
|
|
20993
|
+
if (payload.userTranscription || !openingPhraseText) return;
|
|
20998
20994
|
sendBack({
|
|
20999
|
-
type: AvatarConnectionMachineEventTypes.
|
|
20995
|
+
type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
|
|
21000
20996
|
entry: {
|
|
21001
|
-
id:
|
|
21002
|
-
role: '
|
|
21003
|
-
text:
|
|
21004
|
-
turnId
|
|
20997
|
+
id: openingPhraseSpeechId !== null && openingPhraseSpeechId !== void 0 ? openingPhraseSpeechId : 'init_response',
|
|
20998
|
+
role: 'avatar',
|
|
20999
|
+
text: openingPhraseText,
|
|
21000
|
+
turnId: 'init_response'
|
|
21005
21001
|
}
|
|
21006
21002
|
});
|
|
21007
|
-
if (awaitingUserTurnQueue.length > 0) {
|
|
21008
|
-
emitQueuedAvatarEntry();
|
|
21009
|
-
} else {
|
|
21010
|
-
pendingUserTurnCount += 1;
|
|
21011
|
-
}
|
|
21012
21003
|
}
|
|
21013
21004
|
}
|
|
21014
21005
|
});
|
|
@@ -21423,8 +21414,7 @@ const showScreenShareErrorNotification = ({
|
|
|
21423
21414
|
}) => {
|
|
21424
21415
|
try {
|
|
21425
21416
|
context.externalActions.showNotification({
|
|
21426
|
-
|
|
21427
|
-
text: 'Screen sharing encountered an error. Please try again.',
|
|
21417
|
+
code: AvatarNotificationCode.ScreenShareError,
|
|
21428
21418
|
variant: AlertIconEnum.Error
|
|
21429
21419
|
});
|
|
21430
21420
|
} catch (error) {
|
|
@@ -23287,8 +23277,7 @@ const showAgentRecoveryExhaustedNotification = ({
|
|
|
23287
23277
|
context
|
|
23288
23278
|
}) => {
|
|
23289
23279
|
const notification = {
|
|
23290
|
-
|
|
23291
|
-
text: "We're having trouble getting a response. Please try asking again.",
|
|
23280
|
+
code: AvatarNotificationCode.AgentRecoveryExhausted,
|
|
23292
23281
|
variant: AlertIconEnum.Warning,
|
|
23293
23282
|
options: {
|
|
23294
23283
|
showClose: true
|
|
@@ -23332,6 +23321,7 @@ const avatarConnectionMachine = setup({
|
|
|
23332
23321
|
logAgentClientToolCallResult,
|
|
23333
23322
|
recordClientToolCallLogEntry,
|
|
23334
23323
|
appendAvatarChatEntry,
|
|
23324
|
+
removeStaleChatEntriesForRetry,
|
|
23335
23325
|
appendUserChatEntry,
|
|
23336
23326
|
appendUserTextEntry,
|
|
23337
23327
|
setScreenShareActive,
|
|
@@ -23505,7 +23495,7 @@ const avatarConnectionMachine = setup({
|
|
|
23505
23495
|
content: null,
|
|
23506
23496
|
chatLog: [],
|
|
23507
23497
|
clientToolCallLog: [],
|
|
23508
|
-
|
|
23498
|
+
pendingTextInput: null,
|
|
23509
23499
|
userAgent: '',
|
|
23510
23500
|
userAgentHints: null,
|
|
23511
23501
|
kalturaAnalyticsService: input.kalturaAnalyticsService,
|
|
@@ -24390,4 +24380,4 @@ class AvatarClientManager extends ActorManager {
|
|
|
24390
24380
|
}
|
|
24391
24381
|
}
|
|
24392
24382
|
|
|
24393
|
-
export { ApplicationFeatures, AvatarClientManager, AvatarConnectionMachineEventTypes, AvatarConnectionMachineId, ButtonNames, PageNames, agentRawTextClientToolDeltaSchema, agentRawTextDeltaSchema, agentRawTextSchema, agentTurnToTalkSchema, askPermissionsSchema, avatarClientLoggerFactory, avatarConnectionMachine, avatarRTCClientManager, clientConfigurationSchema, conversationEndedSchema, conversationTimeWarningSchema, createTinyPubsubEventsHandler, createTinyPubsubEventsSource, generatingSpeechSchema, getQueryParam, initializeLogsCollectionIfNeeded, isMobileDevice, joinCompleteSchema, noAgentSchema, onServerConnectedSchema, pauseExpiryWarningSchema, resetWebsocketClientTeardownState, showAgentSchema, showStartSpeechSchema, stvFinishedTalkingSchema, stvNewSessionSchema, stvSpeechChunkSchema, stvStartedTalkingSchema, timingTracker, websocketClientManager, websocketDebugInfo, websocketMessage, websocketNonRecoveredReconnect, websocketRecoveryState, websocketSystemMessage };
|
|
24383
|
+
export { ApplicationFeatures, AvatarClientManager, AvatarConnectionMachineEventTypes, AvatarConnectionMachineId, AvatarNotificationCode, ButtonNames, PageNames, agentRawTextClientToolDeltaSchema, agentRawTextDeltaSchema, agentRawTextSchema, agentTurnToTalkSchema, askPermissionsSchema, avatarClientLoggerFactory, avatarConnectionMachine, avatarRTCClientManager, clientConfigurationSchema, conversationEndedSchema, conversationTimeWarningSchema, createTinyPubsubEventsHandler, createTinyPubsubEventsSource, generatingSpeechSchema, getAvatarDisconnectNotificationVariant, getQueryParam, initializeLogsCollectionIfNeeded, isMappedAvatarDisconnectReason, isMobileDevice, joinCompleteSchema, noAgentSchema, onServerConnectedSchema, pauseExpiryWarningSchema, resetWebsocketClientTeardownState, showAgentSchema, showStartSpeechSchema, stvFinishedTalkingSchema, stvNewSessionSchema, stvSpeechChunkSchema, stvStartedTalkingSchema, timingTracker, unsupportedClientSchema, websocketClientManager, websocketDebugInfo, websocketMessage, websocketNonRecoveredReconnect, websocketRecoveryState, websocketSystemMessage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaltura-sdk/avatars",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"author": "kaltura",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"repository": {
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
},
|
|
10
10
|
"types": "./src/index.d.ts",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@kaltura-sdk/rtc-avatar": "^1.32.
|
|
13
|
-
"@kaltura-sdk/rtc-core": "^1.25.
|
|
12
|
+
"@kaltura-sdk/rtc-avatar": "^1.32.34",
|
|
13
|
+
"@kaltura-sdk/rtc-core": "^1.25.21",
|
|
14
14
|
"@local/genie-shared-utils": "^1.0.0",
|
|
15
15
|
"@unisphere/core": "^1.99.0",
|
|
16
|
-
"@unisphere/genie-types": "^1.39.
|
|
16
|
+
"@unisphere/genie-types": "^1.39.3",
|
|
17
17
|
"@unisphere/notifications-core": "^1.25.0",
|
|
18
18
|
"@unisphere/runtime": "^1.98.0",
|
|
19
19
|
"@unisphere/runtime-js": "^1.96.0",
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ActionFunction, MachineContext } from 'xstate';
|
|
2
|
+
export declare function asHarnessAction<TContext extends MachineContext, TEvent extends {
|
|
3
|
+
type: string;
|
|
4
|
+
}>(action: unknown): ActionFunction<TContext, TEvent, TEvent, undefined, never, never, never, never, never>;
|
package/src/lib/avatar-client-manager/machines/avatar-connection/avatar-connection-machine.d.ts
CHANGED
|
@@ -391,6 +391,9 @@ export declare const avatarConnectionMachine: import("xstate").StateMachine<Avat
|
|
|
391
391
|
} | {
|
|
392
392
|
type: "appendAvatarChatEntry";
|
|
393
393
|
params: unknown;
|
|
394
|
+
} | {
|
|
395
|
+
type: "removeStaleChatEntriesForRetry";
|
|
396
|
+
params: unknown;
|
|
394
397
|
} | {
|
|
395
398
|
type: "appendUserChatEntry";
|
|
396
399
|
params: unknown;
|
package/src/lib/avatar-client-manager/machines/avatar-connection/states/connected-state.d.ts
CHANGED
|
@@ -728,13 +728,13 @@ export declare const connectedState: {
|
|
|
728
728
|
actions: "resetAgentRecoveryRetryCount";
|
|
729
729
|
} | {
|
|
730
730
|
guard: "hasMoreRecoveryRetries";
|
|
731
|
-
actions: readonly ["incrementAgentRecoveryRetryCount", "sendReprocessTurn"];
|
|
731
|
+
actions: readonly ["removeStaleChatEntriesForRetry", "incrementAgentRecoveryRetryCount", "sendReprocessTurn"];
|
|
732
732
|
} | {
|
|
733
733
|
guard: "justExceededRecoveryRetries";
|
|
734
734
|
actions: readonly ["incrementAgentRecoveryRetryCount", "showAgentRecoveryExhaustedNotification"];
|
|
735
735
|
})[];
|
|
736
736
|
RequestAgentTurnRecovery: {
|
|
737
|
-
actions: "sendReprocessTurn";
|
|
737
|
+
actions: readonly ["removeStaleChatEntriesForRetry", "sendReprocessTurn"];
|
|
738
738
|
};
|
|
739
739
|
};
|
|
740
740
|
invoke: {
|
|
@@ -6,13 +6,13 @@ export declare const agentRecoveryMonitorState: {
|
|
|
6
6
|
actions: "resetAgentRecoveryRetryCount";
|
|
7
7
|
} | {
|
|
8
8
|
guard: "hasMoreRecoveryRetries";
|
|
9
|
-
actions: readonly ["incrementAgentRecoveryRetryCount", "sendReprocessTurn"];
|
|
9
|
+
actions: readonly ["removeStaleChatEntriesForRetry", "incrementAgentRecoveryRetryCount", "sendReprocessTurn"];
|
|
10
10
|
} | {
|
|
11
11
|
guard: "justExceededRecoveryRetries";
|
|
12
12
|
actions: readonly ["incrementAgentRecoveryRetryCount", "showAgentRecoveryExhaustedNotification"];
|
|
13
13
|
})[];
|
|
14
14
|
RequestAgentTurnRecovery: {
|
|
15
|
-
actions: "sendReprocessTurn";
|
|
15
|
+
actions: readonly ["removeStaleChatEntriesForRetry", "sendReprocessTurn"];
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
18
|
invoke: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UnisphereLoggerType } from '@unisphere/core';
|
|
2
|
-
import {
|
|
2
|
+
import { AvatarNotificationRequest } from './notifications';
|
|
3
3
|
import { clientConfigurationSchema } from './schemas';
|
|
4
4
|
import { z } from 'zod/mini';
|
|
5
5
|
import { KalturaAnalyticsServiceType, PubSubServiceType } from '@unisphere/runtime';
|
|
@@ -53,7 +53,7 @@ export interface AvatarConnectionMachineContext {
|
|
|
53
53
|
/** @deprecated TODO sk consider to avoid saving entire service object inside the machine's context */
|
|
54
54
|
pubSubService: PubSubServiceType;
|
|
55
55
|
externalActions: {
|
|
56
|
-
showNotification: (notification:
|
|
56
|
+
showNotification: (notification: AvatarNotificationRequest) => void;
|
|
57
57
|
};
|
|
58
58
|
connectionId: string;
|
|
59
59
|
clientConfiguration: z.infer<typeof clientConfigurationSchema>['clientConfiguration'] | null;
|
|
@@ -66,7 +66,11 @@ export interface AvatarConnectionMachineContext {
|
|
|
66
66
|
content: UnisphereChatBlock | null;
|
|
67
67
|
chatLog: ChatEntry[];
|
|
68
68
|
clientToolCallLog: ClientToolCallLogEntry[];
|
|
69
|
-
|
|
69
|
+
pendingTextInput: {
|
|
70
|
+
id: string | null;
|
|
71
|
+
turnId: string | null;
|
|
72
|
+
text: string;
|
|
73
|
+
} | null;
|
|
70
74
|
isAgentTalking: boolean;
|
|
71
75
|
userAgent: string;
|
|
72
76
|
userAgentHints: Record<string, string> | null;
|
|
@@ -164,6 +164,7 @@ export type AgentStartSpeechReceivedEvent = {
|
|
|
164
164
|
export type AvatarChatEntryReceivedEvent = {
|
|
165
165
|
type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived;
|
|
166
166
|
entry: ChatEntry;
|
|
167
|
+
isFinalSegment?: boolean;
|
|
167
168
|
};
|
|
168
169
|
export type UserVoiceTurnReceivedEvent = {
|
|
169
170
|
type: AvatarConnectionMachineEventTypes.UserVoiceTurnReceived;
|
|
@@ -6,6 +6,7 @@ export * from './events';
|
|
|
6
6
|
export * from './context';
|
|
7
7
|
export * from './input';
|
|
8
8
|
export * from './schemas';
|
|
9
|
+
export * from './notifications';
|
|
9
10
|
export declare const AvatarConnectionMachineId = "AvatarClient";
|
|
10
11
|
export type AvatarConnectionActor = ActorRefFrom<typeof avatarConnectionMachine>;
|
|
11
12
|
export type AvatarConnectionMachineState = SnapshotFrom<typeof avatarConnectionMachine>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UnisphereLoggerType } from '@unisphere/core';
|
|
2
|
-
import {
|
|
2
|
+
import { AvatarNotificationRequest } from './notifications';
|
|
3
3
|
import { KalturaAnalyticsServiceType, PubSubServiceType } from '@unisphere/runtime';
|
|
4
4
|
import { AvatarRuntimeSettings } from '@unisphere/genie-types';
|
|
5
5
|
import type { ClientToolHandler, ClientToolsInput, ClientToolsRegistry, GetClientToolHandler } from '@local/genie-shared-utils';
|
|
@@ -14,7 +14,7 @@ export interface AvatarConnectionMachineInput {
|
|
|
14
14
|
kalturaAnalyticsService: KalturaAnalyticsServiceType;
|
|
15
15
|
pubSubService: PubSubServiceType;
|
|
16
16
|
externalActions: {
|
|
17
|
-
showNotification: (notification:
|
|
17
|
+
showNotification: (notification: AvatarNotificationRequest) => void;
|
|
18
18
|
};
|
|
19
19
|
runtimeSettings: AvatarRuntimeSettings;
|
|
20
20
|
stickyId?: string;
|
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AlertIconEnum } from '@unisphere/notifications-core';
|
|
2
2
|
import { DisconnectReasons } from '@unisphere/genie-types';
|
|
3
|
+
export declare enum AvatarNotificationCode {
|
|
4
|
+
ScreenShareError = "ScreenShareError",
|
|
5
|
+
AgentRecoveryExhausted = "AgentRecoveryExhausted",
|
|
6
|
+
ConversationTimeWarning = "ConversationTimeWarning"
|
|
7
|
+
}
|
|
8
|
+
export type AvatarNotificationReasonCode = DisconnectReasons | AvatarNotificationCode;
|
|
9
|
+
export interface AvatarNotificationRequest {
|
|
10
|
+
code: AvatarNotificationReasonCode;
|
|
11
|
+
variant: AlertIconEnum;
|
|
12
|
+
/** Interpolation values the host's translation for `code` may reference. */
|
|
13
|
+
params?: Record<string, string | number>;
|
|
14
|
+
options?: {
|
|
15
|
+
duration?: number;
|
|
16
|
+
isPermanent?: boolean;
|
|
17
|
+
showClose?: boolean;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
3
20
|
export declare function isMappedAvatarDisconnectReason(reason: DisconnectReasons): boolean;
|
|
4
|
-
export declare function
|
|
21
|
+
export declare function getAvatarDisconnectNotificationVariant(reason: DisconnectReasons): AlertIconEnum;
|
|
@@ -69,6 +69,7 @@ export declare const agentRawTextDeltaSchema: z.ZodMiniObject<{
|
|
|
69
69
|
role: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
70
70
|
type: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
71
71
|
content: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniUnknown>]>>;
|
|
72
|
+
human: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
72
73
|
segmentNumber: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
73
74
|
et: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
74
75
|
segmentStart: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
@@ -86,6 +87,7 @@ export declare const agentRawTextClientToolDeltaSchema: z.ZodMiniObject<{
|
|
|
86
87
|
threadId: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
87
88
|
role: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
88
89
|
content: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniUnknown>]>>;
|
|
90
|
+
human: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
89
91
|
segmentNumber: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
90
92
|
et: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
91
93
|
segmentStart: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
@@ -103,3 +105,6 @@ export declare const stvSpeechChunkSchema: z.ZodMiniObject<{
|
|
|
103
105
|
durationMs: z.ZodMiniNumber<number>;
|
|
104
106
|
speechId: z.ZodMiniString<string>;
|
|
105
107
|
}, z.core.$strip>;
|
|
108
|
+
export declare const unsupportedClientSchema: z.ZodMiniObject<{
|
|
109
|
+
code: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
110
|
+
}, z.core.$strip>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AvatarNotificationRequest } from './machines/avatar-connection/types/notifications';
|
|
2
2
|
export type AvatarClientManagerActions = {
|
|
3
|
-
showNotification: (notification:
|
|
3
|
+
showNotification: (notification: AvatarNotificationRequest) => void;
|
|
4
4
|
};
|