@absolutejs/voice 0.0.22-beta.4 → 0.0.22-beta.41
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/dist/angular/index.d.ts +1 -0
- package/dist/angular/index.js +172 -2
- package/dist/angular/voice-provider-status.service.d.ts +12 -0
- package/dist/angular/voice-stream.service.d.ts +2 -0
- package/dist/assistantHealth.d.ts +81 -0
- package/dist/client/actions.d.ts +22 -0
- package/dist/client/connection.d.ts +3 -0
- package/dist/client/htmxBootstrap.js +44 -2
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +125 -2
- package/dist/client/providerStatus.d.ts +19 -0
- package/dist/diagnosticsRoutes.d.ts +44 -0
- package/dist/handoff.d.ts +54 -0
- package/dist/handoffHealth.d.ts +94 -0
- package/dist/index.d.ts +26 -2
- package/dist/index.js +3551 -128
- package/dist/modelAdapters.d.ts +99 -0
- package/dist/opsConsoleRoutes.d.ts +77 -0
- package/dist/opsWebhook.d.ts +126 -0
- package/dist/providerAdapters.d.ts +37 -0
- package/dist/providerHealth.d.ts +79 -0
- package/dist/qualityRoutes.d.ts +76 -0
- package/dist/queue.d.ts +52 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +148 -2
- package/dist/react/useVoiceController.d.ts +2 -0
- package/dist/react/useVoiceProviderStatus.d.ts +8 -0
- package/dist/react/useVoiceStream.d.ts +2 -0
- package/dist/resilienceRoutes.d.ts +106 -0
- package/dist/sessionReplay.d.ts +175 -0
- package/dist/svelte/createVoiceProviderStatus.d.ts +8 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.js +127 -2
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1468 -7
- package/dist/testing/ioProviderSimulator.d.ts +41 -0
- package/dist/testing/providerSimulator.d.ts +44 -0
- package/dist/trace.d.ts +1 -1
- package/dist/types.d.ts +84 -2
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.js +161 -2
- package/dist/vue/useVoiceProviderStatus.d.ts +9 -0
- package/dist/vue/useVoiceStream.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2992,6 +2992,289 @@ var toVoiceSessionSummary = (session) => ({
|
|
|
2992
2992
|
// src/session.ts
|
|
2993
2993
|
import { Buffer } from "buffer";
|
|
2994
2994
|
|
|
2995
|
+
// src/handoff.ts
|
|
2996
|
+
var toHex3 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2997
|
+
var signHandoffBody = async (input) => {
|
|
2998
|
+
const encoder = new TextEncoder;
|
|
2999
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
3000
|
+
hash: "SHA-256",
|
|
3001
|
+
name: "HMAC"
|
|
3002
|
+
}, false, ["sign"]);
|
|
3003
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
|
|
3004
|
+
return `sha256=${toHex3(new Uint8Array(signature))}`;
|
|
3005
|
+
};
|
|
3006
|
+
var toErrorMessage2 = (error) => error instanceof Error ? error.message : String(error);
|
|
3007
|
+
var createSkippedDelivery = (adapter) => ({
|
|
3008
|
+
adapterId: adapter.id,
|
|
3009
|
+
adapterKind: adapter.kind,
|
|
3010
|
+
status: "skipped"
|
|
3011
|
+
});
|
|
3012
|
+
var aggregateHandoffStatus = (deliveries) => {
|
|
3013
|
+
const statuses = Object.values(deliveries).map((delivery) => delivery.status);
|
|
3014
|
+
if (statuses.some((status) => status === "failed")) {
|
|
3015
|
+
return "failed";
|
|
3016
|
+
}
|
|
3017
|
+
if (statuses.some((status) => status === "delivered")) {
|
|
3018
|
+
return "delivered";
|
|
3019
|
+
}
|
|
3020
|
+
return "skipped";
|
|
3021
|
+
};
|
|
3022
|
+
var createHandoffDeliveryId = (input) => [
|
|
3023
|
+
"voice-handoff",
|
|
3024
|
+
input.sessionId,
|
|
3025
|
+
input.action,
|
|
3026
|
+
Date.now(),
|
|
3027
|
+
crypto.randomUUID()
|
|
3028
|
+
].join(":");
|
|
3029
|
+
var resolveHandoffDeliveryError = (deliveries) => Object.values(deliveries).map((delivery) => delivery.error).find(Boolean);
|
|
3030
|
+
var defaultWebhookBody = (input) => ({
|
|
3031
|
+
action: input.action,
|
|
3032
|
+
metadata: input.metadata,
|
|
3033
|
+
reason: input.reason,
|
|
3034
|
+
result: input.result,
|
|
3035
|
+
session: {
|
|
3036
|
+
id: input.session.id,
|
|
3037
|
+
scenarioId: input.session.scenarioId,
|
|
3038
|
+
status: input.session.status
|
|
3039
|
+
},
|
|
3040
|
+
source: "absolutejs-voice",
|
|
3041
|
+
target: input.target
|
|
3042
|
+
});
|
|
3043
|
+
var deliverVoiceHandoff = async (input) => {
|
|
3044
|
+
if (!input.config || input.config.adapters.length === 0) {
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
const deliveries = {};
|
|
3048
|
+
for (const adapter of input.config.adapters) {
|
|
3049
|
+
if (adapter.actions && !adapter.actions.includes(input.handoff.action)) {
|
|
3050
|
+
deliveries[adapter.id] = createSkippedDelivery(adapter);
|
|
3051
|
+
continue;
|
|
3052
|
+
}
|
|
3053
|
+
try {
|
|
3054
|
+
const result = await adapter.handoff(input.handoff);
|
|
3055
|
+
deliveries[adapter.id] = {
|
|
3056
|
+
...result,
|
|
3057
|
+
adapterId: adapter.id,
|
|
3058
|
+
adapterKind: adapter.kind
|
|
3059
|
+
};
|
|
3060
|
+
} catch (error) {
|
|
3061
|
+
deliveries[adapter.id] = {
|
|
3062
|
+
adapterId: adapter.id,
|
|
3063
|
+
adapterKind: adapter.kind,
|
|
3064
|
+
error: toErrorMessage2(error),
|
|
3065
|
+
status: "failed"
|
|
3066
|
+
};
|
|
3067
|
+
if (input.config.failMode === "throw") {
|
|
3068
|
+
throw error;
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
return {
|
|
3073
|
+
action: input.handoff.action,
|
|
3074
|
+
deliveries,
|
|
3075
|
+
status: aggregateHandoffStatus(deliveries)
|
|
3076
|
+
};
|
|
3077
|
+
};
|
|
3078
|
+
var createVoiceHandoffDeliveryRecord = (input) => {
|
|
3079
|
+
const now = Date.now();
|
|
3080
|
+
return {
|
|
3081
|
+
action: input.action,
|
|
3082
|
+
context: input.context,
|
|
3083
|
+
createdAt: now,
|
|
3084
|
+
deliveryAttempts: 0,
|
|
3085
|
+
deliveryStatus: "pending",
|
|
3086
|
+
id: input.id ?? createHandoffDeliveryId({
|
|
3087
|
+
action: input.action,
|
|
3088
|
+
sessionId: input.session.id
|
|
3089
|
+
}),
|
|
3090
|
+
metadata: input.metadata,
|
|
3091
|
+
reason: input.reason,
|
|
3092
|
+
result: input.result,
|
|
3093
|
+
session: input.session,
|
|
3094
|
+
sessionId: input.session.id,
|
|
3095
|
+
target: input.target,
|
|
3096
|
+
updatedAt: now
|
|
3097
|
+
};
|
|
3098
|
+
};
|
|
3099
|
+
var applyVoiceHandoffDeliveryResult = (delivery, result) => ({
|
|
3100
|
+
...delivery,
|
|
3101
|
+
deliveredAt: result.status === "delivered" || result.status === "skipped" ? Date.now() : delivery.deliveredAt,
|
|
3102
|
+
deliveries: result.deliveries,
|
|
3103
|
+
deliveryAttempts: (delivery.deliveryAttempts ?? 0) + 1,
|
|
3104
|
+
deliveryError: result.status === "failed" ? resolveHandoffDeliveryError(result.deliveries) : undefined,
|
|
3105
|
+
deliveryStatus: result.status,
|
|
3106
|
+
updatedAt: Date.now()
|
|
3107
|
+
});
|
|
3108
|
+
var deliverVoiceHandoffDelivery = async (options) => {
|
|
3109
|
+
const result = await deliverVoiceHandoff({
|
|
3110
|
+
config: {
|
|
3111
|
+
adapters: options.adapters,
|
|
3112
|
+
failMode: options.failMode
|
|
3113
|
+
},
|
|
3114
|
+
handoff: {
|
|
3115
|
+
action: options.delivery.action,
|
|
3116
|
+
api: options.api,
|
|
3117
|
+
context: options.delivery.context,
|
|
3118
|
+
metadata: options.delivery.metadata,
|
|
3119
|
+
reason: options.delivery.reason,
|
|
3120
|
+
result: options.delivery.result,
|
|
3121
|
+
session: options.delivery.session,
|
|
3122
|
+
target: options.delivery.target
|
|
3123
|
+
}
|
|
3124
|
+
});
|
|
3125
|
+
return result ? applyVoiceHandoffDeliveryResult(options.delivery, result) : {
|
|
3126
|
+
...options.delivery,
|
|
3127
|
+
deliveryAttempts: (options.delivery.deliveryAttempts ?? 0) + 1,
|
|
3128
|
+
deliveryStatus: "skipped",
|
|
3129
|
+
updatedAt: Date.now()
|
|
3130
|
+
};
|
|
3131
|
+
};
|
|
3132
|
+
var createVoiceMemoryHandoffDeliveryStore = () => {
|
|
3133
|
+
const deliveries = new Map;
|
|
3134
|
+
return {
|
|
3135
|
+
get: async (id) => deliveries.get(id),
|
|
3136
|
+
list: async () => [...deliveries.values()].sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)),
|
|
3137
|
+
remove: async (id) => {
|
|
3138
|
+
deliveries.delete(id);
|
|
3139
|
+
},
|
|
3140
|
+
set: async (id, delivery) => {
|
|
3141
|
+
deliveries.set(id, delivery);
|
|
3142
|
+
}
|
|
3143
|
+
};
|
|
3144
|
+
};
|
|
3145
|
+
var createVoiceWebhookHandoffAdapter = (options) => ({
|
|
3146
|
+
actions: options.actions,
|
|
3147
|
+
handoff: async (input) => {
|
|
3148
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
3149
|
+
if (typeof fetchImpl !== "function") {
|
|
3150
|
+
return {
|
|
3151
|
+
deliveredTo: options.url,
|
|
3152
|
+
error: "Handoff delivery failed: fetch is not available in this runtime.",
|
|
3153
|
+
status: "failed"
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3156
|
+
const body = JSON.stringify(await options.body?.(input) ?? defaultWebhookBody(input));
|
|
3157
|
+
const headers = {
|
|
3158
|
+
"content-type": "application/json",
|
|
3159
|
+
...options.headers
|
|
3160
|
+
};
|
|
3161
|
+
if (options.signingSecret) {
|
|
3162
|
+
const timestamp = String(Date.now());
|
|
3163
|
+
headers["x-absolutejs-timestamp"] = timestamp;
|
|
3164
|
+
headers["x-absolutejs-signature"] = await signHandoffBody({
|
|
3165
|
+
body,
|
|
3166
|
+
secret: options.signingSecret,
|
|
3167
|
+
timestamp
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
|
|
3171
|
+
const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
3172
|
+
try {
|
|
3173
|
+
const response = await fetchImpl(options.url, {
|
|
3174
|
+
body,
|
|
3175
|
+
headers,
|
|
3176
|
+
method: options.method ?? "POST",
|
|
3177
|
+
signal: controller?.signal
|
|
3178
|
+
});
|
|
3179
|
+
if (!response.ok) {
|
|
3180
|
+
return {
|
|
3181
|
+
deliveredTo: options.url,
|
|
3182
|
+
error: `Handoff delivery failed with response ${response.status}.`,
|
|
3183
|
+
status: "failed"
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
return {
|
|
3187
|
+
deliveredAt: Date.now(),
|
|
3188
|
+
deliveredTo: options.url,
|
|
3189
|
+
status: "delivered"
|
|
3190
|
+
};
|
|
3191
|
+
} finally {
|
|
3192
|
+
if (timeout) {
|
|
3193
|
+
clearTimeout(timeout);
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
},
|
|
3197
|
+
id: options.id,
|
|
3198
|
+
kind: options.kind ?? "webhook"
|
|
3199
|
+
});
|
|
3200
|
+
var escapeXml = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
3201
|
+
var defaultTwilioTransferTwiML = (input) => {
|
|
3202
|
+
if (!input.target) {
|
|
3203
|
+
return "<Response><Hangup /></Response>";
|
|
3204
|
+
}
|
|
3205
|
+
return `<Response><Dial>${escapeXml(input.target)}</Dial></Response>`;
|
|
3206
|
+
};
|
|
3207
|
+
var resolveTwilioCallSid = async (resolver, input) => {
|
|
3208
|
+
if (typeof resolver === "function") {
|
|
3209
|
+
return resolver(input);
|
|
3210
|
+
}
|
|
3211
|
+
if (typeof resolver === "string" && resolver.length > 0) {
|
|
3212
|
+
return resolver;
|
|
3213
|
+
}
|
|
3214
|
+
const metadataSid = typeof input.metadata?.callSid === "string" ? input.metadata.callSid : undefined;
|
|
3215
|
+
const sessionMetadata = input.session.metadata && typeof input.session.metadata === "object" ? input.session.metadata : undefined;
|
|
3216
|
+
const sessionSid = typeof sessionMetadata?.callSid === "string" ? sessionMetadata.callSid : undefined;
|
|
3217
|
+
return metadataSid ?? sessionSid;
|
|
3218
|
+
};
|
|
3219
|
+
var createVoiceTwilioRedirectHandoffAdapter = (options) => ({
|
|
3220
|
+
actions: options.actions ?? ["transfer"],
|
|
3221
|
+
handoff: async (input) => {
|
|
3222
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
3223
|
+
const callSid = await resolveTwilioCallSid(options.callSid, input);
|
|
3224
|
+
if (!callSid) {
|
|
3225
|
+
return {
|
|
3226
|
+
error: "Twilio handoff requires a callSid.",
|
|
3227
|
+
status: "failed"
|
|
3228
|
+
};
|
|
3229
|
+
}
|
|
3230
|
+
if (typeof fetchImpl !== "function") {
|
|
3231
|
+
return {
|
|
3232
|
+
error: "Twilio handoff failed: fetch is not available in this runtime.",
|
|
3233
|
+
status: "failed"
|
|
3234
|
+
};
|
|
3235
|
+
}
|
|
3236
|
+
const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(options.accountSid)}/Calls/${encodeURIComponent(callSid)}.json`;
|
|
3237
|
+
const body = new URLSearchParams({
|
|
3238
|
+
Twiml: await (options.buildTwiML?.(input) ?? defaultTwilioTransferTwiML(input))
|
|
3239
|
+
});
|
|
3240
|
+
const auth = btoa(`${options.accountSid}:${options.authToken}`);
|
|
3241
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
|
|
3242
|
+
const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
3243
|
+
try {
|
|
3244
|
+
const response = await fetchImpl(url, {
|
|
3245
|
+
body,
|
|
3246
|
+
headers: {
|
|
3247
|
+
authorization: `Basic ${auth}`,
|
|
3248
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
3249
|
+
},
|
|
3250
|
+
method: "POST",
|
|
3251
|
+
signal: controller?.signal
|
|
3252
|
+
});
|
|
3253
|
+
if (!response.ok) {
|
|
3254
|
+
return {
|
|
3255
|
+
deliveredTo: url,
|
|
3256
|
+
error: `Twilio handoff failed with response ${response.status}.`,
|
|
3257
|
+
status: "failed"
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
return {
|
|
3261
|
+
deliveredAt: Date.now(),
|
|
3262
|
+
deliveredTo: url,
|
|
3263
|
+
metadata: {
|
|
3264
|
+
callSid
|
|
3265
|
+
},
|
|
3266
|
+
status: "delivered"
|
|
3267
|
+
};
|
|
3268
|
+
} finally {
|
|
3269
|
+
if (timeout) {
|
|
3270
|
+
clearTimeout(timeout);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
},
|
|
3274
|
+
id: options.id ?? "twilio-redirect",
|
|
3275
|
+
kind: "twilio-redirect"
|
|
3276
|
+
});
|
|
3277
|
+
|
|
2995
3278
|
// src/turnDetection.ts
|
|
2996
3279
|
var DEFAULT_SILENCE_MS = 700;
|
|
2997
3280
|
var DEFAULT_SPEECH_THRESHOLD = 0.015;
|
|
@@ -3288,6 +3571,7 @@ var pushCallLifecycleEvent = (session, input) => {
|
|
|
3288
3571
|
}
|
|
3289
3572
|
return lifecycle;
|
|
3290
3573
|
};
|
|
3574
|
+
var getLatestCallLifecycleEvent = (session) => session.call?.events.at(-1);
|
|
3291
3575
|
var createVoiceSession = (options) => {
|
|
3292
3576
|
const logger = resolveLogger(options.logger);
|
|
3293
3577
|
const reconnect = {
|
|
@@ -3388,6 +3672,64 @@ var createVoiceSession = (options) => {
|
|
|
3388
3672
|
});
|
|
3389
3673
|
}
|
|
3390
3674
|
};
|
|
3675
|
+
const sendCallLifecycle = async (session) => {
|
|
3676
|
+
const event = getLatestCallLifecycleEvent(session);
|
|
3677
|
+
if (!event) {
|
|
3678
|
+
return;
|
|
3679
|
+
}
|
|
3680
|
+
await send({
|
|
3681
|
+
event,
|
|
3682
|
+
sessionId: options.id,
|
|
3683
|
+
type: "call_lifecycle"
|
|
3684
|
+
});
|
|
3685
|
+
};
|
|
3686
|
+
const runHandoff = async (input) => {
|
|
3687
|
+
const queuedDelivery = options.handoff?.deliveryQueue ? createVoiceHandoffDeliveryRecord({
|
|
3688
|
+
action: input.action,
|
|
3689
|
+
context: options.context,
|
|
3690
|
+
metadata: input.metadata,
|
|
3691
|
+
reason: input.reason,
|
|
3692
|
+
result: input.result,
|
|
3693
|
+
session: input.session,
|
|
3694
|
+
target: input.target
|
|
3695
|
+
}) : undefined;
|
|
3696
|
+
if (queuedDelivery) {
|
|
3697
|
+
await options.handoff?.deliveryQueue?.set(queuedDelivery.id, queuedDelivery);
|
|
3698
|
+
}
|
|
3699
|
+
if (options.handoff?.enqueueOnly) {
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
const result = await deliverVoiceHandoff({
|
|
3703
|
+
config: options.handoff,
|
|
3704
|
+
handoff: {
|
|
3705
|
+
action: input.action,
|
|
3706
|
+
api,
|
|
3707
|
+
context: options.context,
|
|
3708
|
+
metadata: input.metadata,
|
|
3709
|
+
reason: input.reason,
|
|
3710
|
+
result: input.result,
|
|
3711
|
+
session: input.session,
|
|
3712
|
+
target: input.target
|
|
3713
|
+
}
|
|
3714
|
+
});
|
|
3715
|
+
if (!result) {
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
if (queuedDelivery) {
|
|
3719
|
+
const updatedDelivery = applyVoiceHandoffDeliveryResult(queuedDelivery, result);
|
|
3720
|
+
await options.handoff?.deliveryQueue?.set(updatedDelivery.id, updatedDelivery);
|
|
3721
|
+
}
|
|
3722
|
+
await appendTrace({
|
|
3723
|
+
metadata: input.metadata,
|
|
3724
|
+
payload: {
|
|
3725
|
+
...result,
|
|
3726
|
+
reason: input.reason,
|
|
3727
|
+
target: input.target
|
|
3728
|
+
},
|
|
3729
|
+
session: input.session,
|
|
3730
|
+
type: "call.handoff"
|
|
3731
|
+
});
|
|
3732
|
+
};
|
|
3391
3733
|
const readSession = async () => options.store.getOrCreate(options.id);
|
|
3392
3734
|
const writeSession = async (mutate) => {
|
|
3393
3735
|
const session = await options.store.getOrCreate(options.id);
|
|
@@ -3578,6 +3920,7 @@ var createVoiceSession = (options) => {
|
|
|
3578
3920
|
await appendTrace({
|
|
3579
3921
|
payload: {
|
|
3580
3922
|
disposition,
|
|
3923
|
+
metadata: input.metadata,
|
|
3581
3924
|
reason: input.reason,
|
|
3582
3925
|
target: input.target,
|
|
3583
3926
|
type: "end"
|
|
@@ -3585,6 +3928,7 @@ var createVoiceSession = (options) => {
|
|
|
3585
3928
|
session,
|
|
3586
3929
|
type: "call.lifecycle"
|
|
3587
3930
|
});
|
|
3931
|
+
await sendCallLifecycle(session);
|
|
3588
3932
|
await send({
|
|
3589
3933
|
sessionId: options.id,
|
|
3590
3934
|
type: "complete"
|
|
@@ -3664,6 +4008,15 @@ var createVoiceSession = (options) => {
|
|
|
3664
4008
|
session,
|
|
3665
4009
|
type: "call.lifecycle"
|
|
3666
4010
|
});
|
|
4011
|
+
await sendCallLifecycle(session);
|
|
4012
|
+
await runHandoff({
|
|
4013
|
+
action: "transfer",
|
|
4014
|
+
metadata: input.metadata,
|
|
4015
|
+
reason: input.reason,
|
|
4016
|
+
result: input.result,
|
|
4017
|
+
session,
|
|
4018
|
+
target: input.target
|
|
4019
|
+
});
|
|
3667
4020
|
await completeInternal(input.result, {
|
|
3668
4021
|
disposition: "transferred",
|
|
3669
4022
|
invokeOnComplete: false,
|
|
@@ -3689,6 +4042,14 @@ var createVoiceSession = (options) => {
|
|
|
3689
4042
|
session,
|
|
3690
4043
|
type: "call.lifecycle"
|
|
3691
4044
|
});
|
|
4045
|
+
await sendCallLifecycle(session);
|
|
4046
|
+
await runHandoff({
|
|
4047
|
+
action: "escalate",
|
|
4048
|
+
metadata: input.metadata,
|
|
4049
|
+
reason: input.reason,
|
|
4050
|
+
result: input.result,
|
|
4051
|
+
session
|
|
4052
|
+
});
|
|
3692
4053
|
await completeInternal(input.result, {
|
|
3693
4054
|
disposition: "escalated",
|
|
3694
4055
|
invokeOnComplete: false,
|
|
@@ -3711,6 +4072,13 @@ var createVoiceSession = (options) => {
|
|
|
3711
4072
|
session,
|
|
3712
4073
|
type: "call.lifecycle"
|
|
3713
4074
|
});
|
|
4075
|
+
await sendCallLifecycle(session);
|
|
4076
|
+
await runHandoff({
|
|
4077
|
+
action: "no-answer",
|
|
4078
|
+
metadata: input?.metadata,
|
|
4079
|
+
result: input?.result,
|
|
4080
|
+
session
|
|
4081
|
+
});
|
|
3714
4082
|
await completeInternal(input?.result, {
|
|
3715
4083
|
disposition: "no-answer",
|
|
3716
4084
|
invokeOnComplete: false,
|
|
@@ -3732,6 +4100,13 @@ var createVoiceSession = (options) => {
|
|
|
3732
4100
|
session,
|
|
3733
4101
|
type: "call.lifecycle"
|
|
3734
4102
|
});
|
|
4103
|
+
await sendCallLifecycle(session);
|
|
4104
|
+
await runHandoff({
|
|
4105
|
+
action: "voicemail",
|
|
4106
|
+
metadata: input?.metadata,
|
|
4107
|
+
result: input?.result,
|
|
4108
|
+
session
|
|
4109
|
+
});
|
|
3735
4110
|
await completeInternal(input?.result, {
|
|
3736
4111
|
disposition: "voicemail",
|
|
3737
4112
|
invokeOnComplete: false,
|
|
@@ -4518,6 +4893,7 @@ var createVoiceSession = (options) => {
|
|
|
4518
4893
|
session,
|
|
4519
4894
|
type: "call.lifecycle"
|
|
4520
4895
|
});
|
|
4896
|
+
await sendCallLifecycle(session);
|
|
4521
4897
|
}
|
|
4522
4898
|
await send({
|
|
4523
4899
|
sessionId: options.id,
|
|
@@ -4755,6 +5131,14 @@ var isVoiceClientMessage = (value) => {
|
|
|
4755
5131
|
return false;
|
|
4756
5132
|
}
|
|
4757
5133
|
switch (value.type) {
|
|
5134
|
+
case "call_control":
|
|
5135
|
+
if (!("action" in value)) {
|
|
5136
|
+
return false;
|
|
5137
|
+
}
|
|
5138
|
+
if (value.action !== "complete" && value.action !== "escalate" && value.action !== "no-answer" && value.action !== "transfer" && value.action !== "voicemail") {
|
|
5139
|
+
return false;
|
|
5140
|
+
}
|
|
5141
|
+
return (!("metadata" in value) || value.metadata === undefined || value.metadata !== null && typeof value.metadata === "object") && (!("reason" in value) || value.reason === undefined || typeof value.reason === "string") && (!("target" in value) || value.target === undefined || typeof value.target === "string");
|
|
4758
5142
|
case "close":
|
|
4759
5143
|
return true;
|
|
4760
5144
|
case "end_turn":
|
|
@@ -4895,6 +5279,7 @@ var voice = (config) => {
|
|
|
4895
5279
|
audioConditioning: sessionOptions.audioConditioning,
|
|
4896
5280
|
context,
|
|
4897
5281
|
id: sessionId,
|
|
5282
|
+
handoff: config.handoff,
|
|
4898
5283
|
languageStrategy: config.languageStrategy,
|
|
4899
5284
|
lexicon,
|
|
4900
5285
|
logger: sessionOptions.logger,
|
|
@@ -5006,6 +5391,42 @@ var voice = (config) => {
|
|
|
5006
5391
|
await current.close(message.reason);
|
|
5007
5392
|
runtime.activeSessions.delete(sessionState.sessionId);
|
|
5008
5393
|
}
|
|
5394
|
+
if (message.type === "call_control" && current) {
|
|
5395
|
+
if (message.action === "transfer") {
|
|
5396
|
+
if (message.target) {
|
|
5397
|
+
await current.transfer({
|
|
5398
|
+
metadata: message.metadata,
|
|
5399
|
+
reason: message.reason,
|
|
5400
|
+
target: message.target
|
|
5401
|
+
});
|
|
5402
|
+
} else {
|
|
5403
|
+
ws.send(JSON.stringify({
|
|
5404
|
+
message: "call_control transfer requires target",
|
|
5405
|
+
recoverable: true,
|
|
5406
|
+
type: "error"
|
|
5407
|
+
}));
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
if (message.action === "escalate") {
|
|
5411
|
+
await current.escalate({
|
|
5412
|
+
metadata: message.metadata,
|
|
5413
|
+
reason: message.reason ?? "client-requested-escalation"
|
|
5414
|
+
});
|
|
5415
|
+
}
|
|
5416
|
+
if (message.action === "voicemail") {
|
|
5417
|
+
await current.markVoicemail({
|
|
5418
|
+
metadata: message.metadata
|
|
5419
|
+
});
|
|
5420
|
+
}
|
|
5421
|
+
if (message.action === "no-answer") {
|
|
5422
|
+
await current.markNoAnswer({
|
|
5423
|
+
metadata: message.metadata
|
|
5424
|
+
});
|
|
5425
|
+
}
|
|
5426
|
+
if (message.action === "complete") {
|
|
5427
|
+
await current.complete();
|
|
5428
|
+
}
|
|
5429
|
+
}
|
|
5009
5430
|
if (message.type === "start" && message.sessionId && message.sessionId !== sessionState.sessionId) {
|
|
5010
5431
|
const currentSession = runtime.activeSessions.get(sessionState.sessionId);
|
|
5011
5432
|
if (currentSession) {
|
|
@@ -5054,7 +5475,7 @@ var voice = (config) => {
|
|
|
5054
5475
|
};
|
|
5055
5476
|
// src/agent.ts
|
|
5056
5477
|
var normalizeText3 = (value) => typeof value === "string" ? value.trim() : "";
|
|
5057
|
-
var
|
|
5478
|
+
var toErrorMessage3 = (error) => error instanceof Error ? error.message : String(error);
|
|
5058
5479
|
var createHistoryMessages = (session, turn) => {
|
|
5059
5480
|
const messages = [];
|
|
5060
5481
|
for (const previousTurn of session.turns) {
|
|
@@ -5150,6 +5571,17 @@ var createVoiceAgent = (options) => {
|
|
|
5150
5571
|
if (output.assistantText?.trim()) {
|
|
5151
5572
|
messages.push({
|
|
5152
5573
|
content: output.assistantText,
|
|
5574
|
+
metadata: output.toolCalls?.length ? {
|
|
5575
|
+
toolCalls: output.toolCalls
|
|
5576
|
+
} : undefined,
|
|
5577
|
+
role: "assistant"
|
|
5578
|
+
});
|
|
5579
|
+
} else if (output.toolCalls?.length) {
|
|
5580
|
+
messages.push({
|
|
5581
|
+
content: "",
|
|
5582
|
+
metadata: {
|
|
5583
|
+
toolCalls: output.toolCalls
|
|
5584
|
+
},
|
|
5153
5585
|
role: "assistant"
|
|
5154
5586
|
});
|
|
5155
5587
|
}
|
|
@@ -5224,7 +5656,7 @@ var createVoiceAgent = (options) => {
|
|
|
5224
5656
|
toolCallId: toolCall.id
|
|
5225
5657
|
});
|
|
5226
5658
|
} catch (error) {
|
|
5227
|
-
const errorMessage =
|
|
5659
|
+
const errorMessage = toErrorMessage3(error);
|
|
5228
5660
|
toolResults.push({
|
|
5229
5661
|
error: errorMessage,
|
|
5230
5662
|
status: "error",
|
|
@@ -6090,53 +6522,352 @@ var summarizeVoiceAssistantRuns = async (input) => {
|
|
|
6090
6522
|
totalRuns: assistantRuns.length
|
|
6091
6523
|
};
|
|
6092
6524
|
};
|
|
6093
|
-
// src/
|
|
6094
|
-
import {
|
|
6095
|
-
import { join } from "path";
|
|
6525
|
+
// src/assistantHealth.ts
|
|
6526
|
+
import { Elysia as Elysia3 } from "elysia";
|
|
6096
6527
|
|
|
6097
|
-
// src/
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
}
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
deliveryAttempts: input.deliveryAttempts,
|
|
6130
|
-
deliveryError: input.deliveryError,
|
|
6131
|
-
deliveryStatus: input.deliveryStatus ?? "pending",
|
|
6132
|
-
events: input.events,
|
|
6133
|
-
id: input.id ?? createVoiceTraceSinkDeliveryId(input.events),
|
|
6134
|
-
sinkDeliveries: input.sinkDeliveries,
|
|
6135
|
-
updatedAt: input.updatedAt ?? createdAt
|
|
6528
|
+
// src/providerHealth.ts
|
|
6529
|
+
import { Elysia as Elysia2 } from "elysia";
|
|
6530
|
+
var getString = (value) => typeof value === "string" ? value : undefined;
|
|
6531
|
+
var getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
6532
|
+
var isProviderStatus = (value) => value === "success" || value === "fallback" || value === "error";
|
|
6533
|
+
var summarizeVoiceProviderHealth = async (input) => {
|
|
6534
|
+
const options = Array.isArray(input) ? { events: input } : input;
|
|
6535
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
6536
|
+
const providers = options.providers ?? [];
|
|
6537
|
+
const providerSet = new Set(providers);
|
|
6538
|
+
const now = options.now ?? Date.now();
|
|
6539
|
+
const entries = new Map;
|
|
6540
|
+
const isAllowedProvider = (value) => typeof value === "string" && (providerSet.size === 0 || providerSet.has(value));
|
|
6541
|
+
const getEntry = (provider) => {
|
|
6542
|
+
const existing = entries.get(provider);
|
|
6543
|
+
if (existing) {
|
|
6544
|
+
return existing;
|
|
6545
|
+
}
|
|
6546
|
+
const entry = {
|
|
6547
|
+
elapsedCount: 0,
|
|
6548
|
+
elapsedTotal: 0,
|
|
6549
|
+
errorCount: 0,
|
|
6550
|
+
fallbackCount: 0,
|
|
6551
|
+
provider,
|
|
6552
|
+
rateLimited: false,
|
|
6553
|
+
recommended: false,
|
|
6554
|
+
runCount: 0,
|
|
6555
|
+
status: "idle",
|
|
6556
|
+
timeoutCount: 0
|
|
6557
|
+
};
|
|
6558
|
+
entries.set(provider, entry);
|
|
6559
|
+
return entry;
|
|
6136
6560
|
};
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6561
|
+
for (const provider of providers) {
|
|
6562
|
+
getEntry(provider);
|
|
6563
|
+
}
|
|
6564
|
+
const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
|
|
6565
|
+
for (const event of events) {
|
|
6566
|
+
if (event.type === "assistant.run") {
|
|
6567
|
+
if (hasProviderRouterEvents) {
|
|
6568
|
+
continue;
|
|
6569
|
+
}
|
|
6570
|
+
const provider2 = event.payload.variantId;
|
|
6571
|
+
if (!isAllowedProvider(provider2)) {
|
|
6572
|
+
continue;
|
|
6573
|
+
}
|
|
6574
|
+
const entry2 = getEntry(provider2);
|
|
6575
|
+
entry2.runCount += 1;
|
|
6576
|
+
const elapsedMs = getNumber(event.payload.elapsedMs);
|
|
6577
|
+
if (elapsedMs !== undefined) {
|
|
6578
|
+
entry2.elapsedCount += 1;
|
|
6579
|
+
entry2.elapsedTotal += elapsedMs;
|
|
6580
|
+
}
|
|
6581
|
+
continue;
|
|
6582
|
+
}
|
|
6583
|
+
if (event.type !== "session.error") {
|
|
6584
|
+
continue;
|
|
6585
|
+
}
|
|
6586
|
+
const provider = event.payload.provider;
|
|
6587
|
+
if (!isAllowedProvider(provider)) {
|
|
6588
|
+
continue;
|
|
6589
|
+
}
|
|
6590
|
+
const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
6591
|
+
const applyProviderHealth = () => {
|
|
6592
|
+
const entry2 = getEntry(provider);
|
|
6593
|
+
const providerHealth = event.payload.providerHealth;
|
|
6594
|
+
if (providerHealth && typeof providerHealth === "object") {
|
|
6595
|
+
const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
|
|
6596
|
+
if (suppressedUntil2 !== undefined) {
|
|
6597
|
+
entry2.suppressedUntil = suppressedUntil2;
|
|
6598
|
+
}
|
|
6599
|
+
}
|
|
6600
|
+
const suppressedUntil = getNumber(event.payload.suppressedUntil);
|
|
6601
|
+
if (suppressedUntil !== undefined) {
|
|
6602
|
+
entry2.suppressedUntil = suppressedUntil;
|
|
6603
|
+
}
|
|
6604
|
+
const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
|
|
6605
|
+
if (suppressionRemainingMs !== undefined) {
|
|
6606
|
+
entry2.suppressionRemainingMs = suppressionRemainingMs;
|
|
6607
|
+
}
|
|
6608
|
+
return entry2;
|
|
6609
|
+
};
|
|
6610
|
+
if (providerStatus === "success" || providerStatus === "fallback") {
|
|
6611
|
+
const entry2 = applyProviderHealth();
|
|
6612
|
+
entry2.runCount += 1;
|
|
6613
|
+
entry2.lastSuccessAt = event.at;
|
|
6614
|
+
if (providerStatus === "success") {
|
|
6615
|
+
entry2.lastError = undefined;
|
|
6616
|
+
entry2.rateLimited = false;
|
|
6617
|
+
entry2.suppressedUntil = undefined;
|
|
6618
|
+
entry2.suppressionRemainingMs = undefined;
|
|
6619
|
+
}
|
|
6620
|
+
const elapsedMs = getNumber(event.payload.elapsedMs);
|
|
6621
|
+
if (elapsedMs !== undefined) {
|
|
6622
|
+
entry2.elapsedCount += 1;
|
|
6623
|
+
entry2.elapsedTotal += elapsedMs;
|
|
6624
|
+
}
|
|
6625
|
+
const selectedProvider = event.payload.selectedProvider;
|
|
6626
|
+
if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
|
|
6627
|
+
getEntry(selectedProvider).fallbackCount += 1;
|
|
6628
|
+
}
|
|
6629
|
+
continue;
|
|
6630
|
+
}
|
|
6631
|
+
const entry = applyProviderHealth();
|
|
6632
|
+
entry.errorCount += 1;
|
|
6633
|
+
if (event.payload.timedOut === true) {
|
|
6634
|
+
entry.timeoutCount += 1;
|
|
6635
|
+
}
|
|
6636
|
+
entry.lastError = getString(event.payload.error);
|
|
6637
|
+
entry.lastErrorAt = event.at;
|
|
6638
|
+
entry.rateLimited ||= event.payload.rateLimited === true;
|
|
6639
|
+
}
|
|
6640
|
+
const summaries = [...entries.values()].map((entry) => {
|
|
6641
|
+
const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
|
|
6642
|
+
const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
|
|
6643
|
+
const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
|
|
6644
|
+
const recoverable = hadSuppression && !activeSuppression;
|
|
6645
|
+
const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
|
|
6646
|
+
const status = activeSuppression ? "suppressed" : recoverable ? "recoverable" : entry.rateLimited ? "rate-limited" : entry.errorCount > 0 && (!entry.lastSuccessAt || !entry.lastErrorAt || entry.lastErrorAt > entry.lastSuccessAt) ? "degraded" : entry.runCount > 0 ? "healthy" : "idle";
|
|
6647
|
+
return {
|
|
6648
|
+
averageElapsedMs,
|
|
6649
|
+
errorCount: entry.errorCount,
|
|
6650
|
+
fallbackCount: entry.fallbackCount,
|
|
6651
|
+
lastError: entry.lastError,
|
|
6652
|
+
lastErrorAt: entry.lastErrorAt,
|
|
6653
|
+
lastSuccessAt: entry.lastSuccessAt,
|
|
6654
|
+
provider: entry.provider,
|
|
6655
|
+
rateLimited: entry.rateLimited,
|
|
6656
|
+
recommended: false,
|
|
6657
|
+
runCount: entry.runCount,
|
|
6658
|
+
status,
|
|
6659
|
+
suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
|
|
6660
|
+
suppressedUntil: entry.suppressedUntil,
|
|
6661
|
+
timeoutCount: entry.timeoutCount
|
|
6662
|
+
};
|
|
6663
|
+
});
|
|
6664
|
+
const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
|
|
6665
|
+
if (recommended) {
|
|
6666
|
+
recommended.recommended = true;
|
|
6667
|
+
}
|
|
6668
|
+
return summaries;
|
|
6669
|
+
};
|
|
6670
|
+
var escapeHtml3 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6671
|
+
var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
|
|
6672
|
+
'<div class="voice-provider-health">',
|
|
6673
|
+
...providers.map((provider) => {
|
|
6674
|
+
const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
|
|
6675
|
+
return [
|
|
6676
|
+
`<article class="voice-provider-card ${escapeHtml3(provider.status)}">`,
|
|
6677
|
+
'<div class="voice-provider-card-header">',
|
|
6678
|
+
`<strong>${escapeHtml3(provider.provider)}</strong>`,
|
|
6679
|
+
`<span>${escapeHtml3(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
|
|
6680
|
+
"</div>",
|
|
6681
|
+
"<dl>",
|
|
6682
|
+
`<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
|
|
6683
|
+
`<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
|
|
6684
|
+
`<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
|
|
6685
|
+
`<div><dt>Timeouts</dt><dd>${String(provider.timeoutCount)}</dd></div>`,
|
|
6686
|
+
`<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
|
|
6687
|
+
"</dl>",
|
|
6688
|
+
suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
|
|
6689
|
+
provider.lastError ? `<p>${escapeHtml3(provider.lastError)}</p>` : "",
|
|
6690
|
+
"</article>"
|
|
6691
|
+
].join("");
|
|
6692
|
+
}),
|
|
6693
|
+
"</div>"
|
|
6694
|
+
].join("");
|
|
6695
|
+
var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
|
|
6696
|
+
var createVoiceProviderHealthHTMLHandler = (options) => async () => {
|
|
6697
|
+
const providers = await summarizeVoiceProviderHealth(options);
|
|
6698
|
+
const render = options.render ?? renderVoiceProviderHealthHTML;
|
|
6699
|
+
const body = await render(providers);
|
|
6700
|
+
return new Response(body, {
|
|
6701
|
+
headers: {
|
|
6702
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6703
|
+
...options.headers
|
|
6704
|
+
}
|
|
6705
|
+
});
|
|
6706
|
+
};
|
|
6707
|
+
var createVoiceProviderHealthRoutes = (options) => {
|
|
6708
|
+
const path = options.path ?? "/api/provider-status";
|
|
6709
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
6710
|
+
const routes = new Elysia2({
|
|
6711
|
+
name: options.name ?? "absolutejs-voice-provider-health"
|
|
6712
|
+
}).get(path, createVoiceProviderHealthJSONHandler(options));
|
|
6713
|
+
if (htmlPath) {
|
|
6714
|
+
routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
|
|
6715
|
+
}
|
|
6716
|
+
return routes;
|
|
6717
|
+
};
|
|
6718
|
+
|
|
6719
|
+
// src/assistantHealth.ts
|
|
6720
|
+
var escapeHtml4 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6721
|
+
var renderCountMap = (values) => {
|
|
6722
|
+
const entries = Object.entries(values).sort((left, right) => right[1] - left[1]);
|
|
6723
|
+
if (entries.length === 0) {
|
|
6724
|
+
return '<p class="voice-assistant-health-empty">No data yet.</p>';
|
|
6725
|
+
}
|
|
6726
|
+
return [
|
|
6727
|
+
'<div class="voice-assistant-health-metrics">',
|
|
6728
|
+
...entries.map(([label, value]) => `<div><span>${escapeHtml4(label)}</span><strong>${String(value)}</strong></div>`),
|
|
6729
|
+
"</div>"
|
|
6730
|
+
].join("");
|
|
6731
|
+
};
|
|
6732
|
+
var getString2 = (value) => typeof value === "string" ? value : undefined;
|
|
6733
|
+
var getRecentFailures = (events, maxFailures, replayHref) => events.filter((event) => event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string") || event.type === "assistant.guardrail" && event.payload.action === "blocked").toReversed().slice(0, maxFailures).map((event) => {
|
|
6734
|
+
const failure = {
|
|
6735
|
+
at: event.at,
|
|
6736
|
+
assistantId: getString2(event.payload.assistantId),
|
|
6737
|
+
error: getString2(event.payload.error),
|
|
6738
|
+
provider: getString2(event.payload.provider),
|
|
6739
|
+
rateLimited: event.payload.rateLimited === true ? true : undefined,
|
|
6740
|
+
sessionId: event.sessionId,
|
|
6741
|
+
status: getString2(event.payload.providerStatus),
|
|
6742
|
+
turnId: event.turnId,
|
|
6743
|
+
type: event.type
|
|
6744
|
+
};
|
|
6745
|
+
const href = replayHref === false ? undefined : typeof replayHref === "function" ? replayHref(failure) : `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
|
|
6746
|
+
return {
|
|
6747
|
+
...failure,
|
|
6748
|
+
replayHref: href
|
|
6749
|
+
};
|
|
6750
|
+
});
|
|
6751
|
+
var summarizeVoiceAssistantHealth = async (options) => {
|
|
6752
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
6753
|
+
return {
|
|
6754
|
+
assistantRuns: await summarizeVoiceAssistantRuns({ events }),
|
|
6755
|
+
providerHealth: await summarizeVoiceProviderHealth({
|
|
6756
|
+
events,
|
|
6757
|
+
providers: options.providers
|
|
6758
|
+
}),
|
|
6759
|
+
recentFailures: getRecentFailures(events, options.maxFailures ?? 8, options.replayHref)
|
|
6760
|
+
};
|
|
6761
|
+
};
|
|
6762
|
+
var renderVoiceAssistantHealthHTML = (summary) => {
|
|
6763
|
+
const assistant = summary.assistantRuns.assistants[0];
|
|
6764
|
+
const failures = summary.recentFailures;
|
|
6765
|
+
return [
|
|
6766
|
+
'<div class="voice-assistant-health">',
|
|
6767
|
+
'<section class="voice-assistant-health-grid">',
|
|
6768
|
+
`<article><span>Runs</span><strong>${String(assistant?.runCount ?? 0)}</strong></article>`,
|
|
6769
|
+
`<article><span>Sessions</span><strong>${String(assistant?.sessions ?? 0)}</strong></article>`,
|
|
6770
|
+
`<article><span>Guardrails</span><strong>${String(assistant?.guardrailCount ?? 0)}</strong></article>`,
|
|
6771
|
+
`<article><span>Avg latency</span><strong>${String(assistant?.averageElapsedMs ?? 0)}ms</strong></article>`,
|
|
6772
|
+
"</section>",
|
|
6773
|
+
"<section>",
|
|
6774
|
+
"<h3>Provider Health</h3>",
|
|
6775
|
+
renderVoiceProviderHealthHTML(summary.providerHealth),
|
|
6776
|
+
"</section>",
|
|
6777
|
+
'<section class="voice-assistant-health-columns">',
|
|
6778
|
+
`<article><h3>Outcomes</h3>${renderCountMap(assistant?.outcomes ?? {})}</article>`,
|
|
6779
|
+
`<article><h3>Variants</h3>${renderCountMap(assistant?.variants ?? {})}</article>`,
|
|
6780
|
+
`<article><h3>Tools</h3>${renderCountMap(assistant?.toolCalls ?? {})}</article>`,
|
|
6781
|
+
`<article><h3>Artifact Plans</h3>${renderCountMap(assistant?.artifactPlans ?? {})}</article>`,
|
|
6782
|
+
"</section>",
|
|
6783
|
+
"<section>",
|
|
6784
|
+
"<h3>Recent Failures</h3>",
|
|
6785
|
+
failures.length === 0 ? '<p class="voice-assistant-health-empty">No failures yet.</p>' : [
|
|
6786
|
+
'<div class="voice-assistant-health-failures">',
|
|
6787
|
+
...failures.map((failure) => [
|
|
6788
|
+
"<article>",
|
|
6789
|
+
`<strong>${escapeHtml4(failure.provider ?? failure.assistantId ?? failure.type)}</strong>`,
|
|
6790
|
+
`<span>${escapeHtml4(failure.status ?? (failure.rateLimited ? "rate-limited" : "error"))}</span>`,
|
|
6791
|
+
failure.error ? `<p>${escapeHtml4(failure.error)}</p>` : "",
|
|
6792
|
+
`<small>${escapeHtml4(failure.sessionId)}${failure.turnId ? ` / ${escapeHtml4(failure.turnId)}` : ""}</small>`,
|
|
6793
|
+
failure.replayHref ? `<p><a href="${escapeHtml4(failure.replayHref)}">Open replay</a></p>` : "",
|
|
6794
|
+
"</article>"
|
|
6795
|
+
].join("")),
|
|
6796
|
+
"</div>"
|
|
6797
|
+
].join(""),
|
|
6798
|
+
"</section>",
|
|
6799
|
+
"</div>"
|
|
6800
|
+
].join("");
|
|
6801
|
+
};
|
|
6802
|
+
var createVoiceAssistantHealthJSONHandler = (options) => async () => summarizeVoiceAssistantHealth(options);
|
|
6803
|
+
var createVoiceAssistantHealthHTMLHandler = (options) => async () => {
|
|
6804
|
+
const summary = await summarizeVoiceAssistantHealth(options);
|
|
6805
|
+
const render = options.render ?? renderVoiceAssistantHealthHTML;
|
|
6806
|
+
const body = await render(summary);
|
|
6807
|
+
return new Response(body, {
|
|
6808
|
+
headers: {
|
|
6809
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6810
|
+
...options.headers
|
|
6811
|
+
}
|
|
6812
|
+
});
|
|
6813
|
+
};
|
|
6814
|
+
var createVoiceAssistantHealthRoutes = (options) => {
|
|
6815
|
+
const path = options.path ?? "/api/assistant-health";
|
|
6816
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
6817
|
+
const routes = new Elysia3({
|
|
6818
|
+
name: options.name ?? "absolutejs-voice-assistant-health"
|
|
6819
|
+
}).get(path, createVoiceAssistantHealthJSONHandler(options));
|
|
6820
|
+
if (htmlPath) {
|
|
6821
|
+
routes.get(htmlPath, createVoiceAssistantHealthHTMLHandler(options));
|
|
6822
|
+
}
|
|
6823
|
+
return routes;
|
|
6824
|
+
};
|
|
6825
|
+
// src/diagnosticsRoutes.ts
|
|
6826
|
+
import { Elysia as Elysia4 } from "elysia";
|
|
6827
|
+
|
|
6828
|
+
// src/trace.ts
|
|
6829
|
+
var createVoiceTraceEventId = (event) => [
|
|
6830
|
+
event.sessionId,
|
|
6831
|
+
event.turnId ?? "session",
|
|
6832
|
+
event.type,
|
|
6833
|
+
String(event.at ?? Date.now()),
|
|
6834
|
+
crypto.randomUUID()
|
|
6835
|
+
].map(encodeURIComponent).join(":");
|
|
6836
|
+
var createVoiceTraceEvent = (event) => ({
|
|
6837
|
+
...event,
|
|
6838
|
+
at: event.at,
|
|
6839
|
+
id: event.id ?? createVoiceTraceEventId({
|
|
6840
|
+
at: event.at,
|
|
6841
|
+
sessionId: event.sessionId,
|
|
6842
|
+
turnId: event.turnId,
|
|
6843
|
+
type: event.type
|
|
6844
|
+
})
|
|
6845
|
+
});
|
|
6846
|
+
var createVoiceTraceSinkDeliveryId = (events) => {
|
|
6847
|
+
const firstEvent = events[0];
|
|
6848
|
+
return [
|
|
6849
|
+
firstEvent?.sessionId ?? "trace",
|
|
6850
|
+
firstEvent?.traceId ?? "sink",
|
|
6851
|
+
String(firstEvent?.at ?? Date.now()),
|
|
6852
|
+
crypto.randomUUID()
|
|
6853
|
+
].map(encodeURIComponent).join(":");
|
|
6854
|
+
};
|
|
6855
|
+
var createVoiceTraceSinkDeliveryRecord = (input) => {
|
|
6856
|
+
const createdAt = input.createdAt ?? Date.now();
|
|
6857
|
+
return {
|
|
6858
|
+
createdAt,
|
|
6859
|
+
deliveredAt: input.deliveredAt,
|
|
6860
|
+
deliveryAttempts: input.deliveryAttempts,
|
|
6861
|
+
deliveryError: input.deliveryError,
|
|
6862
|
+
deliveryStatus: input.deliveryStatus ?? "pending",
|
|
6863
|
+
events: input.events,
|
|
6864
|
+
id: input.id ?? createVoiceTraceSinkDeliveryId(input.events),
|
|
6865
|
+
sinkDeliveries: input.sinkDeliveries,
|
|
6866
|
+
updatedAt: input.updatedAt ?? createdAt
|
|
6867
|
+
};
|
|
6868
|
+
};
|
|
6869
|
+
var matchesTraceFilter = (event, filter) => {
|
|
6870
|
+
if (filter.sessionId !== undefined && event.sessionId !== filter.sessionId) {
|
|
6140
6871
|
return false;
|
|
6141
6872
|
}
|
|
6142
6873
|
if (filter.turnId !== undefined && event.turnId !== filter.turnId) {
|
|
@@ -6196,7 +6927,7 @@ var sleep3 = async (delayMs) => {
|
|
|
6196
6927
|
}
|
|
6197
6928
|
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
6198
6929
|
};
|
|
6199
|
-
var
|
|
6930
|
+
var toHex4 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
6200
6931
|
var signVoiceTraceSinkBody = async (input) => {
|
|
6201
6932
|
const encoder = new TextEncoder;
|
|
6202
6933
|
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
@@ -6205,7 +6936,7 @@ var signVoiceTraceSinkBody = async (input) => {
|
|
|
6205
6936
|
}, false, ["sign"]);
|
|
6206
6937
|
const payload = encoder.encode(`${input.timestamp}.${input.body}`);
|
|
6207
6938
|
const signature = await crypto.subtle.sign("HMAC", key, payload);
|
|
6208
|
-
return `sha256=${
|
|
6939
|
+
return `sha256=${toHex4(new Uint8Array(signature))}`;
|
|
6209
6940
|
};
|
|
6210
6941
|
var createVoiceTraceSinkDeliveryError = (input) => {
|
|
6211
6942
|
if (input.response) {
|
|
@@ -6426,7 +7157,7 @@ var exportVoiceTrace = async (input) => {
|
|
|
6426
7157
|
};
|
|
6427
7158
|
};
|
|
6428
7159
|
var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
6429
|
-
var
|
|
7160
|
+
var escapeHtml5 = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6430
7161
|
var formatTraceValue = (value) => {
|
|
6431
7162
|
if (value === undefined || value === null) {
|
|
6432
7163
|
return "";
|
|
@@ -6704,10 +7435,10 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6704
7435
|
const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
|
|
6705
7436
|
return [
|
|
6706
7437
|
"<tr>",
|
|
6707
|
-
`<td>${
|
|
6708
|
-
`<td>${
|
|
6709
|
-
`<td>${
|
|
6710
|
-
`<td><code>${
|
|
7438
|
+
`<td>${escapeHtml5(String(offset))}</td>`,
|
|
7439
|
+
`<td>${escapeHtml5(event.type)}</td>`,
|
|
7440
|
+
`<td>${escapeHtml5(event.turnId ?? "")}</td>`,
|
|
7441
|
+
`<td><code>${escapeHtml5(JSON.stringify(event.payload))}</code></td>`,
|
|
6711
7442
|
"</tr>"
|
|
6712
7443
|
].join("");
|
|
6713
7444
|
}).join(`
|
|
@@ -6718,7 +7449,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6718
7449
|
"<head>",
|
|
6719
7450
|
'<meta charset="utf-8" />',
|
|
6720
7451
|
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
6721
|
-
`<title>${
|
|
7452
|
+
`<title>${escapeHtml5(options.title ?? "Voice Trace")}</title>`,
|
|
6722
7453
|
"<style>",
|
|
6723
7454
|
"body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
|
|
6724
7455
|
"main{max-width:1100px;margin:auto}",
|
|
@@ -6732,7 +7463,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6732
7463
|
"</style>",
|
|
6733
7464
|
"</head>",
|
|
6734
7465
|
"<body><main>",
|
|
6735
|
-
`<h1>${
|
|
7466
|
+
`<h1>${escapeHtml5(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
|
|
6736
7467
|
`<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
|
|
6737
7468
|
'<section class="summary">',
|
|
6738
7469
|
`<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
|
|
@@ -6746,7 +7477,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6746
7477
|
eventRows,
|
|
6747
7478
|
"</tbody></table>",
|
|
6748
7479
|
"<h2>Markdown Export</h2>",
|
|
6749
|
-
`<pre>${
|
|
7480
|
+
`<pre>${escapeHtml5(markdown)}</pre>`,
|
|
6750
7481
|
"</main></body></html>"
|
|
6751
7482
|
].join(`
|
|
6752
7483
|
`);
|
|
@@ -6758,7 +7489,385 @@ var buildVoiceTraceReplay = (events, options = {}) => ({
|
|
|
6758
7489
|
summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
|
|
6759
7490
|
});
|
|
6760
7491
|
|
|
7492
|
+
// src/diagnosticsRoutes.ts
|
|
7493
|
+
var escapeHtml6 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
7494
|
+
var getString3 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
7495
|
+
var getNumber2 = (value) => {
|
|
7496
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : undefined;
|
|
7497
|
+
return typeof parsed === "number" && Number.isFinite(parsed) ? parsed : undefined;
|
|
7498
|
+
};
|
|
7499
|
+
var getBoolean = (value) => value === true || value === "true" || value === "1";
|
|
7500
|
+
var parseTraceTypeFilter = (value) => {
|
|
7501
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
7502
|
+
return;
|
|
7503
|
+
}
|
|
7504
|
+
const types = value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
7505
|
+
return types.length <= 1 ? types[0] : types;
|
|
7506
|
+
};
|
|
7507
|
+
var resolveVoiceDiagnosticsTraceFilter = (query) => ({
|
|
7508
|
+
limit: getNumber2(query.limit),
|
|
7509
|
+
scenarioId: getString3(query.scenarioId),
|
|
7510
|
+
sessionId: getString3(query.sessionId),
|
|
7511
|
+
traceId: getString3(query.traceId),
|
|
7512
|
+
turnId: getString3(query.turnId),
|
|
7513
|
+
type: parseTraceTypeFilter(query.type)
|
|
7514
|
+
});
|
|
7515
|
+
var filterByDiagnosticsQuery = (events, query) => {
|
|
7516
|
+
const provider = getString3(query.provider);
|
|
7517
|
+
const status = getString3(query.status);
|
|
7518
|
+
const since = getNumber2(query.since);
|
|
7519
|
+
const until = getNumber2(query.until);
|
|
7520
|
+
return filterVoiceTraceEvents(events, resolveVoiceDiagnosticsTraceFilter(query)).filter((event) => (!provider || event.payload.provider === provider) && (!status || event.payload.providerStatus === status || event.payload.status === status) && (since === undefined || event.at >= since) && (until === undefined || event.at <= until));
|
|
7521
|
+
};
|
|
7522
|
+
var buildVoiceDiagnosticsMarkdown = (events, options = {}) => {
|
|
7523
|
+
const summary = summarizeVoiceTrace(events);
|
|
7524
|
+
const evaluation = evaluateVoiceTrace(events, options.evaluation);
|
|
7525
|
+
const trace = renderVoiceTraceMarkdown(events, {
|
|
7526
|
+
evaluation: options.evaluation,
|
|
7527
|
+
title: options.title ?? `Voice Diagnostics ${summary.sessionId ?? ""}`.trim()
|
|
7528
|
+
});
|
|
7529
|
+
return [
|
|
7530
|
+
`# ${options.title ?? "Voice Diagnostics Bug Report"}`,
|
|
7531
|
+
"",
|
|
7532
|
+
`Session: ${summary.sessionId ?? "unknown"}`,
|
|
7533
|
+
`Pass: ${evaluation.pass ? "yes" : "no"}`,
|
|
7534
|
+
`Events: ${summary.eventCount}`,
|
|
7535
|
+
`Turns: ${summary.turnCount}`,
|
|
7536
|
+
`Errors: ${summary.errorCount}`,
|
|
7537
|
+
`Tool errors: ${summary.toolErrorCount}`,
|
|
7538
|
+
`Estimated cost units: ${summary.cost.estimatedRelativeCostUnits}`,
|
|
7539
|
+
"",
|
|
7540
|
+
"## Issues",
|
|
7541
|
+
"",
|
|
7542
|
+
evaluation.issues.length ? evaluation.issues.map((issue) => `- [${issue.severity}] ${issue.code}: ${issue.message}`).join(`
|
|
7543
|
+
`) : "- none",
|
|
7544
|
+
"",
|
|
7545
|
+
"## Trace",
|
|
7546
|
+
"",
|
|
7547
|
+
trace
|
|
7548
|
+
].join(`
|
|
7549
|
+
`);
|
|
7550
|
+
};
|
|
7551
|
+
var renderDiagnosticsIndex = (input) => {
|
|
7552
|
+
const sessions = new Map;
|
|
7553
|
+
for (const event of input.events) {
|
|
7554
|
+
sessions.set(event.sessionId, [...sessions.get(event.sessionId) ?? [], event]);
|
|
7555
|
+
}
|
|
7556
|
+
const rows = [...sessions.entries()].sort(([, left], [, right]) => (right.at(-1)?.at ?? 0) - (left.at(-1)?.at ?? 0)).slice(0, 50).map(([sessionId, events]) => {
|
|
7557
|
+
const summary = summarizeVoiceTrace(events);
|
|
7558
|
+
const encoded = encodeURIComponent(sessionId);
|
|
7559
|
+
return `<tr><td>${escapeHtml6(sessionId)}</td><td>${summary.eventCount}</td><td>${summary.turnCount}</td><td>${summary.errorCount}</td><td><a href="${input.basePath}/html?sessionId=${encoded}&redact=true">HTML</a> \xB7 <a href="${input.basePath}/markdown?sessionId=${encoded}&redact=true">Markdown</a> \xB7 <a href="${input.basePath}/json?sessionId=${encoded}&redact=true">JSON</a></td></tr>`;
|
|
7560
|
+
}).join("");
|
|
7561
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml6(input.title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}table{width:100%;border-collapse:collapse;background:white}td,th{border-bottom:1px solid #eee;padding:.7rem;text-align:left}a{color:#9a3412}</style></head><body><main><h1>${escapeHtml6(input.title)}</h1><p>Recent voice trace diagnostics. Exports support filters: sessionId, traceId, turnId, scenarioId, type, provider, status, since, until, limit, redact.</p><table><thead><tr><th>Session</th><th>Events</th><th>Turns</th><th>Errors</th><th>Exports</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
|
|
7562
|
+
};
|
|
7563
|
+
var withRedaction = (events, query, defaultRedact) => {
|
|
7564
|
+
const shouldRedact = query.redact === undefined ? defaultRedact : getBoolean(query.redact);
|
|
7565
|
+
return shouldRedact ? redactVoiceTraceEvents(events, shouldRedact) : events;
|
|
7566
|
+
};
|
|
7567
|
+
var createVoiceDiagnosticsRoutes = (options) => {
|
|
7568
|
+
const path = options.path ?? "/diagnostics";
|
|
7569
|
+
const title = options.title ?? "AbsoluteJS Voice Diagnostics";
|
|
7570
|
+
const routes = new Elysia4({
|
|
7571
|
+
name: options.name ?? "absolutejs-voice-diagnostics"
|
|
7572
|
+
});
|
|
7573
|
+
routes.get(path, async () => {
|
|
7574
|
+
const events = await options.store.list();
|
|
7575
|
+
return new Response(renderDiagnosticsIndex({ basePath: path, events, title }), {
|
|
7576
|
+
headers: {
|
|
7577
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7578
|
+
...options.headers
|
|
7579
|
+
}
|
|
7580
|
+
});
|
|
7581
|
+
});
|
|
7582
|
+
routes.get(`${path}/json`, async ({ query }) => {
|
|
7583
|
+
const events = filterByDiagnosticsQuery(await options.store.list(), query);
|
|
7584
|
+
const redacted = withRedaction(events, query, options.redact);
|
|
7585
|
+
return Response.json({
|
|
7586
|
+
...await exportVoiceTrace({
|
|
7587
|
+
filter: resolveVoiceDiagnosticsTraceFilter(query),
|
|
7588
|
+
redact: false,
|
|
7589
|
+
store: {
|
|
7590
|
+
...options.store,
|
|
7591
|
+
list: async () => redacted
|
|
7592
|
+
}
|
|
7593
|
+
}),
|
|
7594
|
+
filteredCount: events.length,
|
|
7595
|
+
redacted: redacted !== events
|
|
7596
|
+
});
|
|
7597
|
+
});
|
|
7598
|
+
routes.get(`${path}/markdown`, async ({ query }) => {
|
|
7599
|
+
const events = withRedaction(filterByDiagnosticsQuery(await options.store.list(), query), query, options.redact ?? true);
|
|
7600
|
+
const body = buildVoiceDiagnosticsMarkdown(events, {
|
|
7601
|
+
evaluation: options.evaluation,
|
|
7602
|
+
title
|
|
7603
|
+
});
|
|
7604
|
+
return new Response(body, {
|
|
7605
|
+
headers: {
|
|
7606
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
7607
|
+
...options.headers
|
|
7608
|
+
}
|
|
7609
|
+
});
|
|
7610
|
+
});
|
|
7611
|
+
routes.get(`${path}/html`, async ({ query }) => {
|
|
7612
|
+
const events = withRedaction(filterByDiagnosticsQuery(await options.store.list(), query), query, options.redact ?? true);
|
|
7613
|
+
const body = renderVoiceTraceHTML(events, {
|
|
7614
|
+
evaluation: options.evaluation,
|
|
7615
|
+
title
|
|
7616
|
+
});
|
|
7617
|
+
return new Response(body, {
|
|
7618
|
+
headers: {
|
|
7619
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7620
|
+
...options.headers
|
|
7621
|
+
}
|
|
7622
|
+
});
|
|
7623
|
+
});
|
|
7624
|
+
return routes;
|
|
7625
|
+
};
|
|
7626
|
+
// src/sessionReplay.ts
|
|
7627
|
+
import { Elysia as Elysia5 } from "elysia";
|
|
7628
|
+
var getString4 = (value) => typeof value === "string" ? value : undefined;
|
|
7629
|
+
var escapeHtml7 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
7630
|
+
var increment2 = (record, key) => {
|
|
7631
|
+
record[key] = (record[key] ?? 0) + 1;
|
|
7632
|
+
};
|
|
7633
|
+
var buildReplayTurns = (events) => {
|
|
7634
|
+
const turns = new Map;
|
|
7635
|
+
const getTurn = (turnId) => {
|
|
7636
|
+
const existing = turns.get(turnId);
|
|
7637
|
+
if (existing) {
|
|
7638
|
+
return existing;
|
|
7639
|
+
}
|
|
7640
|
+
const turn = {
|
|
7641
|
+
assistantReplies: [],
|
|
7642
|
+
errors: [],
|
|
7643
|
+
id: turnId,
|
|
7644
|
+
modelCalls: [],
|
|
7645
|
+
tools: [],
|
|
7646
|
+
transcripts: []
|
|
7647
|
+
};
|
|
7648
|
+
turns.set(turnId, turn);
|
|
7649
|
+
return turn;
|
|
7650
|
+
};
|
|
7651
|
+
for (const event of events) {
|
|
7652
|
+
const turnId = event.turnId ?? "session";
|
|
7653
|
+
const turn = getTurn(turnId);
|
|
7654
|
+
switch (event.type) {
|
|
7655
|
+
case "turn.transcript":
|
|
7656
|
+
turn.transcripts.push({
|
|
7657
|
+
isFinal: event.payload.isFinal === true,
|
|
7658
|
+
text: getString4(event.payload.text)
|
|
7659
|
+
});
|
|
7660
|
+
break;
|
|
7661
|
+
case "turn.committed":
|
|
7662
|
+
turn.committedText = getString4(event.payload.text);
|
|
7663
|
+
break;
|
|
7664
|
+
case "turn.assistant": {
|
|
7665
|
+
const text = getString4(event.payload.text);
|
|
7666
|
+
if (text) {
|
|
7667
|
+
turn.assistantReplies.push(text);
|
|
7668
|
+
}
|
|
7669
|
+
break;
|
|
7670
|
+
}
|
|
7671
|
+
case "agent.model":
|
|
7672
|
+
case "assistant.run":
|
|
7673
|
+
turn.modelCalls.push(event.payload);
|
|
7674
|
+
break;
|
|
7675
|
+
case "agent.tool":
|
|
7676
|
+
turn.tools.push(event.payload);
|
|
7677
|
+
break;
|
|
7678
|
+
case "session.error":
|
|
7679
|
+
turn.errors.push(event.payload);
|
|
7680
|
+
break;
|
|
7681
|
+
}
|
|
7682
|
+
}
|
|
7683
|
+
return [...turns.values()];
|
|
7684
|
+
};
|
|
7685
|
+
var summarizeVoiceSessionReplay = async (options) => {
|
|
7686
|
+
const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
|
|
7687
|
+
const events = filterVoiceTraceEvents(sourceEvents, {
|
|
7688
|
+
sessionId: options.sessionId
|
|
7689
|
+
});
|
|
7690
|
+
const replay = buildVoiceTraceReplay(events, {
|
|
7691
|
+
evaluation: options.evaluation,
|
|
7692
|
+
redact: options.redact,
|
|
7693
|
+
title: options.title ?? `Voice Session ${options.sessionId}`
|
|
7694
|
+
});
|
|
7695
|
+
const startedAt = replay.summary.startedAt;
|
|
7696
|
+
return {
|
|
7697
|
+
evaluation: replay.evaluation,
|
|
7698
|
+
events,
|
|
7699
|
+
html: replay.html,
|
|
7700
|
+
markdown: replay.markdown,
|
|
7701
|
+
sessionId: options.sessionId,
|
|
7702
|
+
summary: replay.summary,
|
|
7703
|
+
timeline: events.map((event) => ({
|
|
7704
|
+
at: event.at,
|
|
7705
|
+
offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
|
|
7706
|
+
payload: event.payload,
|
|
7707
|
+
turnId: event.turnId,
|
|
7708
|
+
type: event.type
|
|
7709
|
+
})),
|
|
7710
|
+
turns: buildReplayTurns(events)
|
|
7711
|
+
};
|
|
7712
|
+
};
|
|
7713
|
+
var summarizeVoiceSessions = async (options = {}) => {
|
|
7714
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
7715
|
+
const grouped = new Map;
|
|
7716
|
+
for (const event of events) {
|
|
7717
|
+
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
7718
|
+
}
|
|
7719
|
+
const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
|
|
7720
|
+
const sorted = filterVoiceTraceEvents(sessionEvents);
|
|
7721
|
+
const summary = buildVoiceTraceReplay(sorted, {
|
|
7722
|
+
evaluation: {
|
|
7723
|
+
requireAssistantReply: false,
|
|
7724
|
+
requireCompletedCall: false,
|
|
7725
|
+
requireTranscript: false,
|
|
7726
|
+
requireTurn: false
|
|
7727
|
+
}
|
|
7728
|
+
}).summary;
|
|
7729
|
+
const providerErrors = {};
|
|
7730
|
+
const providers = new Set;
|
|
7731
|
+
let latestOutcome;
|
|
7732
|
+
let errorCount = 0;
|
|
7733
|
+
for (const event of sorted) {
|
|
7734
|
+
const provider = getString4(event.payload.provider);
|
|
7735
|
+
if (provider) {
|
|
7736
|
+
providers.add(provider);
|
|
7737
|
+
}
|
|
7738
|
+
if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
|
|
7739
|
+
errorCount += 1;
|
|
7740
|
+
increment2(providerErrors, provider ?? "unknown");
|
|
7741
|
+
}
|
|
7742
|
+
const outcome = getString4(event.payload.outcome);
|
|
7743
|
+
if (outcome) {
|
|
7744
|
+
latestOutcome = outcome;
|
|
7745
|
+
}
|
|
7746
|
+
}
|
|
7747
|
+
const item = {
|
|
7748
|
+
endedAt: summary.endedAt,
|
|
7749
|
+
errorCount,
|
|
7750
|
+
eventCount: summary.eventCount,
|
|
7751
|
+
latestOutcome,
|
|
7752
|
+
providerErrors,
|
|
7753
|
+
providers: [...providers].sort(),
|
|
7754
|
+
sessionId,
|
|
7755
|
+
startedAt: summary.startedAt,
|
|
7756
|
+
status: errorCount > 0 ? "failed" : "healthy",
|
|
7757
|
+
transcriptCount: summary.transcriptCount,
|
|
7758
|
+
turnCount: summary.turnCount
|
|
7759
|
+
};
|
|
7760
|
+
const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
|
|
7761
|
+
return {
|
|
7762
|
+
...item,
|
|
7763
|
+
replayHref
|
|
7764
|
+
};
|
|
7765
|
+
});
|
|
7766
|
+
const search = options.q?.trim().toLowerCase();
|
|
7767
|
+
return sessions.filter((session) => {
|
|
7768
|
+
if (options.status && options.status !== "all" && session.status !== options.status) {
|
|
7769
|
+
return false;
|
|
7770
|
+
}
|
|
7771
|
+
if (options.provider && !session.providers.includes(options.provider)) {
|
|
7772
|
+
return false;
|
|
7773
|
+
}
|
|
7774
|
+
if (!search) {
|
|
7775
|
+
return true;
|
|
7776
|
+
}
|
|
7777
|
+
return [
|
|
7778
|
+
session.sessionId,
|
|
7779
|
+
session.latestOutcome,
|
|
7780
|
+
session.status,
|
|
7781
|
+
...session.providers
|
|
7782
|
+
].some((value) => value?.toLowerCase().includes(search));
|
|
7783
|
+
}).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
|
|
7784
|
+
};
|
|
7785
|
+
var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
|
|
7786
|
+
'<div class="voice-sessions-list">',
|
|
7787
|
+
...sessions.map((session) => [
|
|
7788
|
+
`<article class="voice-session-card ${escapeHtml7(session.status)}">`,
|
|
7789
|
+
'<div class="voice-session-card-header">',
|
|
7790
|
+
`<strong>${escapeHtml7(session.sessionId)}</strong>`,
|
|
7791
|
+
`<span>${escapeHtml7(session.status)}</span>`,
|
|
7792
|
+
"</div>",
|
|
7793
|
+
"<dl>",
|
|
7794
|
+
`<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
|
|
7795
|
+
`<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
|
|
7796
|
+
`<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
|
|
7797
|
+
`<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
|
|
7798
|
+
"</dl>",
|
|
7799
|
+
session.latestOutcome ? `<p>Outcome: ${escapeHtml7(session.latestOutcome)}</p>` : "",
|
|
7800
|
+
session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml7).join(", ")}</p>` : "",
|
|
7801
|
+
session.replayHref ? `<p><a href="${escapeHtml7(session.replayHref)}">Open replay</a></p>` : "",
|
|
7802
|
+
"</article>"
|
|
7803
|
+
].join("")),
|
|
7804
|
+
"</div>"
|
|
7805
|
+
].join("");
|
|
7806
|
+
var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
|
|
7807
|
+
...options,
|
|
7808
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
7809
|
+
provider: query?.provider ?? options.provider,
|
|
7810
|
+
q: query?.q ?? options.q,
|
|
7811
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
7812
|
+
});
|
|
7813
|
+
var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
|
|
7814
|
+
const sessions = await summarizeVoiceSessions({
|
|
7815
|
+
...options,
|
|
7816
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
7817
|
+
provider: query?.provider ?? options.provider,
|
|
7818
|
+
q: query?.q ?? options.q,
|
|
7819
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
7820
|
+
});
|
|
7821
|
+
const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
|
|
7822
|
+
return new Response(body, {
|
|
7823
|
+
headers: {
|
|
7824
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7825
|
+
...options.headers
|
|
7826
|
+
}
|
|
7827
|
+
});
|
|
7828
|
+
};
|
|
7829
|
+
var createVoiceSessionListRoutes = (options = {}) => {
|
|
7830
|
+
const path = options.path ?? "/api/voice-sessions";
|
|
7831
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
7832
|
+
const routes = new Elysia5({
|
|
7833
|
+
name: options.name ?? "absolutejs-voice-session-list"
|
|
7834
|
+
}).get(path, createVoiceSessionsJSONHandler(options));
|
|
7835
|
+
if (htmlPath) {
|
|
7836
|
+
routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
|
|
7837
|
+
}
|
|
7838
|
+
return routes;
|
|
7839
|
+
};
|
|
7840
|
+
var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
|
|
7841
|
+
...options,
|
|
7842
|
+
sessionId: params.sessionId ?? ""
|
|
7843
|
+
});
|
|
7844
|
+
var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
|
|
7845
|
+
const replay = await summarizeVoiceSessionReplay({
|
|
7846
|
+
...options,
|
|
7847
|
+
sessionId: params.sessionId ?? ""
|
|
7848
|
+
});
|
|
7849
|
+
const body = await (options.render?.(replay) ?? replay.html);
|
|
7850
|
+
return new Response(body, {
|
|
7851
|
+
headers: {
|
|
7852
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7853
|
+
...options.headers
|
|
7854
|
+
}
|
|
7855
|
+
});
|
|
7856
|
+
};
|
|
7857
|
+
var createVoiceSessionReplayRoutes = (options) => {
|
|
7858
|
+
const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
|
|
7859
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
7860
|
+
const routes = new Elysia5({
|
|
7861
|
+
name: options.name ?? "absolutejs-voice-session-replay"
|
|
7862
|
+
}).get(path, createVoiceSessionReplayJSONHandler(options));
|
|
7863
|
+
if (htmlPath) {
|
|
7864
|
+
routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
|
|
7865
|
+
}
|
|
7866
|
+
return routes;
|
|
7867
|
+
};
|
|
6761
7868
|
// src/fileStore.ts
|
|
7869
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
7870
|
+
import { join } from "path";
|
|
6762
7871
|
var listJsonFiles = async (directory) => {
|
|
6763
7872
|
try {
|
|
6764
7873
|
const entries = await readdir(directory, {
|
|
@@ -7006,73 +8115,2021 @@ var createVoiceFileAssistantMemoryStore = (options) => {
|
|
|
7006
8115
|
throw error;
|
|
7007
8116
|
}
|
|
7008
8117
|
};
|
|
7009
|
-
const list = async (input) => {
|
|
7010
|
-
const files = await listJsonFiles(options.directory);
|
|
7011
|
-
const records = await Promise.all(files.map((file) => readJsonFile(file)));
|
|
7012
|
-
return records.filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt);
|
|
7013
|
-
};
|
|
7014
|
-
const set = async (input) => {
|
|
7015
|
-
const existing = await get(input);
|
|
7016
|
-
const record = createVoiceAssistantMemoryRecord({
|
|
7017
|
-
...input,
|
|
7018
|
-
createdAt: input.createdAt ?? existing?.createdAt,
|
|
7019
|
-
updatedAt: input.updatedAt
|
|
7020
|
-
});
|
|
7021
|
-
await writeJsonFile(resolveFilePath(options.directory, createMemoryStoreId(record)), record, options);
|
|
7022
|
-
return record;
|
|
7023
|
-
};
|
|
7024
|
-
const remove = async (input) => {
|
|
7025
|
-
await rm(resolveFilePath(options.directory, createMemoryStoreId(input)), {
|
|
7026
|
-
force: true
|
|
7027
|
-
});
|
|
7028
|
-
};
|
|
7029
|
-
return { delete: remove, get, list, set };
|
|
8118
|
+
const list = async (input) => {
|
|
8119
|
+
const files = await listJsonFiles(options.directory);
|
|
8120
|
+
const records = await Promise.all(files.map((file) => readJsonFile(file)));
|
|
8121
|
+
return records.filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt);
|
|
8122
|
+
};
|
|
8123
|
+
const set = async (input) => {
|
|
8124
|
+
const existing = await get(input);
|
|
8125
|
+
const record = createVoiceAssistantMemoryRecord({
|
|
8126
|
+
...input,
|
|
8127
|
+
createdAt: input.createdAt ?? existing?.createdAt,
|
|
8128
|
+
updatedAt: input.updatedAt
|
|
8129
|
+
});
|
|
8130
|
+
await writeJsonFile(resolveFilePath(options.directory, createMemoryStoreId(record)), record, options);
|
|
8131
|
+
return record;
|
|
8132
|
+
};
|
|
8133
|
+
const remove = async (input) => {
|
|
8134
|
+
await rm(resolveFilePath(options.directory, createMemoryStoreId(input)), {
|
|
8135
|
+
force: true
|
|
8136
|
+
});
|
|
8137
|
+
};
|
|
8138
|
+
return { delete: remove, get, list, set };
|
|
8139
|
+
};
|
|
8140
|
+
var createVoiceFileRuntimeStorage = (options) => ({
|
|
8141
|
+
events: createVoiceFileIntegrationEventStore({
|
|
8142
|
+
...options,
|
|
8143
|
+
directory: join(options.directory, "events")
|
|
8144
|
+
}),
|
|
8145
|
+
externalObjects: createVoiceFileExternalObjectMapStore({
|
|
8146
|
+
...options,
|
|
8147
|
+
directory: join(options.directory, "external-objects")
|
|
8148
|
+
}),
|
|
8149
|
+
memories: createVoiceFileAssistantMemoryStore({
|
|
8150
|
+
...options,
|
|
8151
|
+
directory: join(options.directory, "memories")
|
|
8152
|
+
}),
|
|
8153
|
+
reviews: createVoiceFileReviewStore({
|
|
8154
|
+
...options,
|
|
8155
|
+
directory: join(options.directory, "reviews")
|
|
8156
|
+
}),
|
|
8157
|
+
session: createVoiceFileSessionStore({
|
|
8158
|
+
...options,
|
|
8159
|
+
directory: join(options.directory, "sessions")
|
|
8160
|
+
}),
|
|
8161
|
+
tasks: createVoiceFileTaskStore({
|
|
8162
|
+
...options,
|
|
8163
|
+
directory: join(options.directory, "tasks")
|
|
8164
|
+
}),
|
|
8165
|
+
traceDeliveries: createVoiceFileTraceSinkDeliveryStore({
|
|
8166
|
+
...options,
|
|
8167
|
+
directory: join(options.directory, "trace-deliveries")
|
|
8168
|
+
}),
|
|
8169
|
+
traces: createVoiceFileTraceEventStore({
|
|
8170
|
+
...options,
|
|
8171
|
+
directory: join(options.directory, "traces")
|
|
8172
|
+
})
|
|
8173
|
+
});
|
|
8174
|
+
var createStoredVoiceCallReviewArtifact = (id, artifact) => withVoiceCallReviewId(id, artifact);
|
|
8175
|
+
var createStoredVoiceOpsTask = (id, task) => withVoiceOpsTaskId(id, task);
|
|
8176
|
+
var createStoredVoiceIntegrationEvent = (id, event) => withVoiceIntegrationEventId(id, event);
|
|
8177
|
+
var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectMap({
|
|
8178
|
+
at: mapping.at,
|
|
8179
|
+
externalId: mapping.externalId,
|
|
8180
|
+
provider: mapping.provider,
|
|
8181
|
+
sinkId: mapping.sinkId,
|
|
8182
|
+
sourceId: mapping.sourceId,
|
|
8183
|
+
sourceType: mapping.sourceType
|
|
8184
|
+
});
|
|
8185
|
+
// src/modelAdapters.ts
|
|
8186
|
+
var OUTPUT_SCHEMA = {
|
|
8187
|
+
additionalProperties: false,
|
|
8188
|
+
properties: {
|
|
8189
|
+
assistantText: {
|
|
8190
|
+
type: "string"
|
|
8191
|
+
},
|
|
8192
|
+
complete: {
|
|
8193
|
+
type: "boolean"
|
|
8194
|
+
},
|
|
8195
|
+
escalate: {
|
|
8196
|
+
additionalProperties: false,
|
|
8197
|
+
properties: {
|
|
8198
|
+
metadata: {
|
|
8199
|
+
additionalProperties: true,
|
|
8200
|
+
type: "object"
|
|
8201
|
+
},
|
|
8202
|
+
reason: {
|
|
8203
|
+
type: "string"
|
|
8204
|
+
}
|
|
8205
|
+
},
|
|
8206
|
+
required: ["reason"],
|
|
8207
|
+
type: "object"
|
|
8208
|
+
},
|
|
8209
|
+
noAnswer: {
|
|
8210
|
+
additionalProperties: false,
|
|
8211
|
+
properties: {
|
|
8212
|
+
metadata: {
|
|
8213
|
+
additionalProperties: true,
|
|
8214
|
+
type: "object"
|
|
8215
|
+
}
|
|
8216
|
+
},
|
|
8217
|
+
type: "object"
|
|
8218
|
+
},
|
|
8219
|
+
result: {
|
|
8220
|
+
additionalProperties: true,
|
|
8221
|
+
type: "object"
|
|
8222
|
+
},
|
|
8223
|
+
transfer: {
|
|
8224
|
+
additionalProperties: false,
|
|
8225
|
+
properties: {
|
|
8226
|
+
metadata: {
|
|
8227
|
+
additionalProperties: true,
|
|
8228
|
+
type: "object"
|
|
8229
|
+
},
|
|
8230
|
+
reason: {
|
|
8231
|
+
type: "string"
|
|
8232
|
+
},
|
|
8233
|
+
target: {
|
|
8234
|
+
type: "string"
|
|
8235
|
+
}
|
|
8236
|
+
},
|
|
8237
|
+
required: ["target"],
|
|
8238
|
+
type: "object"
|
|
8239
|
+
},
|
|
8240
|
+
voicemail: {
|
|
8241
|
+
additionalProperties: false,
|
|
8242
|
+
properties: {
|
|
8243
|
+
metadata: {
|
|
8244
|
+
additionalProperties: true,
|
|
8245
|
+
type: "object"
|
|
8246
|
+
}
|
|
8247
|
+
},
|
|
8248
|
+
type: "object"
|
|
8249
|
+
}
|
|
8250
|
+
},
|
|
8251
|
+
type: "object"
|
|
8252
|
+
};
|
|
8253
|
+
var ROUTE_RESULT_INSTRUCTION = "Return only a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools. Only set transfer, escalate, voicemail, or noAnswer when the user explicitly asks for that lifecycle outcome or a tool result says that exact outcome. Do not infer voicemail from generic words like voice, voice app, or voice integration.";
|
|
8254
|
+
var stripJSONCodeFence = (value) => {
|
|
8255
|
+
const trimmed = value.trim();
|
|
8256
|
+
const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
8257
|
+
return match?.[1]?.trim() ?? value;
|
|
8258
|
+
};
|
|
8259
|
+
var parseJSON = (value) => {
|
|
8260
|
+
try {
|
|
8261
|
+
const parsed = JSON.parse(stripJSONCodeFence(value));
|
|
8262
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
8263
|
+
} catch {
|
|
8264
|
+
return {
|
|
8265
|
+
assistantText: value
|
|
8266
|
+
};
|
|
8267
|
+
}
|
|
8268
|
+
};
|
|
8269
|
+
var parseJSONValue = (value) => {
|
|
8270
|
+
try {
|
|
8271
|
+
return JSON.parse(value);
|
|
8272
|
+
} catch {
|
|
8273
|
+
return value;
|
|
8274
|
+
}
|
|
8275
|
+
};
|
|
8276
|
+
|
|
8277
|
+
class VoiceProviderTimeoutError extends Error {
|
|
8278
|
+
provider;
|
|
8279
|
+
timeoutMs;
|
|
8280
|
+
constructor(provider, timeoutMs) {
|
|
8281
|
+
super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
|
|
8282
|
+
this.name = "VoiceProviderTimeoutError";
|
|
8283
|
+
this.provider = provider;
|
|
8284
|
+
this.timeoutMs = timeoutMs;
|
|
8285
|
+
}
|
|
8286
|
+
}
|
|
8287
|
+
var getMessageToolCalls = (message) => {
|
|
8288
|
+
const toolCalls = message.metadata?.toolCalls;
|
|
8289
|
+
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
8290
|
+
};
|
|
8291
|
+
var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
|
|
8292
|
+
var sleep4 = (ms) => new Promise((resolve2) => {
|
|
8293
|
+
setTimeout(resolve2, ms);
|
|
8294
|
+
});
|
|
8295
|
+
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
8296
|
+
var defaultIsRateLimitError = (error) => /(\b429\b|rate limit|quota|too many requests)/i.test(errorMessage(error));
|
|
8297
|
+
var normalizeRouteOutput = (output) => {
|
|
8298
|
+
const result = {};
|
|
8299
|
+
if (typeof output.assistantText === "string") {
|
|
8300
|
+
result.assistantText = output.assistantText;
|
|
8301
|
+
}
|
|
8302
|
+
if (typeof output.complete === "boolean") {
|
|
8303
|
+
result.complete = output.complete;
|
|
8304
|
+
}
|
|
8305
|
+
if (output.result !== undefined) {
|
|
8306
|
+
result.result = output.result;
|
|
8307
|
+
}
|
|
8308
|
+
if (output.transfer && typeof output.transfer === "object") {
|
|
8309
|
+
const transfer = output.transfer;
|
|
8310
|
+
if (typeof transfer.target === "string") {
|
|
8311
|
+
result.transfer = {
|
|
8312
|
+
metadata: transfer.metadata && typeof transfer.metadata === "object" ? transfer.metadata : undefined,
|
|
8313
|
+
reason: typeof transfer.reason === "string" ? transfer.reason : undefined,
|
|
8314
|
+
target: transfer.target
|
|
8315
|
+
};
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
if (output.escalate && typeof output.escalate === "object") {
|
|
8319
|
+
const escalate = output.escalate;
|
|
8320
|
+
if (typeof escalate.reason === "string") {
|
|
8321
|
+
result.escalate = {
|
|
8322
|
+
metadata: escalate.metadata && typeof escalate.metadata === "object" ? escalate.metadata : undefined,
|
|
8323
|
+
reason: escalate.reason
|
|
8324
|
+
};
|
|
8325
|
+
}
|
|
8326
|
+
}
|
|
8327
|
+
if (output.voicemail && typeof output.voicemail === "object") {
|
|
8328
|
+
const voicemail = output.voicemail;
|
|
8329
|
+
result.voicemail = {
|
|
8330
|
+
metadata: voicemail.metadata && typeof voicemail.metadata === "object" ? voicemail.metadata : undefined
|
|
8331
|
+
};
|
|
8332
|
+
}
|
|
8333
|
+
if (output.noAnswer && typeof output.noAnswer === "object") {
|
|
8334
|
+
const noAnswer = output.noAnswer;
|
|
8335
|
+
result.noAnswer = {
|
|
8336
|
+
metadata: noAnswer.metadata && typeof noAnswer.metadata === "object" ? noAnswer.metadata : undefined
|
|
8337
|
+
};
|
|
8338
|
+
}
|
|
8339
|
+
return result;
|
|
8340
|
+
};
|
|
8341
|
+
var createJSONVoiceAssistantModel = (options) => ({
|
|
8342
|
+
generate: async (input) => {
|
|
8343
|
+
const output = await options.generate(input);
|
|
8344
|
+
if ("assistantText" in output || "toolCalls" in output || "complete" in output || "transfer" in output || "escalate" in output) {
|
|
8345
|
+
return output;
|
|
8346
|
+
}
|
|
8347
|
+
return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
|
|
8348
|
+
}
|
|
8349
|
+
});
|
|
8350
|
+
var createVoiceProviderRouter = (options) => {
|
|
8351
|
+
const providerIds = Object.keys(options.providers);
|
|
8352
|
+
const firstProvider = providerIds[0];
|
|
8353
|
+
const policy = typeof options.policy === "string" ? {
|
|
8354
|
+
strategy: options.policy
|
|
8355
|
+
} : options.policy;
|
|
8356
|
+
const strategy = policy?.strategy ?? "prefer-selected";
|
|
8357
|
+
const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
|
|
8358
|
+
const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
|
|
8359
|
+
const healthState = new Map;
|
|
8360
|
+
const now = () => healthOptions?.now?.() ?? Date.now();
|
|
8361
|
+
const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
|
|
8362
|
+
const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
|
|
8363
|
+
const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
|
|
8364
|
+
const getProviderTimeoutMs = (provider) => {
|
|
8365
|
+
const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
|
|
8366
|
+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
|
|
8367
|
+
};
|
|
8368
|
+
const getHealth = (provider) => {
|
|
8369
|
+
const existing = healthState.get(provider);
|
|
8370
|
+
if (existing) {
|
|
8371
|
+
return existing;
|
|
8372
|
+
}
|
|
8373
|
+
const next = {
|
|
8374
|
+
consecutiveFailures: 0,
|
|
8375
|
+
provider,
|
|
8376
|
+
status: "healthy"
|
|
8377
|
+
};
|
|
8378
|
+
healthState.set(provider, next);
|
|
8379
|
+
return next;
|
|
8380
|
+
};
|
|
8381
|
+
const cloneHealth = (provider) => {
|
|
8382
|
+
if (!healthOptions) {
|
|
8383
|
+
return;
|
|
8384
|
+
}
|
|
8385
|
+
return {
|
|
8386
|
+
...getHealth(provider)
|
|
8387
|
+
};
|
|
8388
|
+
};
|
|
8389
|
+
const getSuppressionRemainingMs = (provider) => {
|
|
8390
|
+
if (!healthOptions) {
|
|
8391
|
+
return;
|
|
8392
|
+
}
|
|
8393
|
+
const suppressedUntil = getHealth(provider).suppressedUntil;
|
|
8394
|
+
return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
|
|
8395
|
+
};
|
|
8396
|
+
const isSuppressed = (provider) => {
|
|
8397
|
+
if (!healthOptions) {
|
|
8398
|
+
return false;
|
|
8399
|
+
}
|
|
8400
|
+
const health = getHealth(provider);
|
|
8401
|
+
return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
|
|
8402
|
+
};
|
|
8403
|
+
const recordProviderSuccess = (provider) => {
|
|
8404
|
+
if (!healthOptions) {
|
|
8405
|
+
return;
|
|
8406
|
+
}
|
|
8407
|
+
const health = getHealth(provider);
|
|
8408
|
+
health.consecutiveFailures = 0;
|
|
8409
|
+
health.status = "healthy";
|
|
8410
|
+
health.suppressedUntil = undefined;
|
|
8411
|
+
return cloneHealth(provider);
|
|
8412
|
+
};
|
|
8413
|
+
const recordProviderError = (provider, isProviderError, rateLimited) => {
|
|
8414
|
+
if (!healthOptions || !isProviderError) {
|
|
8415
|
+
return cloneHealth(provider);
|
|
8416
|
+
}
|
|
8417
|
+
const currentTime = now();
|
|
8418
|
+
const health = getHealth(provider);
|
|
8419
|
+
health.consecutiveFailures += 1;
|
|
8420
|
+
health.lastFailureAt = currentTime;
|
|
8421
|
+
if (rateLimited) {
|
|
8422
|
+
health.lastRateLimitedAt = currentTime;
|
|
8423
|
+
}
|
|
8424
|
+
if (rateLimited || health.consecutiveFailures >= failureThreshold) {
|
|
8425
|
+
health.status = "suppressed";
|
|
8426
|
+
health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
|
|
8427
|
+
}
|
|
8428
|
+
return cloneHealth(provider);
|
|
8429
|
+
};
|
|
8430
|
+
const resolveAllowedProviders = async (input) => {
|
|
8431
|
+
const allowProviders = policy?.allowProviders ?? options.allowProviders;
|
|
8432
|
+
const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
|
|
8433
|
+
return new Set(allowed ?? providerIds);
|
|
8434
|
+
};
|
|
8435
|
+
const sortProviders = (providers) => {
|
|
8436
|
+
if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
|
|
8437
|
+
return providers;
|
|
8438
|
+
}
|
|
8439
|
+
return [...providers].sort((left, right) => {
|
|
8440
|
+
const leftProfile = options.providerProfiles?.[left];
|
|
8441
|
+
const rightProfile = options.providerProfiles?.[right];
|
|
8442
|
+
const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
8443
|
+
const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
8444
|
+
return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
|
|
8445
|
+
});
|
|
8446
|
+
};
|
|
8447
|
+
const resolveOrder = async (input) => {
|
|
8448
|
+
const selectedProvider = await options.selectProvider?.(input);
|
|
8449
|
+
const allowedProviders = await resolveAllowedProviders(input);
|
|
8450
|
+
const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
|
|
8451
|
+
const rankedProviders = sortProviders([
|
|
8452
|
+
...fallbackOrder ?? providerIds
|
|
8453
|
+
]).filter((provider) => allowedProviders.has(provider));
|
|
8454
|
+
const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
|
|
8455
|
+
const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
|
|
8456
|
+
const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
|
|
8457
|
+
const seen = new Set;
|
|
8458
|
+
const order = [];
|
|
8459
|
+
const candidates = strategy === "ordered" ? candidateRankedProviders : [
|
|
8460
|
+
preferred,
|
|
8461
|
+
...candidateRankedProviders,
|
|
8462
|
+
...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
|
|
8463
|
+
];
|
|
8464
|
+
for (const provider of candidates) {
|
|
8465
|
+
if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
|
|
8466
|
+
continue;
|
|
8467
|
+
}
|
|
8468
|
+
seen.add(provider);
|
|
8469
|
+
order.push(provider);
|
|
8470
|
+
}
|
|
8471
|
+
return {
|
|
8472
|
+
order,
|
|
8473
|
+
selectedProvider: preferred
|
|
8474
|
+
};
|
|
8475
|
+
};
|
|
8476
|
+
const emit = async (event, input) => {
|
|
8477
|
+
await options.onProviderEvent?.(event, input);
|
|
8478
|
+
};
|
|
8479
|
+
const runProvider = async (provider, model, input) => {
|
|
8480
|
+
const timeoutMs = getProviderTimeoutMs(provider);
|
|
8481
|
+
if (!timeoutMs) {
|
|
8482
|
+
return model.generate(input);
|
|
8483
|
+
}
|
|
8484
|
+
let timeout;
|
|
8485
|
+
try {
|
|
8486
|
+
return await Promise.race([
|
|
8487
|
+
model.generate(input),
|
|
8488
|
+
new Promise((_, reject) => {
|
|
8489
|
+
timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
|
|
8490
|
+
})
|
|
8491
|
+
]);
|
|
8492
|
+
} finally {
|
|
8493
|
+
if (timeout) {
|
|
8494
|
+
clearTimeout(timeout);
|
|
8495
|
+
}
|
|
8496
|
+
}
|
|
8497
|
+
};
|
|
8498
|
+
return {
|
|
8499
|
+
generate: async (input) => {
|
|
8500
|
+
const { order, selectedProvider } = await resolveOrder(input);
|
|
8501
|
+
if (!selectedProvider || order.length === 0) {
|
|
8502
|
+
throw new Error("Voice provider router has no available providers.");
|
|
8503
|
+
}
|
|
8504
|
+
let lastError;
|
|
8505
|
+
for (const [index, provider] of order.entries()) {
|
|
8506
|
+
const model = options.providers[provider];
|
|
8507
|
+
if (!model) {
|
|
8508
|
+
continue;
|
|
8509
|
+
}
|
|
8510
|
+
const startedAt = Date.now();
|
|
8511
|
+
try {
|
|
8512
|
+
const output = await runProvider(provider, model, input);
|
|
8513
|
+
const providerHealth = recordProviderSuccess(provider);
|
|
8514
|
+
await emit({
|
|
8515
|
+
at: Date.now(),
|
|
8516
|
+
attempt: index + 1,
|
|
8517
|
+
elapsedMs: Date.now() - startedAt,
|
|
8518
|
+
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
8519
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
8520
|
+
provider,
|
|
8521
|
+
providerHealth,
|
|
8522
|
+
recovered: provider !== selectedProvider,
|
|
8523
|
+
selectedProvider,
|
|
8524
|
+
status: provider === selectedProvider ? "success" : "fallback"
|
|
8525
|
+
}, input);
|
|
8526
|
+
return output;
|
|
8527
|
+
} catch (error) {
|
|
8528
|
+
lastError = error;
|
|
8529
|
+
const hasNextProvider = index < order.length - 1;
|
|
8530
|
+
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
8531
|
+
const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
|
|
8532
|
+
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
8533
|
+
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
8534
|
+
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
8535
|
+
const nextProvider = hasNextProvider ? order[index + 1] : undefined;
|
|
8536
|
+
await emit({
|
|
8537
|
+
at: Date.now(),
|
|
8538
|
+
attempt: index + 1,
|
|
8539
|
+
elapsedMs: Date.now() - startedAt,
|
|
8540
|
+
error: errorMessage(error),
|
|
8541
|
+
fallbackProvider: shouldFallback ? nextProvider : undefined,
|
|
8542
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
8543
|
+
provider,
|
|
8544
|
+
providerHealth,
|
|
8545
|
+
rateLimited,
|
|
8546
|
+
selectedProvider,
|
|
8547
|
+
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
8548
|
+
suppressedUntil: providerHealth?.suppressedUntil,
|
|
8549
|
+
status: "error",
|
|
8550
|
+
timedOut
|
|
8551
|
+
}, input);
|
|
8552
|
+
if (!hasNextProvider || !shouldFallback) {
|
|
8553
|
+
throw error;
|
|
8554
|
+
}
|
|
8555
|
+
}
|
|
8556
|
+
}
|
|
8557
|
+
throw lastError ?? new Error("Voice provider router did not run a provider.");
|
|
8558
|
+
}
|
|
8559
|
+
};
|
|
8560
|
+
};
|
|
8561
|
+
var messageToOpenAIInput = (message) => {
|
|
8562
|
+
if (message.role === "tool") {
|
|
8563
|
+
return [
|
|
8564
|
+
{
|
|
8565
|
+
call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
|
|
8566
|
+
output: message.content,
|
|
8567
|
+
type: "function_call_output"
|
|
8568
|
+
}
|
|
8569
|
+
];
|
|
8570
|
+
}
|
|
8571
|
+
const toolCalls = getMessageToolCalls(message);
|
|
8572
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
8573
|
+
return toolCalls.map((toolCall) => ({
|
|
8574
|
+
arguments: JSON.stringify(toolCall.args),
|
|
8575
|
+
call_id: toolCall.id ?? crypto.randomUUID(),
|
|
8576
|
+
name: toolCall.name,
|
|
8577
|
+
type: "function_call"
|
|
8578
|
+
}));
|
|
8579
|
+
}
|
|
8580
|
+
return [
|
|
8581
|
+
{
|
|
8582
|
+
content: message.content,
|
|
8583
|
+
role: message.role === "system" ? "developer" : message.role
|
|
8584
|
+
}
|
|
8585
|
+
];
|
|
8586
|
+
};
|
|
8587
|
+
var messagesToOpenAIInput = (messages) => messages.flatMap(messageToOpenAIInput);
|
|
8588
|
+
var messageToAnthropicMessage = (message) => {
|
|
8589
|
+
if (message.role === "system") {
|
|
8590
|
+
return;
|
|
8591
|
+
}
|
|
8592
|
+
if (message.role === "tool") {
|
|
8593
|
+
if (!message.toolCallId) {
|
|
8594
|
+
return {
|
|
8595
|
+
content: `Tool result from ${message.name ?? "tool"}: ${message.content}`,
|
|
8596
|
+
role: "user"
|
|
8597
|
+
};
|
|
8598
|
+
}
|
|
8599
|
+
return {
|
|
8600
|
+
content: [
|
|
8601
|
+
{
|
|
8602
|
+
content: message.content,
|
|
8603
|
+
tool_use_id: message.toolCallId,
|
|
8604
|
+
type: "tool_result"
|
|
8605
|
+
}
|
|
8606
|
+
],
|
|
8607
|
+
role: "user"
|
|
8608
|
+
};
|
|
8609
|
+
}
|
|
8610
|
+
const toolCalls = getMessageToolCalls(message);
|
|
8611
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
8612
|
+
return {
|
|
8613
|
+
content: [
|
|
8614
|
+
...message.content ? [
|
|
8615
|
+
{
|
|
8616
|
+
text: message.content,
|
|
8617
|
+
type: "text"
|
|
8618
|
+
}
|
|
8619
|
+
] : [],
|
|
8620
|
+
...toolCalls.map((toolCall) => ({
|
|
8621
|
+
id: toolCall.id ?? crypto.randomUUID(),
|
|
8622
|
+
input: toolCall.args,
|
|
8623
|
+
name: toolCall.name,
|
|
8624
|
+
type: "tool_use"
|
|
8625
|
+
}))
|
|
8626
|
+
],
|
|
8627
|
+
role: "assistant"
|
|
8628
|
+
};
|
|
8629
|
+
}
|
|
8630
|
+
return {
|
|
8631
|
+
content: message.content,
|
|
8632
|
+
role: message.role
|
|
8633
|
+
};
|
|
8634
|
+
};
|
|
8635
|
+
var toGeminiSchema = (schema) => {
|
|
8636
|
+
const next = {};
|
|
8637
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
8638
|
+
if (key === "additionalProperties") {
|
|
8639
|
+
continue;
|
|
8640
|
+
}
|
|
8641
|
+
if (key === "type" && typeof value === "string") {
|
|
8642
|
+
next[key] = value.toUpperCase();
|
|
8643
|
+
continue;
|
|
8644
|
+
}
|
|
8645
|
+
if (Array.isArray(value)) {
|
|
8646
|
+
next[key] = value.map((item) => item && typeof item === "object" ? toGeminiSchema(item) : item);
|
|
8647
|
+
continue;
|
|
8648
|
+
}
|
|
8649
|
+
if (value && typeof value === "object") {
|
|
8650
|
+
next[key] = toGeminiSchema(value);
|
|
8651
|
+
continue;
|
|
8652
|
+
}
|
|
8653
|
+
next[key] = value;
|
|
8654
|
+
}
|
|
8655
|
+
return next;
|
|
8656
|
+
};
|
|
8657
|
+
var messageToGeminiContent = (message) => {
|
|
8658
|
+
if (message.role === "system") {
|
|
8659
|
+
return;
|
|
8660
|
+
}
|
|
8661
|
+
if (message.role === "tool") {
|
|
8662
|
+
return {
|
|
8663
|
+
parts: [
|
|
8664
|
+
{
|
|
8665
|
+
functionResponse: {
|
|
8666
|
+
id: message.toolCallId,
|
|
8667
|
+
name: message.name ?? "tool",
|
|
8668
|
+
response: {
|
|
8669
|
+
result: parseJSONValue(message.content)
|
|
8670
|
+
}
|
|
8671
|
+
}
|
|
8672
|
+
}
|
|
8673
|
+
],
|
|
8674
|
+
role: "user"
|
|
8675
|
+
};
|
|
8676
|
+
}
|
|
8677
|
+
const toolCalls = getMessageToolCalls(message);
|
|
8678
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
8679
|
+
return {
|
|
8680
|
+
parts: [
|
|
8681
|
+
...message.content ? [
|
|
8682
|
+
{
|
|
8683
|
+
text: message.content
|
|
8684
|
+
}
|
|
8685
|
+
] : [],
|
|
8686
|
+
...toolCalls.map((toolCall) => ({
|
|
8687
|
+
functionCall: {
|
|
8688
|
+
args: toolCall.args,
|
|
8689
|
+
id: toolCall.id,
|
|
8690
|
+
name: toolCall.name
|
|
8691
|
+
}
|
|
8692
|
+
}))
|
|
8693
|
+
],
|
|
8694
|
+
role: "model"
|
|
8695
|
+
};
|
|
8696
|
+
}
|
|
8697
|
+
return {
|
|
8698
|
+
parts: [
|
|
8699
|
+
{
|
|
8700
|
+
text: message.content
|
|
8701
|
+
}
|
|
8702
|
+
],
|
|
8703
|
+
role: message.role === "assistant" ? "model" : "user"
|
|
8704
|
+
};
|
|
8705
|
+
};
|
|
8706
|
+
var extractText = (response) => {
|
|
8707
|
+
if (typeof response.output_text === "string") {
|
|
8708
|
+
return response.output_text;
|
|
8709
|
+
}
|
|
8710
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
8711
|
+
for (const item of output) {
|
|
8712
|
+
if (!item || typeof item !== "object") {
|
|
8713
|
+
continue;
|
|
8714
|
+
}
|
|
8715
|
+
const record = item;
|
|
8716
|
+
const content = Array.isArray(record.content) ? record.content : [];
|
|
8717
|
+
for (const contentItem of content) {
|
|
8718
|
+
if (!contentItem || typeof contentItem !== "object") {
|
|
8719
|
+
continue;
|
|
8720
|
+
}
|
|
8721
|
+
const contentRecord = contentItem;
|
|
8722
|
+
if (typeof contentRecord.text === "string") {
|
|
8723
|
+
return contentRecord.text;
|
|
8724
|
+
}
|
|
8725
|
+
}
|
|
8726
|
+
}
|
|
8727
|
+
return "";
|
|
8728
|
+
};
|
|
8729
|
+
var extractToolCalls = (response) => {
|
|
8730
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
8731
|
+
const toolCalls = [];
|
|
8732
|
+
for (const item of output) {
|
|
8733
|
+
if (!item || typeof item !== "object") {
|
|
8734
|
+
continue;
|
|
8735
|
+
}
|
|
8736
|
+
const record = item;
|
|
8737
|
+
if (record.type !== "function_call" || typeof record.name !== "string") {
|
|
8738
|
+
continue;
|
|
8739
|
+
}
|
|
8740
|
+
const args = typeof record.arguments === "string" ? parseJSON(record.arguments) : {};
|
|
8741
|
+
toolCalls.push({
|
|
8742
|
+
args,
|
|
8743
|
+
id: typeof record.call_id === "string" ? record.call_id : typeof record.id === "string" ? record.id : undefined,
|
|
8744
|
+
name: record.name
|
|
8745
|
+
});
|
|
8746
|
+
}
|
|
8747
|
+
return toolCalls;
|
|
8748
|
+
};
|
|
8749
|
+
var createOpenAIVoiceAssistantModel = (options) => {
|
|
8750
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8751
|
+
const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
|
|
8752
|
+
const model = options.model ?? "gpt-4.1-mini";
|
|
8753
|
+
return {
|
|
8754
|
+
generate: async (input) => {
|
|
8755
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
|
|
8756
|
+
body: JSON.stringify({
|
|
8757
|
+
input: messagesToOpenAIInput(input.messages),
|
|
8758
|
+
instructions: [
|
|
8759
|
+
input.system,
|
|
8760
|
+
"Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools."
|
|
8761
|
+
].filter(Boolean).join(`
|
|
8762
|
+
|
|
8763
|
+
`),
|
|
8764
|
+
max_output_tokens: options.maxOutputTokens,
|
|
8765
|
+
model,
|
|
8766
|
+
temperature: options.temperature,
|
|
8767
|
+
text: {
|
|
8768
|
+
format: {
|
|
8769
|
+
name: "voice_route_result",
|
|
8770
|
+
schema: OUTPUT_SCHEMA,
|
|
8771
|
+
strict: false,
|
|
8772
|
+
type: "json_schema"
|
|
8773
|
+
}
|
|
8774
|
+
},
|
|
8775
|
+
tool_choice: input.tools.length ? "auto" : "none",
|
|
8776
|
+
tools: input.tools.map((tool) => ({
|
|
8777
|
+
description: tool.description,
|
|
8778
|
+
name: tool.name,
|
|
8779
|
+
parameters: tool.parameters ?? {
|
|
8780
|
+
additionalProperties: true,
|
|
8781
|
+
type: "object"
|
|
8782
|
+
},
|
|
8783
|
+
strict: false,
|
|
8784
|
+
type: "function"
|
|
8785
|
+
}))
|
|
8786
|
+
}),
|
|
8787
|
+
headers: {
|
|
8788
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
8789
|
+
"content-type": "application/json"
|
|
8790
|
+
},
|
|
8791
|
+
method: "POST"
|
|
8792
|
+
});
|
|
8793
|
+
if (!response.ok) {
|
|
8794
|
+
throw createHTTPError("OpenAI", response);
|
|
8795
|
+
}
|
|
8796
|
+
const body = await response.json();
|
|
8797
|
+
if (body.usage && typeof body.usage === "object") {
|
|
8798
|
+
await options.onUsage?.(body.usage);
|
|
8799
|
+
}
|
|
8800
|
+
const toolCalls = extractToolCalls(body);
|
|
8801
|
+
if (toolCalls.length) {
|
|
8802
|
+
return {
|
|
8803
|
+
toolCalls
|
|
8804
|
+
};
|
|
8805
|
+
}
|
|
8806
|
+
return normalizeRouteOutput(parseJSON(extractText(body)));
|
|
8807
|
+
}
|
|
8808
|
+
};
|
|
8809
|
+
};
|
|
8810
|
+
var extractAnthropicText = (response) => {
|
|
8811
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
8812
|
+
return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
|
|
8813
|
+
`);
|
|
8814
|
+
};
|
|
8815
|
+
var extractAnthropicToolCalls = (response) => {
|
|
8816
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
8817
|
+
const toolCalls = [];
|
|
8818
|
+
for (const item of content) {
|
|
8819
|
+
if (!item || typeof item !== "object") {
|
|
8820
|
+
continue;
|
|
8821
|
+
}
|
|
8822
|
+
const record = item;
|
|
8823
|
+
if (record.type !== "tool_use" || typeof record.name !== "string") {
|
|
8824
|
+
continue;
|
|
8825
|
+
}
|
|
8826
|
+
toolCalls.push({
|
|
8827
|
+
args: record.input && typeof record.input === "object" ? record.input : {},
|
|
8828
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
8829
|
+
name: record.name
|
|
8830
|
+
});
|
|
8831
|
+
}
|
|
8832
|
+
return toolCalls;
|
|
8833
|
+
};
|
|
8834
|
+
var createAnthropicVoiceAssistantModel = (options) => {
|
|
8835
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8836
|
+
const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
|
|
8837
|
+
const model = options.model ?? "claude-sonnet-4-5";
|
|
8838
|
+
return {
|
|
8839
|
+
generate: async (input) => {
|
|
8840
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
|
|
8841
|
+
body: JSON.stringify({
|
|
8842
|
+
max_tokens: options.maxOutputTokens ?? 1024,
|
|
8843
|
+
messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
|
|
8844
|
+
model,
|
|
8845
|
+
system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
8846
|
+
|
|
8847
|
+
`),
|
|
8848
|
+
temperature: options.temperature,
|
|
8849
|
+
tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
|
|
8850
|
+
tools: input.tools.map((tool) => ({
|
|
8851
|
+
description: tool.description,
|
|
8852
|
+
input_schema: tool.parameters ?? {
|
|
8853
|
+
additionalProperties: true,
|
|
8854
|
+
type: "object"
|
|
8855
|
+
},
|
|
8856
|
+
name: tool.name
|
|
8857
|
+
}))
|
|
8858
|
+
}),
|
|
8859
|
+
headers: {
|
|
8860
|
+
"anthropic-version": options.version ?? "2023-06-01",
|
|
8861
|
+
"content-type": "application/json",
|
|
8862
|
+
"x-api-key": options.apiKey
|
|
8863
|
+
},
|
|
8864
|
+
method: "POST"
|
|
8865
|
+
});
|
|
8866
|
+
if (!response.ok) {
|
|
8867
|
+
throw createHTTPError("Anthropic", response);
|
|
8868
|
+
}
|
|
8869
|
+
const body = await response.json();
|
|
8870
|
+
if (body.usage && typeof body.usage === "object") {
|
|
8871
|
+
await options.onUsage?.(body.usage);
|
|
8872
|
+
}
|
|
8873
|
+
const toolCalls = extractAnthropicToolCalls(body);
|
|
8874
|
+
if (toolCalls.length) {
|
|
8875
|
+
return {
|
|
8876
|
+
assistantText: extractAnthropicText(body) || undefined,
|
|
8877
|
+
toolCalls
|
|
8878
|
+
};
|
|
8879
|
+
}
|
|
8880
|
+
return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
|
|
8881
|
+
}
|
|
8882
|
+
};
|
|
8883
|
+
};
|
|
8884
|
+
var extractGeminiCandidateParts = (response) => {
|
|
8885
|
+
const candidates = Array.isArray(response.candidates) ? response.candidates : [];
|
|
8886
|
+
const first = candidates[0];
|
|
8887
|
+
if (!first || typeof first !== "object") {
|
|
8888
|
+
return [];
|
|
8889
|
+
}
|
|
8890
|
+
const content = first.content;
|
|
8891
|
+
if (!content || typeof content !== "object") {
|
|
8892
|
+
return [];
|
|
8893
|
+
}
|
|
8894
|
+
const parts = content.parts;
|
|
8895
|
+
return Array.isArray(parts) ? parts : [];
|
|
8896
|
+
};
|
|
8897
|
+
var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
8898
|
+
`);
|
|
8899
|
+
var extractGeminiToolCalls = (response) => {
|
|
8900
|
+
const toolCalls = [];
|
|
8901
|
+
for (const part of extractGeminiCandidateParts(response)) {
|
|
8902
|
+
if (!part || typeof part !== "object") {
|
|
8903
|
+
continue;
|
|
8904
|
+
}
|
|
8905
|
+
const functionCall = part.functionCall;
|
|
8906
|
+
if (!functionCall || typeof functionCall !== "object") {
|
|
8907
|
+
continue;
|
|
8908
|
+
}
|
|
8909
|
+
const record = functionCall;
|
|
8910
|
+
if (typeof record.name !== "string") {
|
|
8911
|
+
continue;
|
|
8912
|
+
}
|
|
8913
|
+
toolCalls.push({
|
|
8914
|
+
args: record.args && typeof record.args === "object" ? record.args : {},
|
|
8915
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
8916
|
+
name: record.name
|
|
8917
|
+
});
|
|
8918
|
+
}
|
|
8919
|
+
return toolCalls;
|
|
8920
|
+
};
|
|
8921
|
+
var createGeminiVoiceAssistantModel = (options) => {
|
|
8922
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8923
|
+
const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
8924
|
+
const model = options.model ?? "gemini-2.5-flash";
|
|
8925
|
+
const maxRetries = Math.max(0, options.maxRetries ?? 2);
|
|
8926
|
+
return {
|
|
8927
|
+
generate: async (input) => {
|
|
8928
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
|
|
8929
|
+
let response;
|
|
8930
|
+
for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
|
|
8931
|
+
response = await fetchImpl(endpoint, {
|
|
8932
|
+
body: JSON.stringify({
|
|
8933
|
+
contents: input.messages.map(messageToGeminiContent).filter(Boolean),
|
|
8934
|
+
generationConfig: {
|
|
8935
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
8936
|
+
...input.tools.length ? {} : {
|
|
8937
|
+
responseMimeType: "application/json",
|
|
8938
|
+
responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
|
|
8939
|
+
},
|
|
8940
|
+
temperature: options.temperature
|
|
8941
|
+
},
|
|
8942
|
+
systemInstruction: {
|
|
8943
|
+
parts: [
|
|
8944
|
+
{
|
|
8945
|
+
text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
8946
|
+
|
|
8947
|
+
`)
|
|
8948
|
+
}
|
|
8949
|
+
]
|
|
8950
|
+
},
|
|
8951
|
+
tools: input.tools.length ? [
|
|
8952
|
+
{
|
|
8953
|
+
functionDeclarations: input.tools.map((tool) => ({
|
|
8954
|
+
description: tool.description,
|
|
8955
|
+
name: tool.name,
|
|
8956
|
+
parameters: toGeminiSchema(tool.parameters ?? {
|
|
8957
|
+
additionalProperties: true,
|
|
8958
|
+
type: "object"
|
|
8959
|
+
})
|
|
8960
|
+
}))
|
|
8961
|
+
}
|
|
8962
|
+
] : undefined
|
|
8963
|
+
}),
|
|
8964
|
+
headers: {
|
|
8965
|
+
"content-type": "application/json"
|
|
8966
|
+
},
|
|
8967
|
+
method: "POST"
|
|
8968
|
+
});
|
|
8969
|
+
if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
|
|
8970
|
+
break;
|
|
8971
|
+
}
|
|
8972
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
8973
|
+
await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
|
|
8974
|
+
}
|
|
8975
|
+
if (!response) {
|
|
8976
|
+
throw new Error("Gemini voice assistant model failed: no response");
|
|
8977
|
+
}
|
|
8978
|
+
if (!response.ok) {
|
|
8979
|
+
throw createHTTPError("Gemini", response);
|
|
8980
|
+
}
|
|
8981
|
+
const body = await response.json();
|
|
8982
|
+
if (body.usageMetadata && typeof body.usageMetadata === "object") {
|
|
8983
|
+
await options.onUsage?.(body.usageMetadata);
|
|
8984
|
+
}
|
|
8985
|
+
const toolCalls = extractGeminiToolCalls(body);
|
|
8986
|
+
if (toolCalls.length) {
|
|
8987
|
+
return {
|
|
8988
|
+
assistantText: extractGeminiText(body) || undefined,
|
|
8989
|
+
toolCalls
|
|
8990
|
+
};
|
|
8991
|
+
}
|
|
8992
|
+
return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
|
|
8993
|
+
}
|
|
8994
|
+
};
|
|
8995
|
+
};
|
|
8996
|
+
// src/opsConsoleRoutes.ts
|
|
8997
|
+
import { Elysia as Elysia9 } from "elysia";
|
|
8998
|
+
|
|
8999
|
+
// src/handoffHealth.ts
|
|
9000
|
+
import { Elysia as Elysia6 } from "elysia";
|
|
9001
|
+
var escapeHtml8 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9002
|
+
var getString5 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9003
|
+
var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
|
|
9004
|
+
var increment3 = (record, key) => {
|
|
9005
|
+
record[key] = (record[key] ?? 0) + 1;
|
|
9006
|
+
};
|
|
9007
|
+
var normalizeDelivery = (adapterId, value) => {
|
|
9008
|
+
const record = value && typeof value === "object" ? value : {};
|
|
9009
|
+
return {
|
|
9010
|
+
adapterId: getString5(record.adapterId) ?? adapterId,
|
|
9011
|
+
adapterKind: getString5(record.adapterKind),
|
|
9012
|
+
deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
|
|
9013
|
+
deliveredTo: getString5(record.deliveredTo),
|
|
9014
|
+
error: getString5(record.error),
|
|
9015
|
+
status: isStatus(record.status) ? record.status : "failed"
|
|
9016
|
+
};
|
|
9017
|
+
};
|
|
9018
|
+
var normalizeDeliveries = (payload) => {
|
|
9019
|
+
const deliveries = payload.deliveries;
|
|
9020
|
+
if (!deliveries || typeof deliveries !== "object") {
|
|
9021
|
+
return [];
|
|
9022
|
+
}
|
|
9023
|
+
return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
|
|
9024
|
+
};
|
|
9025
|
+
var resolveReplayHref = (event, replayHref) => {
|
|
9026
|
+
if (replayHref === false) {
|
|
9027
|
+
return;
|
|
9028
|
+
}
|
|
9029
|
+
if (typeof replayHref === "function") {
|
|
9030
|
+
return replayHref(event);
|
|
9031
|
+
}
|
|
9032
|
+
return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
|
|
9033
|
+
};
|
|
9034
|
+
var summarizeVoiceHandoffHealth = async (options = {}) => {
|
|
9035
|
+
const sourceEvents = options.events ?? await options.store?.list() ?? [];
|
|
9036
|
+
const search = options.q?.trim().toLowerCase();
|
|
9037
|
+
const byAction = {};
|
|
9038
|
+
const byAdapter = {};
|
|
9039
|
+
const byStatus = {
|
|
9040
|
+
delivered: 0,
|
|
9041
|
+
failed: 0,
|
|
9042
|
+
skipped: 0
|
|
9043
|
+
};
|
|
9044
|
+
const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
|
|
9045
|
+
const status = isStatus(event.payload.status) ? event.payload.status : "failed";
|
|
9046
|
+
const deliveries = normalizeDeliveries(event.payload);
|
|
9047
|
+
const item = {
|
|
9048
|
+
action: getString5(event.payload.action),
|
|
9049
|
+
at: event.at,
|
|
9050
|
+
deliveries,
|
|
9051
|
+
reason: getString5(event.payload.reason),
|
|
9052
|
+
sessionId: event.sessionId,
|
|
9053
|
+
status,
|
|
9054
|
+
target: getString5(event.payload.target)
|
|
9055
|
+
};
|
|
9056
|
+
return {
|
|
9057
|
+
...item,
|
|
9058
|
+
replayHref: resolveReplayHref(item, options.replayHref)
|
|
9059
|
+
};
|
|
9060
|
+
}).filter((event) => {
|
|
9061
|
+
if (options.status && options.status !== "all" && event.status !== options.status) {
|
|
9062
|
+
return false;
|
|
9063
|
+
}
|
|
9064
|
+
if (!search) {
|
|
9065
|
+
return true;
|
|
9066
|
+
}
|
|
9067
|
+
return [
|
|
9068
|
+
event.action,
|
|
9069
|
+
event.reason,
|
|
9070
|
+
event.sessionId,
|
|
9071
|
+
event.status,
|
|
9072
|
+
event.target,
|
|
9073
|
+
...event.deliveries.flatMap((delivery) => [
|
|
9074
|
+
delivery.adapterId,
|
|
9075
|
+
delivery.adapterKind,
|
|
9076
|
+
delivery.deliveredTo,
|
|
9077
|
+
delivery.error,
|
|
9078
|
+
delivery.status
|
|
9079
|
+
])
|
|
9080
|
+
].some((value) => value?.toLowerCase().includes(search));
|
|
9081
|
+
}).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
|
|
9082
|
+
for (const event of events) {
|
|
9083
|
+
byStatus[event.status] += 1;
|
|
9084
|
+
if (event.action) {
|
|
9085
|
+
increment3(byAction, event.action);
|
|
9086
|
+
}
|
|
9087
|
+
for (const delivery of event.deliveries) {
|
|
9088
|
+
byAdapter[delivery.adapterId] ??= {
|
|
9089
|
+
delivered: 0,
|
|
9090
|
+
failed: 0,
|
|
9091
|
+
skipped: 0
|
|
9092
|
+
};
|
|
9093
|
+
byAdapter[delivery.adapterId][delivery.status] += 1;
|
|
9094
|
+
}
|
|
9095
|
+
}
|
|
9096
|
+
return {
|
|
9097
|
+
byAction,
|
|
9098
|
+
byAdapter,
|
|
9099
|
+
byStatus,
|
|
9100
|
+
events,
|
|
9101
|
+
failed: byStatus.failed,
|
|
9102
|
+
total: events.length
|
|
9103
|
+
};
|
|
9104
|
+
};
|
|
9105
|
+
var renderMetricGrid = (summary) => [
|
|
9106
|
+
'<section class="voice-handoff-health-grid">',
|
|
9107
|
+
`<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
|
|
9108
|
+
`<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
|
|
9109
|
+
`<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
|
|
9110
|
+
`<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
|
|
9111
|
+
"</section>"
|
|
9112
|
+
].join("");
|
|
9113
|
+
var renderActionSummary = (summary) => {
|
|
9114
|
+
const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
|
|
9115
|
+
const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
|
|
9116
|
+
return [
|
|
9117
|
+
'<section class="voice-handoff-health-columns">',
|
|
9118
|
+
"<article><h3>Actions</h3>",
|
|
9119
|
+
actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml8(action)}: ${String(count)}</li>`).join("")}</ul>`,
|
|
9120
|
+
"</article>",
|
|
9121
|
+
"<article><h3>Adapters</h3>",
|
|
9122
|
+
adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml8(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
|
|
9123
|
+
"</article>",
|
|
9124
|
+
"</section>"
|
|
9125
|
+
].join("");
|
|
9126
|
+
};
|
|
9127
|
+
var renderVoiceHandoffHealthHTML = (summary) => [
|
|
9128
|
+
'<div class="voice-handoff-health">',
|
|
9129
|
+
renderMetricGrid(summary),
|
|
9130
|
+
renderActionSummary(summary),
|
|
9131
|
+
"<section>",
|
|
9132
|
+
"<h3>Recent Handoffs</h3>",
|
|
9133
|
+
summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
|
|
9134
|
+
'<div class="voice-handoff-health-events">',
|
|
9135
|
+
...summary.events.map((event) => [
|
|
9136
|
+
`<article class="${escapeHtml8(event.status)}">`,
|
|
9137
|
+
'<div class="voice-handoff-health-event-header">',
|
|
9138
|
+
`<strong>${escapeHtml8(event.action ?? "handoff")}</strong>`,
|
|
9139
|
+
`<span>${escapeHtml8(event.status)}</span>`,
|
|
9140
|
+
"</div>",
|
|
9141
|
+
`<p><small>${escapeHtml8(event.sessionId)}</small></p>`,
|
|
9142
|
+
event.target ? `<p>Target: ${escapeHtml8(event.target)}</p>` : "",
|
|
9143
|
+
event.reason ? `<p>Reason: ${escapeHtml8(event.reason)}</p>` : "",
|
|
9144
|
+
event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
|
|
9145
|
+
"<li>",
|
|
9146
|
+
`${escapeHtml8(delivery.adapterId)}: ${escapeHtml8(delivery.status)}`,
|
|
9147
|
+
delivery.deliveredTo ? ` to ${escapeHtml8(delivery.deliveredTo)}` : "",
|
|
9148
|
+
delivery.error ? ` (${escapeHtml8(delivery.error)})` : "",
|
|
9149
|
+
"</li>"
|
|
9150
|
+
].join("")).join("")}</ul>` : "",
|
|
9151
|
+
event.replayHref ? `<p><a href="${escapeHtml8(event.replayHref)}">Open replay</a></p>` : "",
|
|
9152
|
+
"</article>"
|
|
9153
|
+
].join("")),
|
|
9154
|
+
"</div>"
|
|
9155
|
+
].join(""),
|
|
9156
|
+
"</section>",
|
|
9157
|
+
"</div>"
|
|
9158
|
+
].join("");
|
|
9159
|
+
var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
|
|
9160
|
+
...options,
|
|
9161
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
9162
|
+
q: query?.q ?? options.q,
|
|
9163
|
+
status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
|
|
9164
|
+
});
|
|
9165
|
+
var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
|
|
9166
|
+
const summary = await summarizeVoiceHandoffHealth({
|
|
9167
|
+
...options,
|
|
9168
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
9169
|
+
q: query?.q ?? options.q,
|
|
9170
|
+
status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
|
|
9171
|
+
});
|
|
9172
|
+
const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
|
|
9173
|
+
return new Response(body, {
|
|
9174
|
+
headers: {
|
|
9175
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9176
|
+
...options.headers
|
|
9177
|
+
}
|
|
9178
|
+
});
|
|
9179
|
+
};
|
|
9180
|
+
var createVoiceHandoffHealthRoutes = (options = {}) => {
|
|
9181
|
+
const path = options.path ?? "/api/voice-handoffs";
|
|
9182
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
9183
|
+
const routes = new Elysia6({
|
|
9184
|
+
name: options.name ?? "absolutejs-voice-handoff-health"
|
|
9185
|
+
}).get(path, createVoiceHandoffHealthJSONHandler(options));
|
|
9186
|
+
if (htmlPath) {
|
|
9187
|
+
routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
|
|
9188
|
+
}
|
|
9189
|
+
return routes;
|
|
9190
|
+
};
|
|
9191
|
+
|
|
9192
|
+
// src/qualityRoutes.ts
|
|
9193
|
+
import { Elysia as Elysia7 } from "elysia";
|
|
9194
|
+
var DEFAULT_THRESHOLDS = {
|
|
9195
|
+
maxDuplicateTurnRate: 0,
|
|
9196
|
+
maxEmptyTurnRate: 0.02,
|
|
9197
|
+
maxHandoffFailureRate: 0,
|
|
9198
|
+
maxMissingAssistantReplyRate: 0.05,
|
|
9199
|
+
maxProviderAverageLatencyMs: 3000,
|
|
9200
|
+
maxProviderErrorRate: 0.05,
|
|
9201
|
+
maxProviderFallbackRate: 0.25,
|
|
9202
|
+
maxProviderTimeoutRate: 0.03
|
|
9203
|
+
};
|
|
9204
|
+
var getString6 = (value) => typeof value === "string" ? value : undefined;
|
|
9205
|
+
var getNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
9206
|
+
var rate = (count, total) => count / Math.max(1, total);
|
|
9207
|
+
var roundMetric2 = (value) => Math.round(value * 1e4) / 1e4;
|
|
9208
|
+
var createMetric = (input) => ({
|
|
9209
|
+
...input,
|
|
9210
|
+
actual: roundMetric2(input.actual),
|
|
9211
|
+
pass: input.actual <= input.threshold
|
|
9212
|
+
});
|
|
9213
|
+
var evaluateVoiceQuality = async (input) => {
|
|
9214
|
+
const events = filterVoiceTraceEvents(input.events ?? await input.store?.list() ?? []);
|
|
9215
|
+
const thresholds = {
|
|
9216
|
+
...DEFAULT_THRESHOLDS,
|
|
9217
|
+
...input.thresholds
|
|
9218
|
+
};
|
|
9219
|
+
const committedTurns = events.filter((event) => event.type === "turn.committed");
|
|
9220
|
+
const assistantReplies = events.filter((event) => event.type === "turn.assistant");
|
|
9221
|
+
const sessionIdsWithAssistantReply = new Set(assistantReplies.map((event) => event.sessionId));
|
|
9222
|
+
const sessionsWithTurns = new Set(committedTurns.map((event) => event.sessionId));
|
|
9223
|
+
const emptyTurns = committedTurns.filter((event) => !getString6(event.payload.text)?.trim());
|
|
9224
|
+
const turnTextsBySession = new Map;
|
|
9225
|
+
let duplicateTurns = 0;
|
|
9226
|
+
for (const turn of committedTurns) {
|
|
9227
|
+
const normalized = getString6(turn.payload.text)?.trim().toLowerCase();
|
|
9228
|
+
if (!normalized) {
|
|
9229
|
+
continue;
|
|
9230
|
+
}
|
|
9231
|
+
const seen = turnTextsBySession.get(turn.sessionId) ?? new Set;
|
|
9232
|
+
if (seen.has(normalized)) {
|
|
9233
|
+
duplicateTurns += 1;
|
|
9234
|
+
}
|
|
9235
|
+
seen.add(normalized);
|
|
9236
|
+
turnTextsBySession.set(turn.sessionId, seen);
|
|
9237
|
+
}
|
|
9238
|
+
const missingAssistantReplySessions = [...sessionsWithTurns].filter((sessionId) => !sessionIdsWithAssistantReply.has(sessionId)).length;
|
|
9239
|
+
const providerEvents = events.filter((event) => event.type === "session.error" && typeof event.payload.provider === "string" && typeof event.payload.providerStatus === "string");
|
|
9240
|
+
const providerErrors = providerEvents.filter((event) => event.payload.providerStatus === "error");
|
|
9241
|
+
const providerFallbacks = providerEvents.filter((event) => event.payload.providerStatus === "fallback");
|
|
9242
|
+
const providerTimeouts = providerEvents.filter((event) => event.payload.timedOut === true);
|
|
9243
|
+
const providerLatencies = providerEvents.map((event) => getNumber3(event.payload.elapsedMs)).filter((value) => value !== undefined);
|
|
9244
|
+
const averageProviderLatencyMs = providerLatencies.length > 0 ? providerLatencies.reduce((sum, value) => sum + value, 0) / providerLatencies.length : 0;
|
|
9245
|
+
const handoffHealth = await summarizeVoiceHandoffHealth({ events });
|
|
9246
|
+
const metrics = {
|
|
9247
|
+
duplicateTurnRate: createMetric({
|
|
9248
|
+
actual: rate(duplicateTurns, committedTurns.length),
|
|
9249
|
+
label: "Duplicate turn rate",
|
|
9250
|
+
threshold: thresholds.maxDuplicateTurnRate,
|
|
9251
|
+
unit: "rate"
|
|
9252
|
+
}),
|
|
9253
|
+
emptyTurnRate: createMetric({
|
|
9254
|
+
actual: rate(emptyTurns.length, committedTurns.length),
|
|
9255
|
+
label: "Empty turn rate",
|
|
9256
|
+
threshold: thresholds.maxEmptyTurnRate,
|
|
9257
|
+
unit: "rate"
|
|
9258
|
+
}),
|
|
9259
|
+
handoffFailureRate: createMetric({
|
|
9260
|
+
actual: rate(handoffHealth.failed, handoffHealth.total),
|
|
9261
|
+
label: "Handoff failure rate",
|
|
9262
|
+
threshold: thresholds.maxHandoffFailureRate,
|
|
9263
|
+
unit: "rate"
|
|
9264
|
+
}),
|
|
9265
|
+
missingAssistantReplyRate: createMetric({
|
|
9266
|
+
actual: rate(missingAssistantReplySessions, sessionsWithTurns.size),
|
|
9267
|
+
label: "Missing assistant reply rate",
|
|
9268
|
+
threshold: thresholds.maxMissingAssistantReplyRate,
|
|
9269
|
+
unit: "rate"
|
|
9270
|
+
}),
|
|
9271
|
+
providerAverageLatencyMs: createMetric({
|
|
9272
|
+
actual: averageProviderLatencyMs,
|
|
9273
|
+
label: "Average provider latency",
|
|
9274
|
+
threshold: thresholds.maxProviderAverageLatencyMs,
|
|
9275
|
+
unit: "ms"
|
|
9276
|
+
}),
|
|
9277
|
+
providerErrorRate: createMetric({
|
|
9278
|
+
actual: rate(providerErrors.length, providerEvents.length),
|
|
9279
|
+
label: "Provider error rate",
|
|
9280
|
+
threshold: thresholds.maxProviderErrorRate,
|
|
9281
|
+
unit: "rate"
|
|
9282
|
+
}),
|
|
9283
|
+
providerFallbackRate: createMetric({
|
|
9284
|
+
actual: rate(providerFallbacks.length, providerEvents.length),
|
|
9285
|
+
label: "Provider fallback rate",
|
|
9286
|
+
threshold: thresholds.maxProviderFallbackRate,
|
|
9287
|
+
unit: "rate"
|
|
9288
|
+
}),
|
|
9289
|
+
providerTimeoutRate: createMetric({
|
|
9290
|
+
actual: rate(providerTimeouts.length, providerEvents.length),
|
|
9291
|
+
label: "Provider timeout rate",
|
|
9292
|
+
threshold: thresholds.maxProviderTimeoutRate,
|
|
9293
|
+
unit: "rate"
|
|
9294
|
+
})
|
|
9295
|
+
};
|
|
9296
|
+
const status = Object.values(metrics).every((metric) => metric.pass) ? "pass" : "fail";
|
|
9297
|
+
return {
|
|
9298
|
+
checkedAt: Date.now(),
|
|
9299
|
+
eventCount: events.length,
|
|
9300
|
+
metrics,
|
|
9301
|
+
status,
|
|
9302
|
+
thresholds
|
|
9303
|
+
};
|
|
9304
|
+
};
|
|
9305
|
+
var escapeHtml9 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9306
|
+
var formatMetricValue = (metric) => metric.unit === "rate" ? `${(metric.actual * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.actual)}ms` : String(metric.actual);
|
|
9307
|
+
var formatThreshold = (metric) => metric.unit === "rate" ? `${(metric.threshold * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.threshold)}ms` : String(metric.threshold);
|
|
9308
|
+
var renderVoiceQualityHTML = (report, options = {}) => {
|
|
9309
|
+
const rows = Object.entries(report.metrics).map(([key, metric]) => `<tr class="${metric.pass ? "pass" : "fail"}"><td>${escapeHtml9(metric.label)}</td><td>${escapeHtml9(formatMetricValue(metric))}</td><td>${escapeHtml9(formatThreshold(metric))}</td><td>${metric.pass ? "pass" : "fail"}</td><td><code>${escapeHtml9(key)}</code></td></tr>`).join("");
|
|
9310
|
+
const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
|
|
9311
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>AbsoluteJS Voice Quality</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}nav{display:flex;flex-wrap:wrap;gap:.5rem;margin:0 0 1.25rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;padding:.35rem .75rem;font-weight:800}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}table{border-collapse:collapse;width:100%;background:white;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}.pass td{border-left:4px solid #16a34a}.fail td{border-left:4px solid #dc2626}code{background:#f3f4f6;padding:.15rem .3rem;border-radius:.3rem}</style></head><body><main>${links}<h1>Voice quality gates</h1><p class="status ${report.status}">${report.status}</p><p>${report.eventCount} event(s) checked.</p><table><thead><tr><th>Metric</th><th>Actual</th><th>Threshold</th><th>Status</th><th>Key</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
|
|
9312
|
+
};
|
|
9313
|
+
var createVoiceQualityRoutes = (options) => {
|
|
9314
|
+
const path = options.path ?? "/quality";
|
|
9315
|
+
const routes = new Elysia7({
|
|
9316
|
+
name: options.name ?? "absolutejs-voice-quality"
|
|
9317
|
+
});
|
|
9318
|
+
const getReport = () => evaluateVoiceQuality({
|
|
9319
|
+
events: options.events,
|
|
9320
|
+
store: options.store,
|
|
9321
|
+
thresholds: options.thresholds
|
|
9322
|
+
});
|
|
9323
|
+
routes.get(path, async () => {
|
|
9324
|
+
const report = await getReport();
|
|
9325
|
+
return new Response(renderVoiceQualityHTML(report, { links: options.links }), {
|
|
9326
|
+
headers: {
|
|
9327
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9328
|
+
...options.headers
|
|
9329
|
+
}
|
|
9330
|
+
});
|
|
9331
|
+
});
|
|
9332
|
+
routes.get(`${path}/json`, async () => getReport());
|
|
9333
|
+
routes.get(`${path}/status`, async ({ set }) => {
|
|
9334
|
+
const report = await getReport();
|
|
9335
|
+
if (report.status === "fail") {
|
|
9336
|
+
set.status = 503;
|
|
9337
|
+
}
|
|
9338
|
+
return report;
|
|
9339
|
+
});
|
|
9340
|
+
return routes;
|
|
9341
|
+
};
|
|
9342
|
+
|
|
9343
|
+
// src/resilienceRoutes.ts
|
|
9344
|
+
import { Elysia as Elysia8 } from "elysia";
|
|
9345
|
+
var escapeHtml10 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9346
|
+
var getString7 = (value) => typeof value === "string" ? value : undefined;
|
|
9347
|
+
var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
9348
|
+
var getBoolean2 = (value) => value === true;
|
|
9349
|
+
var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
|
|
9350
|
+
var listVoiceRoutingEvents = (events) => {
|
|
9351
|
+
const routingEvents = [];
|
|
9352
|
+
for (const event of events) {
|
|
9353
|
+
if (event.type !== "session.error") {
|
|
9354
|
+
continue;
|
|
9355
|
+
}
|
|
9356
|
+
const provider = getString7(event.payload.provider);
|
|
9357
|
+
const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
9358
|
+
if (!provider || !providerStatus) {
|
|
9359
|
+
continue;
|
|
9360
|
+
}
|
|
9361
|
+
const kind = getString7(event.payload.kind);
|
|
9362
|
+
routingEvents.push({
|
|
9363
|
+
at: event.at,
|
|
9364
|
+
attempt: getNumber4(event.payload.attempt),
|
|
9365
|
+
elapsedMs: getNumber4(event.payload.elapsedMs),
|
|
9366
|
+
error: getString7(event.payload.error),
|
|
9367
|
+
fallbackProvider: getString7(event.payload.fallbackProvider),
|
|
9368
|
+
kind: kind === "stt" || kind === "tts" ? kind : "llm",
|
|
9369
|
+
latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
|
|
9370
|
+
operation: getString7(event.payload.operation),
|
|
9371
|
+
provider,
|
|
9372
|
+
selectedProvider: getString7(event.payload.selectedProvider),
|
|
9373
|
+
sessionId: event.sessionId,
|
|
9374
|
+
status: providerStatus,
|
|
9375
|
+
timedOut: getBoolean2(event.payload.timedOut),
|
|
9376
|
+
turnId: event.turnId
|
|
9377
|
+
});
|
|
9378
|
+
}
|
|
9379
|
+
return routingEvents.sort((left, right) => right.at - left.at);
|
|
9380
|
+
};
|
|
9381
|
+
var summarizeRoutingEvents = (events) => {
|
|
9382
|
+
const byKind = new Map;
|
|
9383
|
+
let errors = 0;
|
|
9384
|
+
let fallbacks = 0;
|
|
9385
|
+
let timeouts = 0;
|
|
9386
|
+
for (const event of events) {
|
|
9387
|
+
byKind.set(event.kind, (byKind.get(event.kind) ?? 0) + 1);
|
|
9388
|
+
if (event.status === "error") {
|
|
9389
|
+
errors += 1;
|
|
9390
|
+
}
|
|
9391
|
+
if (event.status === "fallback") {
|
|
9392
|
+
fallbacks += 1;
|
|
9393
|
+
}
|
|
9394
|
+
if (event.timedOut) {
|
|
9395
|
+
timeouts += 1;
|
|
9396
|
+
}
|
|
9397
|
+
}
|
|
9398
|
+
return {
|
|
9399
|
+
byKind,
|
|
9400
|
+
errors,
|
|
9401
|
+
fallbacks,
|
|
9402
|
+
timeouts,
|
|
9403
|
+
total: events.length
|
|
9404
|
+
};
|
|
9405
|
+
};
|
|
9406
|
+
var renderProviderCards = (title, providers) => {
|
|
9407
|
+
if (providers.length === 0) {
|
|
9408
|
+
return `<p class="muted">No ${escapeHtml10(title)} provider health yet.</p>`;
|
|
9409
|
+
}
|
|
9410
|
+
return `<div class="provider-grid">${providers.map((provider) => `
|
|
9411
|
+
<article class="card provider ${escapeHtml10(provider.status)}">
|
|
9412
|
+
<div class="card-header">
|
|
9413
|
+
<strong>${escapeHtml10(provider.provider)}</strong>
|
|
9414
|
+
<span>${escapeHtml10(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
|
|
9415
|
+
</div>
|
|
9416
|
+
<dl>
|
|
9417
|
+
<div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
|
|
9418
|
+
<div><dt>Avg latency</dt><dd>${provider.averageElapsedMs ?? 0}ms</dd></div>
|
|
9419
|
+
<div><dt>Errors</dt><dd>${provider.errorCount}</dd></div>
|
|
9420
|
+
<div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
|
|
9421
|
+
<div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
|
|
9422
|
+
</dl>
|
|
9423
|
+
${provider.lastError ? `<p class="muted">${escapeHtml10(provider.lastError)}</p>` : ""}
|
|
9424
|
+
</article>
|
|
9425
|
+
`).join("")}</div>`;
|
|
9426
|
+
};
|
|
9427
|
+
var renderTimeline2 = (events) => {
|
|
9428
|
+
if (events.length === 0) {
|
|
9429
|
+
return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
|
|
9430
|
+
}
|
|
9431
|
+
return `<div class="timeline">${events.slice(0, 40).map((event) => `
|
|
9432
|
+
<article class="card event ${escapeHtml10(event.status ?? "unknown")}">
|
|
9433
|
+
<div class="card-header">
|
|
9434
|
+
<strong>${escapeHtml10(event.kind.toUpperCase())} ${escapeHtml10(event.operation ?? "generate")}</strong>
|
|
9435
|
+
<span>${new Date(event.at).toLocaleString()}</span>
|
|
9436
|
+
</div>
|
|
9437
|
+
<p>
|
|
9438
|
+
<span class="pill">${escapeHtml10(event.status ?? "unknown")}</span>
|
|
9439
|
+
<span class="pill">provider: ${escapeHtml10(event.provider ?? "unknown")}</span>
|
|
9440
|
+
${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml10(event.fallbackProvider)}</span>` : ""}
|
|
9441
|
+
${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
|
|
9442
|
+
</p>
|
|
9443
|
+
<dl>
|
|
9444
|
+
<div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
|
|
9445
|
+
<div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
|
|
9446
|
+
<div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
|
|
9447
|
+
<div><dt>Session</dt><dd>${escapeHtml10(event.sessionId)}</dd></div>
|
|
9448
|
+
</dl>
|
|
9449
|
+
${event.error ? `<p class="muted">${escapeHtml10(event.error)}</p>` : ""}
|
|
9450
|
+
</article>
|
|
9451
|
+
`).join("")}</div>`;
|
|
9452
|
+
};
|
|
9453
|
+
var renderSimulationControls = (kind, simulation) => {
|
|
9454
|
+
if (!simulation) {
|
|
9455
|
+
return "";
|
|
9456
|
+
}
|
|
9457
|
+
const configuredProviders = simulation.providers.filter((provider) => provider.configured !== false);
|
|
9458
|
+
if (configuredProviders.length === 0) {
|
|
9459
|
+
return `<p class="muted">No ${kind.toUpperCase()} providers are configured for simulation.</p>`;
|
|
9460
|
+
}
|
|
9461
|
+
const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
|
|
9462
|
+
const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
|
|
9463
|
+
const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
|
|
9464
|
+
return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml10(pathPrefix)}">
|
|
9465
|
+
<p class="muted">${escapeHtml10(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
|
|
9466
|
+
<div class="simulate-actions">
|
|
9467
|
+
${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml10(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml10(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
|
|
9468
|
+
${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml10(provider.provider)}">Mark ${escapeHtml10(provider.provider)} recovered</button>`).join("")}
|
|
9469
|
+
</div>
|
|
9470
|
+
${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml10(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
|
|
9471
|
+
<pre class="simulate-output" hidden></pre>
|
|
9472
|
+
</div>`;
|
|
9473
|
+
};
|
|
9474
|
+
var renderVoiceResilienceHTML = (input) => {
|
|
9475
|
+
const summary = summarizeRoutingEvents(input.routingEvents);
|
|
9476
|
+
const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml10(kind)}: ${String(count)}</span>`).join("");
|
|
9477
|
+
const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml10(link.href)}">${escapeHtml10(link.label)}</a>`).join(" \xB7 ") : "";
|
|
9478
|
+
return `<!doctype html>
|
|
9479
|
+
<html lang="en">
|
|
9480
|
+
<head>
|
|
9481
|
+
<meta charset="utf-8" />
|
|
9482
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
9483
|
+
<title>${escapeHtml10(input.title ?? "AbsoluteJS Voice Resilience")}</title>
|
|
9484
|
+
<style>
|
|
9485
|
+
:root { color-scheme: dark; }
|
|
9486
|
+
body { background: radial-gradient(circle at top left, #172554, #09090b 36%, #050505); color: #f4f4f5; font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; padding: 24px; }
|
|
9487
|
+
main { display: grid; gap: 16px; margin: 0 auto; max-width: 1180px; }
|
|
9488
|
+
section, .card { background: rgba(19, 22, 27, 0.92); border: 1px solid #27272a; border-radius: 20px; padding: 20px; }
|
|
9489
|
+
.hero { background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(245, 158, 11, 0.12)); }
|
|
9490
|
+
.grid, .provider-grid { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
9491
|
+
.timeline { display: grid; gap: 12px; }
|
|
9492
|
+
.card-header { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
|
|
9493
|
+
.card-header strong { font-size: 1.05rem; }
|
|
9494
|
+
.metric strong { display: block; font-size: 2rem; margin-top: 6px; }
|
|
9495
|
+
.muted, dt, span { color: #a1a1aa; }
|
|
9496
|
+
dl { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
9497
|
+
dl div { background: #0f1217; border: 1px solid #27272a; border-radius: 12px; padding: 10px; }
|
|
9498
|
+
dd { font-weight: 800; margin: 4px 0 0; }
|
|
9499
|
+
.pill { background: #0f1217; border: 1px solid #3f3f46; border-radius: 999px; color: #d4d4d8; display: inline-flex; margin: 3px 4px 3px 0; padding: 5px 9px; }
|
|
9500
|
+
.danger { border-color: rgba(239, 68, 68, 0.75); color: #fecaca; }
|
|
9501
|
+
.event.error { border-color: rgba(239, 68, 68, 0.7); }
|
|
9502
|
+
.event.fallback { border-color: rgba(245, 158, 11, 0.7); }
|
|
9503
|
+
.event.success, .provider.healthy { border-color: rgba(34, 197, 94, 0.5); }
|
|
9504
|
+
.provider.suppressed, .provider.degraded, .provider.rate-limited { border-color: rgba(239, 68, 68, 0.7); }
|
|
9505
|
+
.provider.recoverable { border-color: rgba(59, 130, 246, 0.7); }
|
|
9506
|
+
button { background: #f59e0b; border: 0; border-radius: 999px; color: #111827; cursor: pointer; font-weight: 800; padding: 10px 14px; }
|
|
9507
|
+
button:disabled { cursor: not-allowed; opacity: 0.45; }
|
|
9508
|
+
.simulate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
|
|
9509
|
+
.simulate-output { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #d4d4d8; overflow: auto; padding: 12px; white-space: pre-wrap; }
|
|
9510
|
+
a { color: #f59e0b; }
|
|
9511
|
+
@media (max-width: 850px) { .grid, .provider-grid, dl { grid-template-columns: 1fr; } }
|
|
9512
|
+
</style>
|
|
9513
|
+
</head>
|
|
9514
|
+
<body>
|
|
9515
|
+
<main>
|
|
9516
|
+
<section class="hero">
|
|
9517
|
+
<h1>Provider routing and resilience</h1>
|
|
9518
|
+
<p>One view for the production reliability story: LLM failover, STT/TTS routing, latency budgets, timeouts, and fallback decisions.</p>
|
|
9519
|
+
${links ? `<p>${links}</p>` : ""}
|
|
9520
|
+
<p>${kindCounts || '<span class="pill">No routing events yet</span>'}</p>
|
|
9521
|
+
</section>
|
|
9522
|
+
<section class="grid">
|
|
9523
|
+
<article class="card metric"><span>Total routing events</span><strong>${summary.total}</strong></article>
|
|
9524
|
+
<article class="card metric"><span>Fallbacks</span><strong>${summary.fallbacks}</strong></article>
|
|
9525
|
+
<article class="card metric"><span>Errors</span><strong>${summary.errors}</strong></article>
|
|
9526
|
+
<article class="card metric"><span>Timeouts</span><strong>${summary.timeouts}</strong></article>
|
|
9527
|
+
</section>
|
|
9528
|
+
<section>
|
|
9529
|
+
<h2>LLM provider health</h2>
|
|
9530
|
+
${renderProviderCards("LLM", input.llmProviderHealth)}
|
|
9531
|
+
</section>
|
|
9532
|
+
<section>
|
|
9533
|
+
<h2>STT provider health</h2>
|
|
9534
|
+
${renderSimulationControls("stt", input.sttSimulation)}
|
|
9535
|
+
${renderProviderCards("STT", input.sttProviderHealth)}
|
|
9536
|
+
</section>
|
|
9537
|
+
<section>
|
|
9538
|
+
<h2>TTS provider health</h2>
|
|
9539
|
+
${renderSimulationControls("tts", input.ttsSimulation)}
|
|
9540
|
+
${renderProviderCards("TTS", input.ttsProviderHealth)}
|
|
9541
|
+
</section>
|
|
9542
|
+
<section>
|
|
9543
|
+
<h2>Routing timeline</h2>
|
|
9544
|
+
${renderTimeline2(input.routingEvents)}
|
|
9545
|
+
</section>
|
|
9546
|
+
</main>
|
|
9547
|
+
<script>
|
|
9548
|
+
const showResult = (panel, result) => {
|
|
9549
|
+
const output = panel.querySelector(".simulate-output");
|
|
9550
|
+
if (!output) return;
|
|
9551
|
+
output.hidden = false;
|
|
9552
|
+
output.textContent = JSON.stringify(result, null, 2);
|
|
9553
|
+
};
|
|
9554
|
+
document.querySelectorAll("[data-sim-prefix]").forEach((panel) => {
|
|
9555
|
+
const prefix = panel.getAttribute("data-sim-prefix");
|
|
9556
|
+
panel.querySelectorAll("[data-provider-fail]").forEach((button) => {
|
|
9557
|
+
button.addEventListener("click", async () => {
|
|
9558
|
+
const provider = button.getAttribute("data-provider-fail");
|
|
9559
|
+
const response = await fetch(prefix + "/failure?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
|
|
9560
|
+
showResult(panel, await response.json());
|
|
9561
|
+
if (response.ok) window.setTimeout(() => window.location.reload(), 450);
|
|
9562
|
+
});
|
|
9563
|
+
});
|
|
9564
|
+
panel.querySelectorAll("[data-provider-recover]").forEach((button) => {
|
|
9565
|
+
button.addEventListener("click", async () => {
|
|
9566
|
+
const provider = button.getAttribute("data-provider-recover");
|
|
9567
|
+
const response = await fetch(prefix + "/recovery?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
|
|
9568
|
+
showResult(panel, await response.json());
|
|
9569
|
+
if (response.ok) window.setTimeout(() => window.location.reload(), 450);
|
|
9570
|
+
});
|
|
9571
|
+
});
|
|
9572
|
+
});
|
|
9573
|
+
</script>
|
|
9574
|
+
</body>
|
|
9575
|
+
</html>`;
|
|
9576
|
+
};
|
|
9577
|
+
var providerFromQuery = (value, providers) => typeof value === "string" && providers.some((provider) => provider.provider === value && provider.configured !== false) ? value : undefined;
|
|
9578
|
+
var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
|
|
9579
|
+
if (!simulation) {
|
|
9580
|
+
return routes;
|
|
9581
|
+
}
|
|
9582
|
+
const pathPrefix = simulation.pathPrefix ?? defaultPathPrefix;
|
|
9583
|
+
routes.post(`${pathPrefix}/failure`, async ({ query, set }) => {
|
|
9584
|
+
const provider = providerFromQuery(query.provider, simulation.providers);
|
|
9585
|
+
if (!provider) {
|
|
9586
|
+
set.status = 400;
|
|
9587
|
+
return {
|
|
9588
|
+
error: "Provider is not configured for simulation."
|
|
9589
|
+
};
|
|
9590
|
+
}
|
|
9591
|
+
if (simulation.failureProviders && !simulation.failureProviders.includes(provider)) {
|
|
9592
|
+
set.status = 400;
|
|
9593
|
+
return {
|
|
9594
|
+
error: `${provider} is not configured for failure simulation.`
|
|
9595
|
+
};
|
|
9596
|
+
}
|
|
9597
|
+
if (simulation.fallbackRequiredProvider && !simulation.providers.some((entry) => entry.provider === simulation.fallbackRequiredProvider && entry.configured !== false)) {
|
|
9598
|
+
set.status = 400;
|
|
9599
|
+
return {
|
|
9600
|
+
error: simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} before simulating fallback.`
|
|
9601
|
+
};
|
|
9602
|
+
}
|
|
9603
|
+
return simulation.run(provider, "failure");
|
|
9604
|
+
});
|
|
9605
|
+
routes.post(`${pathPrefix}/recovery`, async ({ query, set }) => {
|
|
9606
|
+
const provider = providerFromQuery(query.provider, simulation.providers);
|
|
9607
|
+
if (!provider) {
|
|
9608
|
+
set.status = 400;
|
|
9609
|
+
return {
|
|
9610
|
+
error: "Provider is not configured for simulation."
|
|
9611
|
+
};
|
|
9612
|
+
}
|
|
9613
|
+
return simulation.run(provider, "recovery");
|
|
9614
|
+
});
|
|
9615
|
+
return routes;
|
|
9616
|
+
};
|
|
9617
|
+
var createVoiceResilienceRoutes = (options) => {
|
|
9618
|
+
const path = options.path ?? "/resilience";
|
|
9619
|
+
const routes = new Elysia8({
|
|
9620
|
+
name: options.name ?? "absolutejs-voice-resilience"
|
|
9621
|
+
}).get(path, async () => {
|
|
9622
|
+
const events = await options.store.list();
|
|
9623
|
+
const sttEvents = events.filter((event) => event.payload.kind === "stt");
|
|
9624
|
+
const ttsEvents = events.filter((event) => event.payload.kind === "tts");
|
|
9625
|
+
const data = {
|
|
9626
|
+
links: options.links,
|
|
9627
|
+
llmProviderHealth: await summarizeVoiceProviderHealth({
|
|
9628
|
+
events,
|
|
9629
|
+
providers: options.llmProviders ?? []
|
|
9630
|
+
}),
|
|
9631
|
+
routingEvents: listVoiceRoutingEvents(events),
|
|
9632
|
+
sttProviderHealth: await summarizeVoiceProviderHealth({
|
|
9633
|
+
events: sttEvents,
|
|
9634
|
+
providers: options.sttProviders ?? []
|
|
9635
|
+
}),
|
|
9636
|
+
sttSimulation: options.sttSimulation,
|
|
9637
|
+
title: options.title,
|
|
9638
|
+
ttsProviderHealth: await summarizeVoiceProviderHealth({
|
|
9639
|
+
events: ttsEvents,
|
|
9640
|
+
providers: options.ttsProviders ?? []
|
|
9641
|
+
}),
|
|
9642
|
+
ttsSimulation: options.ttsSimulation
|
|
9643
|
+
};
|
|
9644
|
+
const body = await (options.render ?? renderVoiceResilienceHTML)(data);
|
|
9645
|
+
return new Response(body, {
|
|
9646
|
+
headers: {
|
|
9647
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9648
|
+
...options.headers
|
|
9649
|
+
}
|
|
9650
|
+
});
|
|
9651
|
+
});
|
|
9652
|
+
registerSimulationRoutes(routes, options.sttSimulation, "/api/stt-simulate");
|
|
9653
|
+
registerSimulationRoutes(routes, options.ttsSimulation, "/api/tts-simulate");
|
|
9654
|
+
return routes;
|
|
9655
|
+
};
|
|
9656
|
+
|
|
9657
|
+
// src/opsConsoleRoutes.ts
|
|
9658
|
+
var DEFAULT_LINKS = [
|
|
9659
|
+
{
|
|
9660
|
+
description: "Quality gates for CI, deploy checks, and production readiness.",
|
|
9661
|
+
href: "/quality",
|
|
9662
|
+
label: "Quality",
|
|
9663
|
+
statusHref: "/quality/status"
|
|
9664
|
+
},
|
|
9665
|
+
{
|
|
9666
|
+
description: "Provider health, fallback paths, and failure simulation.",
|
|
9667
|
+
href: "/resilience",
|
|
9668
|
+
label: "Resilience"
|
|
9669
|
+
},
|
|
9670
|
+
{
|
|
9671
|
+
description: "Redacted trace exports for debugging and support handoffs.",
|
|
9672
|
+
href: "/diagnostics",
|
|
9673
|
+
label: "Diagnostics"
|
|
9674
|
+
},
|
|
9675
|
+
{
|
|
9676
|
+
description: "Recent sessions with replay links.",
|
|
9677
|
+
href: "/sessions",
|
|
9678
|
+
label: "Sessions"
|
|
9679
|
+
},
|
|
9680
|
+
{
|
|
9681
|
+
description: "Transfer and webhook delivery health.",
|
|
9682
|
+
href: "/handoffs",
|
|
9683
|
+
label: "Handoffs"
|
|
9684
|
+
}
|
|
9685
|
+
];
|
|
9686
|
+
var escapeHtml11 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9687
|
+
var countProviderStatuses = (providers) => {
|
|
9688
|
+
const degradedStatuses = new Set(["degraded", "rate-limited", "suppressed"]);
|
|
9689
|
+
const healthy = providers.filter((provider) => provider.status === "healthy").length;
|
|
9690
|
+
const degraded = providers.filter((provider) => degradedStatuses.has(provider.status)).length;
|
|
9691
|
+
return {
|
|
9692
|
+
degraded,
|
|
9693
|
+
healthy,
|
|
9694
|
+
total: providers.length
|
|
9695
|
+
};
|
|
9696
|
+
};
|
|
9697
|
+
var buildVoiceOpsConsoleReport = async (options) => {
|
|
9698
|
+
const events = await options.store.list();
|
|
9699
|
+
const providers = [
|
|
9700
|
+
...await summarizeVoiceProviderHealth({
|
|
9701
|
+
events,
|
|
9702
|
+
providers: options.llmProviders
|
|
9703
|
+
}),
|
|
9704
|
+
...await summarizeVoiceProviderHealth({
|
|
9705
|
+
events,
|
|
9706
|
+
providers: options.sttProviders
|
|
9707
|
+
}),
|
|
9708
|
+
...await summarizeVoiceProviderHealth({
|
|
9709
|
+
events,
|
|
9710
|
+
providers: options.ttsProviders
|
|
9711
|
+
})
|
|
9712
|
+
];
|
|
9713
|
+
const handoffs = await summarizeVoiceHandoffHealth({ events });
|
|
9714
|
+
const sessions = await summarizeVoiceSessions({
|
|
9715
|
+
events,
|
|
9716
|
+
limit: 8,
|
|
9717
|
+
status: "all"
|
|
9718
|
+
});
|
|
9719
|
+
const quality = await evaluateVoiceQuality({ events });
|
|
9720
|
+
const routingEvents = listVoiceRoutingEvents(events).slice(0, 10);
|
|
9721
|
+
const trace = summarizeVoiceTrace(events);
|
|
9722
|
+
return {
|
|
9723
|
+
checkedAt: Date.now(),
|
|
9724
|
+
eventCount: events.length,
|
|
9725
|
+
handoffs: {
|
|
9726
|
+
failed: handoffs.failed,
|
|
9727
|
+
total: handoffs.total
|
|
9728
|
+
},
|
|
9729
|
+
links: options.links ?? DEFAULT_LINKS,
|
|
9730
|
+
providers: countProviderStatuses(providers),
|
|
9731
|
+
quality,
|
|
9732
|
+
recentRoutingEvents: routingEvents,
|
|
9733
|
+
recentSessions: sessions,
|
|
9734
|
+
sessions: {
|
|
9735
|
+
failed: sessions.filter((session) => session.status === "failed").length,
|
|
9736
|
+
healthy: sessions.filter((session) => session.status === "healthy").length,
|
|
9737
|
+
total: sessions.length
|
|
9738
|
+
},
|
|
9739
|
+
trace
|
|
9740
|
+
};
|
|
9741
|
+
};
|
|
9742
|
+
var renderMetricCard = (input) => `<article class="metric"><span>${escapeHtml11(input.label)}</span><strong>${escapeHtml11(String(input.value))}</strong>${input.status ? `<p class="${escapeHtml11(input.status)}">${escapeHtml11(input.status)}</p>` : ""}${input.href ? `<a href="${escapeHtml11(input.href)}">Open</a>` : ""}</article>`;
|
|
9743
|
+
var renderVoiceOpsConsoleHTML = (report, options = {}) => {
|
|
9744
|
+
const links = report.links.map((link) => `<article class="surface">
|
|
9745
|
+
<div><h2>${escapeHtml11(link.label)}</h2>${link.description ? `<p>${escapeHtml11(link.description)}</p>` : ""}</div>
|
|
9746
|
+
<p><a href="${escapeHtml11(link.href)}">Open ${escapeHtml11(link.label)}</a>${link.statusHref ? ` \xB7 <a href="${escapeHtml11(link.statusHref)}">Status</a>` : ""}</p>
|
|
9747
|
+
</article>`).join("");
|
|
9748
|
+
const sessions = report.recentSessions.length ? report.recentSessions.map((session) => `<tr><td>${escapeHtml11(session.sessionId)}</td><td>${escapeHtml11(session.status)}</td><td>${session.turnCount}</td><td>${session.errorCount}</td><td>${session.replayHref ? `<a href="${escapeHtml11(session.replayHref)}">Replay</a>` : ""}</td></tr>`).join("") : '<tr><td colspan="5">No sessions yet.</td></tr>';
|
|
9749
|
+
const routing = report.recentRoutingEvents.length ? report.recentRoutingEvents.map((event) => `<tr><td>${escapeHtml11(event.kind)}</td><td>${escapeHtml11(event.provider ?? "unknown")}</td><td>${escapeHtml11(event.status ?? "unknown")}</td><td>${event.elapsedMs ?? 0}ms</td><td>${escapeHtml11(event.sessionId)}</td></tr>`).join("") : '<tr><td colspan="5">No provider routing events yet.</td></tr>';
|
|
9750
|
+
const title = options.title ?? "AbsoluteJS Voice Ops Console";
|
|
9751
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml11(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;background:#101316;color:#f6f2e8;margin:0}main{max-width:1180px;margin:auto;padding:32px}a{color:#fbbf24}header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}.eyebrow{color:#fbbf24;font-weight:800;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.2rem,5vw,4.5rem);line-height:.95;margin:.2rem 0 1rem}.muted{color:#a8b0b8}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.metric,.surface{background:#181d22;border:1px solid #2a323a;border-radius:20px;padding:18px}.metric strong{display:block;font-size:2.2rem;margin:.25rem 0}.pass,.healthy{color:#86efac}.fail,.failed,.degraded{color:#fca5a5}.surfaces{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin:24px 0}table{width:100%;border-collapse:collapse;background:#181d22;border-radius:16px;overflow:hidden;margin:12px 0 28px}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left}section{margin-top:30px}@media(max-width:700px){main{padding:20px}header{display:block}}</style></head><body><main><header><div><p class="eyebrow">Self-hosted voice operations</p><h1>${escapeHtml11(title)}</h1><p class="muted">One deployable control plane for quality gates, failover, traces, sessions, handoffs, and provider health.</p></div><p class="muted">Checked ${escapeHtml11(new Date(report.checkedAt).toLocaleString())}</p></header><div class="grid">${renderMetricCard({ label: "Quality", value: report.quality.status, status: report.quality.status, href: "/quality" })}${renderMetricCard({ label: "Events", value: report.eventCount, href: "/diagnostics" })}${renderMetricCard({ label: "Sessions", value: report.sessions.total, status: report.sessions.failed > 0 ? "failed" : "healthy", href: "/sessions" })}${renderMetricCard({ label: "Handoffs failed", value: report.handoffs.failed, status: report.handoffs.failed > 0 ? "failed" : "healthy", href: "/handoffs" })}${renderMetricCard({ label: "Providers degraded", value: report.providers.degraded, status: report.providers.degraded > 0 ? "degraded" : "healthy", href: "/resilience" })}</div><section><h2>Operational Surfaces</h2><div class="surfaces">${links}</div></section><section><h2>Recent Sessions</h2><table><thead><tr><th>Session</th><th>Status</th><th>Turns</th><th>Errors</th><th>Replay</th></tr></thead><tbody>${sessions}</tbody></table></section><section><h2>Recent Provider Routing</h2><table><thead><tr><th>Kind</th><th>Provider</th><th>Status</th><th>Elapsed</th><th>Session</th></tr></thead><tbody>${routing}</tbody></table></section></main></body></html>`;
|
|
9752
|
+
};
|
|
9753
|
+
var createVoiceOpsConsoleRoutes = (options) => {
|
|
9754
|
+
const path = options.path ?? "/ops-console";
|
|
9755
|
+
const routes = new Elysia9({
|
|
9756
|
+
name: options.name ?? "absolutejs-voice-ops-console"
|
|
9757
|
+
});
|
|
9758
|
+
const getReport = () => buildVoiceOpsConsoleReport(options);
|
|
9759
|
+
routes.get(path, async () => {
|
|
9760
|
+
const report = await getReport();
|
|
9761
|
+
return new Response(renderVoiceOpsConsoleHTML(report, { title: options.title }), {
|
|
9762
|
+
headers: {
|
|
9763
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9764
|
+
...options.headers
|
|
9765
|
+
}
|
|
9766
|
+
});
|
|
9767
|
+
});
|
|
9768
|
+
routes.get(`${path}/json`, async () => getReport());
|
|
9769
|
+
return routes;
|
|
9770
|
+
};
|
|
9771
|
+
// src/providerAdapters.ts
|
|
9772
|
+
class VoiceIOProviderTimeoutError extends Error {
|
|
9773
|
+
provider;
|
|
9774
|
+
timeoutMs;
|
|
9775
|
+
constructor(kind, provider, timeoutMs) {
|
|
9776
|
+
super(`Voice ${kind} provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
|
|
9777
|
+
this.name = "VoiceIOProviderTimeoutError";
|
|
9778
|
+
this.provider = provider;
|
|
9779
|
+
this.timeoutMs = timeoutMs;
|
|
9780
|
+
}
|
|
9781
|
+
}
|
|
9782
|
+
var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
|
|
9783
|
+
var createEmitter = () => {
|
|
9784
|
+
const listeners = new Map;
|
|
9785
|
+
return {
|
|
9786
|
+
emit: async (event, payload) => {
|
|
9787
|
+
await Promise.all([...listeners.get(event) ?? []].map((handler) => Promise.resolve(handler(payload))));
|
|
9788
|
+
},
|
|
9789
|
+
on: (event, handler) => {
|
|
9790
|
+
const set = listeners.get(event) ?? new Set;
|
|
9791
|
+
set.add(handler);
|
|
9792
|
+
listeners.set(event, set);
|
|
9793
|
+
return () => {
|
|
9794
|
+
set.delete(handler);
|
|
9795
|
+
};
|
|
9796
|
+
}
|
|
9797
|
+
};
|
|
9798
|
+
};
|
|
9799
|
+
var getTimeoutMs = (options, provider) => {
|
|
9800
|
+
const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
|
|
9801
|
+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
|
|
9802
|
+
};
|
|
9803
|
+
var withTimeout = async (input) => {
|
|
9804
|
+
if (!input.timeoutMs) {
|
|
9805
|
+
return input.run();
|
|
9806
|
+
}
|
|
9807
|
+
let timeout;
|
|
9808
|
+
try {
|
|
9809
|
+
return await Promise.race([
|
|
9810
|
+
Promise.resolve(input.run()),
|
|
9811
|
+
new Promise((_, reject) => {
|
|
9812
|
+
timeout = setTimeout(() => reject(new VoiceIOProviderTimeoutError(input.kind, input.provider, input.timeoutMs)), input.timeoutMs);
|
|
9813
|
+
})
|
|
9814
|
+
]);
|
|
9815
|
+
} finally {
|
|
9816
|
+
if (timeout) {
|
|
9817
|
+
clearTimeout(timeout);
|
|
9818
|
+
}
|
|
9819
|
+
}
|
|
9820
|
+
};
|
|
9821
|
+
var createResolver = (options) => {
|
|
9822
|
+
const providerIds = Object.keys(options.adapters);
|
|
9823
|
+
const firstProvider = providerIds[0];
|
|
9824
|
+
const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
|
|
9825
|
+
const healthState = new Map;
|
|
9826
|
+
const now = () => healthOptions?.now?.() ?? Date.now();
|
|
9827
|
+
const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
|
|
9828
|
+
const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
|
|
9829
|
+
const getHealth = (provider) => {
|
|
9830
|
+
const existing = healthState.get(provider);
|
|
9831
|
+
if (existing) {
|
|
9832
|
+
return existing;
|
|
9833
|
+
}
|
|
9834
|
+
const next = {
|
|
9835
|
+
consecutiveFailures: 0,
|
|
9836
|
+
provider,
|
|
9837
|
+
status: "healthy"
|
|
9838
|
+
};
|
|
9839
|
+
healthState.set(provider, next);
|
|
9840
|
+
return next;
|
|
9841
|
+
};
|
|
9842
|
+
const cloneHealth = (provider) => {
|
|
9843
|
+
if (!healthOptions) {
|
|
9844
|
+
return;
|
|
9845
|
+
}
|
|
9846
|
+
return {
|
|
9847
|
+
...getHealth(provider)
|
|
9848
|
+
};
|
|
9849
|
+
};
|
|
9850
|
+
const getSuppressionRemainingMs = (provider) => {
|
|
9851
|
+
if (!healthOptions) {
|
|
9852
|
+
return;
|
|
9853
|
+
}
|
|
9854
|
+
const suppressedUntil = getHealth(provider).suppressedUntil;
|
|
9855
|
+
return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
|
|
9856
|
+
};
|
|
9857
|
+
const isSuppressed = (provider) => {
|
|
9858
|
+
if (!healthOptions) {
|
|
9859
|
+
return false;
|
|
9860
|
+
}
|
|
9861
|
+
const suppressedUntil = getHealth(provider).suppressedUntil;
|
|
9862
|
+
return typeof suppressedUntil === "number" && suppressedUntil > now();
|
|
9863
|
+
};
|
|
9864
|
+
const recordSuccess = (provider) => {
|
|
9865
|
+
if (!healthOptions) {
|
|
9866
|
+
return;
|
|
9867
|
+
}
|
|
9868
|
+
const health = getHealth(provider);
|
|
9869
|
+
health.consecutiveFailures = 0;
|
|
9870
|
+
health.status = "healthy";
|
|
9871
|
+
health.suppressedUntil = undefined;
|
|
9872
|
+
return cloneHealth(provider);
|
|
9873
|
+
};
|
|
9874
|
+
const recordError = (provider, isProviderError) => {
|
|
9875
|
+
if (!healthOptions || !isProviderError) {
|
|
9876
|
+
return cloneHealth(provider);
|
|
9877
|
+
}
|
|
9878
|
+
const health = getHealth(provider);
|
|
9879
|
+
health.consecutiveFailures += 1;
|
|
9880
|
+
health.lastFailureAt = now();
|
|
9881
|
+
if (health.consecutiveFailures >= failureThreshold) {
|
|
9882
|
+
health.status = "suppressed";
|
|
9883
|
+
health.suppressedUntil = now() + cooldownMs;
|
|
9884
|
+
}
|
|
9885
|
+
return cloneHealth(provider);
|
|
9886
|
+
};
|
|
9887
|
+
const resolveOrder = async (input) => {
|
|
9888
|
+
const selectedProvider = await options.selectProvider?.(input) ?? firstProvider;
|
|
9889
|
+
const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
|
|
9890
|
+
const candidates = [selectedProvider, ...fallbackOrder ?? providerIds];
|
|
9891
|
+
const seen = new Set;
|
|
9892
|
+
const rankedOrder = candidates.filter((provider) => {
|
|
9893
|
+
if (!provider || seen.has(provider) || !options.adapters[provider]) {
|
|
9894
|
+
return false;
|
|
9895
|
+
}
|
|
9896
|
+
seen.add(provider);
|
|
9897
|
+
return true;
|
|
9898
|
+
});
|
|
9899
|
+
const healthyOrder = healthOptions ? rankedOrder.filter((provider) => !isSuppressed(provider)) : rankedOrder;
|
|
9900
|
+
const order = healthyOrder.length ? healthyOrder : rankedOrder;
|
|
9901
|
+
return {
|
|
9902
|
+
order,
|
|
9903
|
+
selectedProvider: selectedProvider && !isSuppressed(selectedProvider) ? selectedProvider : order[0]
|
|
9904
|
+
};
|
|
9905
|
+
};
|
|
9906
|
+
const emit = async (event, input) => {
|
|
9907
|
+
await options.onProviderEvent?.(event, input);
|
|
9908
|
+
};
|
|
9909
|
+
return {
|
|
9910
|
+
emit,
|
|
9911
|
+
getSuppressionRemainingMs,
|
|
9912
|
+
providerIds,
|
|
9913
|
+
recordError,
|
|
9914
|
+
recordSuccess,
|
|
9915
|
+
resolveOrder
|
|
9916
|
+
};
|
|
9917
|
+
};
|
|
9918
|
+
var createVoiceSTTProviderRouter = (options) => {
|
|
9919
|
+
const resolver = createResolver(options);
|
|
9920
|
+
return {
|
|
9921
|
+
kind: "stt",
|
|
9922
|
+
open: async (input) => {
|
|
9923
|
+
const { order, selectedProvider } = await resolver.resolveOrder(input);
|
|
9924
|
+
if (!selectedProvider || order.length === 0) {
|
|
9925
|
+
throw new Error("Voice STT provider router has no available providers.");
|
|
9926
|
+
}
|
|
9927
|
+
let lastError;
|
|
9928
|
+
for (const [index, provider] of order.entries()) {
|
|
9929
|
+
const adapter = options.adapters[provider];
|
|
9930
|
+
if (!adapter) {
|
|
9931
|
+
continue;
|
|
9932
|
+
}
|
|
9933
|
+
const startedAt = Date.now();
|
|
9934
|
+
try {
|
|
9935
|
+
const session = await withTimeout({
|
|
9936
|
+
kind: "stt",
|
|
9937
|
+
operation: "open",
|
|
9938
|
+
provider,
|
|
9939
|
+
run: () => adapter.open(input),
|
|
9940
|
+
timeoutMs: getTimeoutMs(options, provider)
|
|
9941
|
+
});
|
|
9942
|
+
const providerHealth = resolver.recordSuccess(provider);
|
|
9943
|
+
await resolver.emit({
|
|
9944
|
+
at: Date.now(),
|
|
9945
|
+
attempt: index + 1,
|
|
9946
|
+
elapsedMs: Date.now() - startedAt,
|
|
9947
|
+
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
9948
|
+
kind: "stt",
|
|
9949
|
+
latencyBudgetMs: getTimeoutMs(options, provider),
|
|
9950
|
+
operation: "open",
|
|
9951
|
+
provider,
|
|
9952
|
+
providerHealth,
|
|
9953
|
+
selectedProvider,
|
|
9954
|
+
status: provider === selectedProvider ? "success" : "fallback"
|
|
9955
|
+
}, input);
|
|
9956
|
+
return session;
|
|
9957
|
+
} catch (error) {
|
|
9958
|
+
lastError = error;
|
|
9959
|
+
const hasNextProvider = index < order.length - 1;
|
|
9960
|
+
const shouldFallback = options.isProviderError?.(error, provider) ?? true;
|
|
9961
|
+
const providerHealth = resolver.recordError(provider, shouldFallback);
|
|
9962
|
+
await resolver.emit({
|
|
9963
|
+
at: Date.now(),
|
|
9964
|
+
attempt: index + 1,
|
|
9965
|
+
elapsedMs: Date.now() - startedAt,
|
|
9966
|
+
error: errorMessage2(error),
|
|
9967
|
+
fallbackProvider: shouldFallback ? order[index + 1] : undefined,
|
|
9968
|
+
kind: "stt",
|
|
9969
|
+
latencyBudgetMs: getTimeoutMs(options, provider),
|
|
9970
|
+
operation: "open",
|
|
9971
|
+
provider,
|
|
9972
|
+
providerHealth,
|
|
9973
|
+
selectedProvider,
|
|
9974
|
+
status: "error",
|
|
9975
|
+
suppressionRemainingMs: resolver.getSuppressionRemainingMs(provider),
|
|
9976
|
+
suppressedUntil: providerHealth?.suppressedUntil,
|
|
9977
|
+
timedOut: error instanceof VoiceIOProviderTimeoutError
|
|
9978
|
+
}, input);
|
|
9979
|
+
if (!hasNextProvider || !shouldFallback) {
|
|
9980
|
+
throw error;
|
|
9981
|
+
}
|
|
9982
|
+
}
|
|
9983
|
+
}
|
|
9984
|
+
throw lastError ?? new Error("Voice STT provider router did not open a provider.");
|
|
9985
|
+
}
|
|
9986
|
+
};
|
|
9987
|
+
};
|
|
9988
|
+
var createVoiceTTSProviderRouter = (options) => {
|
|
9989
|
+
const resolver = createResolver(options);
|
|
9990
|
+
return {
|
|
9991
|
+
kind: "tts",
|
|
9992
|
+
open: async (input) => {
|
|
9993
|
+
const { order, selectedProvider } = await resolver.resolveOrder(input);
|
|
9994
|
+
if (!selectedProvider || order.length === 0) {
|
|
9995
|
+
throw new Error("Voice TTS provider router has no available providers.");
|
|
9996
|
+
}
|
|
9997
|
+
const emitter = createEmitter();
|
|
9998
|
+
let activeSession;
|
|
9999
|
+
let activeProvider;
|
|
10000
|
+
let nextProviderIndex = 0;
|
|
10001
|
+
const attach = (session) => {
|
|
10002
|
+
session.on("audio", (event) => emitter.emit("audio", event));
|
|
10003
|
+
session.on("error", (event) => emitter.emit("error", event));
|
|
10004
|
+
session.on("close", (event) => emitter.emit("close", event));
|
|
10005
|
+
};
|
|
10006
|
+
const openProvider = async (provider, attempt) => {
|
|
10007
|
+
const adapter = options.adapters[provider];
|
|
10008
|
+
if (!adapter) {
|
|
10009
|
+
throw new Error(`Voice TTS provider ${provider} is not configured.`);
|
|
10010
|
+
}
|
|
10011
|
+
const startedAt = Date.now();
|
|
10012
|
+
const session = await withTimeout({
|
|
10013
|
+
kind: "tts",
|
|
10014
|
+
operation: "open",
|
|
10015
|
+
provider,
|
|
10016
|
+
run: () => adapter.open(input),
|
|
10017
|
+
timeoutMs: getTimeoutMs(options, provider)
|
|
10018
|
+
});
|
|
10019
|
+
attach(session);
|
|
10020
|
+
activeSession = session;
|
|
10021
|
+
activeProvider = provider;
|
|
10022
|
+
const providerHealth = resolver.recordSuccess(provider);
|
|
10023
|
+
await resolver.emit({
|
|
10024
|
+
at: Date.now(),
|
|
10025
|
+
attempt,
|
|
10026
|
+
elapsedMs: Date.now() - startedAt,
|
|
10027
|
+
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
10028
|
+
kind: "tts",
|
|
10029
|
+
latencyBudgetMs: getTimeoutMs(options, provider),
|
|
10030
|
+
operation: "open",
|
|
10031
|
+
provider,
|
|
10032
|
+
providerHealth,
|
|
10033
|
+
selectedProvider,
|
|
10034
|
+
status: provider === selectedProvider ? "success" : "fallback"
|
|
10035
|
+
}, input);
|
|
10036
|
+
return session;
|
|
10037
|
+
};
|
|
10038
|
+
const failProvider = async (inputEvent) => {
|
|
10039
|
+
const shouldFallback = options.isProviderError?.(inputEvent.error, inputEvent.provider) ?? true;
|
|
10040
|
+
const providerHealth = resolver.recordError(inputEvent.provider, shouldFallback);
|
|
10041
|
+
await resolver.emit({
|
|
10042
|
+
at: Date.now(),
|
|
10043
|
+
attempt: inputEvent.attempt,
|
|
10044
|
+
elapsedMs: Date.now() - inputEvent.startedAt,
|
|
10045
|
+
error: errorMessage2(inputEvent.error),
|
|
10046
|
+
fallbackProvider: shouldFallback ? order[nextProviderIndex] : undefined,
|
|
10047
|
+
kind: "tts",
|
|
10048
|
+
latencyBudgetMs: getTimeoutMs(options, inputEvent.provider),
|
|
10049
|
+
operation: inputEvent.operation,
|
|
10050
|
+
provider: inputEvent.provider,
|
|
10051
|
+
providerHealth,
|
|
10052
|
+
selectedProvider,
|
|
10053
|
+
status: "error",
|
|
10054
|
+
suppressionRemainingMs: resolver.getSuppressionRemainingMs(inputEvent.provider),
|
|
10055
|
+
suppressedUntil: providerHealth?.suppressedUntil,
|
|
10056
|
+
timedOut: inputEvent.error instanceof VoiceIOProviderTimeoutError
|
|
10057
|
+
}, input);
|
|
10058
|
+
return shouldFallback;
|
|
10059
|
+
};
|
|
10060
|
+
for (const [index, provider] of order.entries()) {
|
|
10061
|
+
nextProviderIndex = index + 1;
|
|
10062
|
+
const startedAt = Date.now();
|
|
10063
|
+
try {
|
|
10064
|
+
await openProvider(provider, index + 1);
|
|
10065
|
+
break;
|
|
10066
|
+
} catch (error) {
|
|
10067
|
+
const shouldFallback = await failProvider({
|
|
10068
|
+
attempt: index + 1,
|
|
10069
|
+
error,
|
|
10070
|
+
operation: "open",
|
|
10071
|
+
provider,
|
|
10072
|
+
startedAt
|
|
10073
|
+
});
|
|
10074
|
+
if (!shouldFallback || index >= order.length - 1) {
|
|
10075
|
+
throw error;
|
|
10076
|
+
}
|
|
10077
|
+
}
|
|
10078
|
+
}
|
|
10079
|
+
if (!activeSession || !activeProvider) {
|
|
10080
|
+
throw new Error("Voice TTS provider router did not open a provider.");
|
|
10081
|
+
}
|
|
10082
|
+
const sendWithFallback = async (text) => {
|
|
10083
|
+
for (;; ) {
|
|
10084
|
+
const session = activeSession;
|
|
10085
|
+
const provider = activeProvider;
|
|
10086
|
+
if (!session || !provider) {
|
|
10087
|
+
throw new Error("Voice TTS provider router has no active provider.");
|
|
10088
|
+
}
|
|
10089
|
+
const startedAt = Date.now();
|
|
10090
|
+
try {
|
|
10091
|
+
await withTimeout({
|
|
10092
|
+
kind: "tts",
|
|
10093
|
+
operation: "send",
|
|
10094
|
+
provider,
|
|
10095
|
+
run: () => session.send(text),
|
|
10096
|
+
timeoutMs: getTimeoutMs(options, provider)
|
|
10097
|
+
});
|
|
10098
|
+
return;
|
|
10099
|
+
} catch (error) {
|
|
10100
|
+
const shouldFallback = await failProvider({
|
|
10101
|
+
attempt: nextProviderIndex,
|
|
10102
|
+
error,
|
|
10103
|
+
operation: "send",
|
|
10104
|
+
provider,
|
|
10105
|
+
startedAt
|
|
10106
|
+
});
|
|
10107
|
+
const nextProvider = order[nextProviderIndex];
|
|
10108
|
+
if (!shouldFallback || !nextProvider) {
|
|
10109
|
+
throw error;
|
|
10110
|
+
}
|
|
10111
|
+
nextProviderIndex += 1;
|
|
10112
|
+
await session.close("tts-provider-fallback").catch(() => {});
|
|
10113
|
+
await openProvider(nextProvider, nextProviderIndex);
|
|
10114
|
+
}
|
|
10115
|
+
}
|
|
10116
|
+
};
|
|
10117
|
+
return {
|
|
10118
|
+
close: async (reason) => {
|
|
10119
|
+
await activeSession?.close(reason);
|
|
10120
|
+
activeSession = undefined;
|
|
10121
|
+
activeProvider = undefined;
|
|
10122
|
+
await emitter.emit("close", {
|
|
10123
|
+
reason,
|
|
10124
|
+
type: "close"
|
|
10125
|
+
});
|
|
10126
|
+
},
|
|
10127
|
+
on: emitter.on,
|
|
10128
|
+
send: sendWithFallback
|
|
10129
|
+
};
|
|
10130
|
+
}
|
|
10131
|
+
};
|
|
7030
10132
|
};
|
|
7031
|
-
var createVoiceFileRuntimeStorage = (options) => ({
|
|
7032
|
-
events: createVoiceFileIntegrationEventStore({
|
|
7033
|
-
...options,
|
|
7034
|
-
directory: join(options.directory, "events")
|
|
7035
|
-
}),
|
|
7036
|
-
externalObjects: createVoiceFileExternalObjectMapStore({
|
|
7037
|
-
...options,
|
|
7038
|
-
directory: join(options.directory, "external-objects")
|
|
7039
|
-
}),
|
|
7040
|
-
memories: createVoiceFileAssistantMemoryStore({
|
|
7041
|
-
...options,
|
|
7042
|
-
directory: join(options.directory, "memories")
|
|
7043
|
-
}),
|
|
7044
|
-
reviews: createVoiceFileReviewStore({
|
|
7045
|
-
...options,
|
|
7046
|
-
directory: join(options.directory, "reviews")
|
|
7047
|
-
}),
|
|
7048
|
-
session: createVoiceFileSessionStore({
|
|
7049
|
-
...options,
|
|
7050
|
-
directory: join(options.directory, "sessions")
|
|
7051
|
-
}),
|
|
7052
|
-
tasks: createVoiceFileTaskStore({
|
|
7053
|
-
...options,
|
|
7054
|
-
directory: join(options.directory, "tasks")
|
|
7055
|
-
}),
|
|
7056
|
-
traceDeliveries: createVoiceFileTraceSinkDeliveryStore({
|
|
7057
|
-
...options,
|
|
7058
|
-
directory: join(options.directory, "trace-deliveries")
|
|
7059
|
-
}),
|
|
7060
|
-
traces: createVoiceFileTraceEventStore({
|
|
7061
|
-
...options,
|
|
7062
|
-
directory: join(options.directory, "traces")
|
|
7063
|
-
})
|
|
7064
|
-
});
|
|
7065
|
-
var createStoredVoiceCallReviewArtifact = (id, artifact) => withVoiceCallReviewId(id, artifact);
|
|
7066
|
-
var createStoredVoiceOpsTask = (id, task) => withVoiceOpsTaskId(id, task);
|
|
7067
|
-
var createStoredVoiceIntegrationEvent = (id, event) => withVoiceIntegrationEventId(id, event);
|
|
7068
|
-
var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectMap({
|
|
7069
|
-
at: mapping.at,
|
|
7070
|
-
externalId: mapping.externalId,
|
|
7071
|
-
provider: mapping.provider,
|
|
7072
|
-
sinkId: mapping.sinkId,
|
|
7073
|
-
sourceId: mapping.sourceId,
|
|
7074
|
-
sourceType: mapping.sourceType
|
|
7075
|
-
});
|
|
7076
10133
|
// src/sqliteStore.ts
|
|
7077
10134
|
import { Database } from "bun:sqlite";
|
|
7078
10135
|
var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
|
|
@@ -7554,6 +10611,169 @@ var createVoiceMemoryStore = () => {
|
|
|
7554
10611
|
};
|
|
7555
10612
|
return { get, getOrCreate, list, remove, set };
|
|
7556
10613
|
};
|
|
10614
|
+
// src/opsWebhook.ts
|
|
10615
|
+
import { Elysia as Elysia10 } from "elysia";
|
|
10616
|
+
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
10617
|
+
var signVoiceOpsWebhookBody = async (input) => {
|
|
10618
|
+
const encoder = new TextEncoder;
|
|
10619
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
10620
|
+
hash: "SHA-256",
|
|
10621
|
+
name: "HMAC"
|
|
10622
|
+
}, false, ["sign"]);
|
|
10623
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
|
|
10624
|
+
return `sha256=${toHex5(new Uint8Array(signature))}`;
|
|
10625
|
+
};
|
|
10626
|
+
var timingSafeEqual = (left, right) => {
|
|
10627
|
+
const encoder = new TextEncoder;
|
|
10628
|
+
const leftBytes = encoder.encode(left);
|
|
10629
|
+
const rightBytes = encoder.encode(right);
|
|
10630
|
+
if (leftBytes.length !== rightBytes.length) {
|
|
10631
|
+
return false;
|
|
10632
|
+
}
|
|
10633
|
+
let diff = 0;
|
|
10634
|
+
for (let index = 0;index < leftBytes.length; index += 1) {
|
|
10635
|
+
diff |= leftBytes[index] ^ rightBytes[index];
|
|
10636
|
+
}
|
|
10637
|
+
return diff === 0;
|
|
10638
|
+
};
|
|
10639
|
+
var resolveWebhookLink = async (resolver, event) => {
|
|
10640
|
+
if (typeof resolver === "function") {
|
|
10641
|
+
return resolver({
|
|
10642
|
+
event
|
|
10643
|
+
});
|
|
10644
|
+
}
|
|
10645
|
+
return resolver;
|
|
10646
|
+
};
|
|
10647
|
+
var joinBaseUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
10648
|
+
var asString = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
10649
|
+
var buildVoiceOpsWebhookEntity = (event) => ({
|
|
10650
|
+
disposition: asString(event.payload.disposition),
|
|
10651
|
+
outcome: asString(event.payload.outcome),
|
|
10652
|
+
priority: asString(event.payload.priority),
|
|
10653
|
+
queue: asString(event.payload.queue),
|
|
10654
|
+
reviewId: asString(event.payload.reviewId),
|
|
10655
|
+
scenarioId: asString(event.payload.scenarioId),
|
|
10656
|
+
sessionId: asString(event.payload.sessionId),
|
|
10657
|
+
status: asString(event.payload.status),
|
|
10658
|
+
target: asString(event.payload.target),
|
|
10659
|
+
taskId: asString(event.payload.taskId)
|
|
10660
|
+
});
|
|
10661
|
+
var createVoiceOpsWebhookEnvelope = async (input) => {
|
|
10662
|
+
const entity = buildVoiceOpsWebhookEntity(input.event);
|
|
10663
|
+
const replayHref = await resolveWebhookLink(input.replayHref, input.event) ?? (input.baseUrl && entity.sessionId ? joinBaseUrl(input.baseUrl, `/api/voice-sessions/${encodeURIComponent(entity.sessionId)}/replay`) : undefined);
|
|
10664
|
+
const links = {
|
|
10665
|
+
event: await resolveWebhookLink(input.eventHref, input.event),
|
|
10666
|
+
replay: replayHref,
|
|
10667
|
+
review: await resolveWebhookLink(input.reviewHref, input.event),
|
|
10668
|
+
task: await resolveWebhookLink(input.taskHref, input.event)
|
|
10669
|
+
};
|
|
10670
|
+
return {
|
|
10671
|
+
entity,
|
|
10672
|
+
event: {
|
|
10673
|
+
createdAt: input.event.createdAt,
|
|
10674
|
+
id: input.event.id,
|
|
10675
|
+
payload: input.event.payload,
|
|
10676
|
+
type: input.event.type
|
|
10677
|
+
},
|
|
10678
|
+
links: links.event || links.replay || links.review || links.task ? links : undefined,
|
|
10679
|
+
schemaVersion: 1,
|
|
10680
|
+
source: "absolutejs-voice"
|
|
10681
|
+
};
|
|
10682
|
+
};
|
|
10683
|
+
var createVoiceOpsWebhookSink = (options) => createVoiceIntegrationHTTPSink({
|
|
10684
|
+
...options,
|
|
10685
|
+
body: ({ event }) => createVoiceOpsWebhookEnvelope({
|
|
10686
|
+
baseUrl: options.baseUrl,
|
|
10687
|
+
event,
|
|
10688
|
+
eventHref: options.eventHref,
|
|
10689
|
+
replayHref: options.replayHref,
|
|
10690
|
+
reviewHref: options.reviewHref,
|
|
10691
|
+
taskHref: options.taskHref
|
|
10692
|
+
}),
|
|
10693
|
+
kind: options.kind ?? "ops-webhook"
|
|
10694
|
+
});
|
|
10695
|
+
var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
10696
|
+
if (!input.secret) {
|
|
10697
|
+
return {
|
|
10698
|
+
ok: false,
|
|
10699
|
+
reason: "missing-secret"
|
|
10700
|
+
};
|
|
10701
|
+
}
|
|
10702
|
+
if (!input.signature) {
|
|
10703
|
+
return {
|
|
10704
|
+
ok: false,
|
|
10705
|
+
reason: "missing-signature"
|
|
10706
|
+
};
|
|
10707
|
+
}
|
|
10708
|
+
if (!input.signature.startsWith("sha256=")) {
|
|
10709
|
+
return {
|
|
10710
|
+
ok: false,
|
|
10711
|
+
reason: "unsupported-algorithm"
|
|
10712
|
+
};
|
|
10713
|
+
}
|
|
10714
|
+
if (!input.timestamp) {
|
|
10715
|
+
return {
|
|
10716
|
+
ok: false,
|
|
10717
|
+
reason: "missing-timestamp"
|
|
10718
|
+
};
|
|
10719
|
+
}
|
|
10720
|
+
const timestampMs = Number(input.timestamp);
|
|
10721
|
+
const toleranceMs = Math.max(0, input.toleranceMs ?? 5 * 60 * 1000);
|
|
10722
|
+
if (!Number.isFinite(timestampMs) || toleranceMs > 0 && Math.abs((input.now ?? Date.now()) - timestampMs) > toleranceMs) {
|
|
10723
|
+
return {
|
|
10724
|
+
ok: false,
|
|
10725
|
+
reason: "stale-timestamp"
|
|
10726
|
+
};
|
|
10727
|
+
}
|
|
10728
|
+
const expected = await signVoiceOpsWebhookBody({
|
|
10729
|
+
body: input.body,
|
|
10730
|
+
secret: input.secret,
|
|
10731
|
+
timestamp: input.timestamp
|
|
10732
|
+
});
|
|
10733
|
+
if (!timingSafeEqual(expected, input.signature)) {
|
|
10734
|
+
return {
|
|
10735
|
+
ok: false,
|
|
10736
|
+
reason: "invalid-signature"
|
|
10737
|
+
};
|
|
10738
|
+
}
|
|
10739
|
+
return {
|
|
10740
|
+
ok: true
|
|
10741
|
+
};
|
|
10742
|
+
};
|
|
10743
|
+
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
10744
|
+
const path = options.path ?? "/api/voice-ops/webhook";
|
|
10745
|
+
return new Elysia10().post(path, async ({ body, request, set }) => {
|
|
10746
|
+
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
10747
|
+
if (options.signingSecret) {
|
|
10748
|
+
const verification = await verifyVoiceOpsWebhookSignature({
|
|
10749
|
+
body: bodyText,
|
|
10750
|
+
secret: options.signingSecret,
|
|
10751
|
+
signature: request.headers.get("x-absolutejs-signature"),
|
|
10752
|
+
timestamp: request.headers.get("x-absolutejs-timestamp"),
|
|
10753
|
+
toleranceMs: options.toleranceMs
|
|
10754
|
+
});
|
|
10755
|
+
if (!verification.ok) {
|
|
10756
|
+
set.status = 401;
|
|
10757
|
+
return {
|
|
10758
|
+
ok: false,
|
|
10759
|
+
reason: verification.reason
|
|
10760
|
+
};
|
|
10761
|
+
}
|
|
10762
|
+
}
|
|
10763
|
+
const envelope = JSON.parse(bodyText);
|
|
10764
|
+
await options.onEnvelope?.({
|
|
10765
|
+
envelope,
|
|
10766
|
+
request
|
|
10767
|
+
});
|
|
10768
|
+
return {
|
|
10769
|
+
eventId: envelope.event?.id,
|
|
10770
|
+
ok: true,
|
|
10771
|
+
type: envelope.event?.type
|
|
10772
|
+
};
|
|
10773
|
+
}, {
|
|
10774
|
+
parse: "text"
|
|
10775
|
+
});
|
|
10776
|
+
};
|
|
7557
10777
|
// src/queue.ts
|
|
7558
10778
|
var releaseLeaseScript = `
|
|
7559
10779
|
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
|
@@ -7625,6 +10845,8 @@ var shouldDeadLetterSinkEvent = (event, sinks, maxFailures) => typeof maxFailure
|
|
|
7625
10845
|
var shouldDeadLetterTask = (task, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (task.processingAttempts ?? 0) >= maxFailures;
|
|
7626
10846
|
var shouldProcessTraceDeliveryStatus = (status, allowed) => allowed.includes(status);
|
|
7627
10847
|
var shouldDeadLetterTraceDelivery = (delivery, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (delivery.deliveryAttempts ?? 0) >= maxFailures;
|
|
10848
|
+
var shouldProcessHandoffDeliveryStatus = (status, allowed) => allowed.includes(status);
|
|
10849
|
+
var shouldDeadLetterHandoffDelivery = (delivery, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (delivery.deliveryAttempts ?? 0) >= maxFailures;
|
|
7628
10850
|
var summarizeVoiceIntegrationEvents = (events, input = {}) => {
|
|
7629
10851
|
const buildSummary = async () => {
|
|
7630
10852
|
const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((event) => event.id) : []);
|
|
@@ -7706,6 +10928,48 @@ var summarizeVoiceTraceSinkDeliveries = (deliveries, input = {}) => {
|
|
|
7706
10928
|
};
|
|
7707
10929
|
return buildSummary();
|
|
7708
10930
|
};
|
|
10931
|
+
var summarizeVoiceHandoffDeliveries = (deliveries, input = {}) => {
|
|
10932
|
+
const buildSummary = async () => {
|
|
10933
|
+
const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((delivery) => delivery.id) : []);
|
|
10934
|
+
const byAction = new Map;
|
|
10935
|
+
const summary = {
|
|
10936
|
+
byAction: [],
|
|
10937
|
+
deadLettered: 0,
|
|
10938
|
+
delivered: 0,
|
|
10939
|
+
failed: 0,
|
|
10940
|
+
pending: 0,
|
|
10941
|
+
retryEligible: 0,
|
|
10942
|
+
skipped: 0,
|
|
10943
|
+
total: deliveries.length
|
|
10944
|
+
};
|
|
10945
|
+
for (const delivery of deliveries) {
|
|
10946
|
+
byAction.set(delivery.action, (byAction.get(delivery.action) ?? 0) + 1);
|
|
10947
|
+
if (deadLetterIds.has(delivery.id)) {
|
|
10948
|
+
summary.deadLettered += 1;
|
|
10949
|
+
}
|
|
10950
|
+
switch (delivery.deliveryStatus) {
|
|
10951
|
+
case "delivered":
|
|
10952
|
+
summary.delivered += 1;
|
|
10953
|
+
break;
|
|
10954
|
+
case "failed":
|
|
10955
|
+
summary.failed += 1;
|
|
10956
|
+
if ((delivery.deliveryAttempts ?? 0) > 0) {
|
|
10957
|
+
summary.retryEligible += 1;
|
|
10958
|
+
}
|
|
10959
|
+
break;
|
|
10960
|
+
case "skipped":
|
|
10961
|
+
summary.skipped += 1;
|
|
10962
|
+
break;
|
|
10963
|
+
case "pending":
|
|
10964
|
+
summary.pending += 1;
|
|
10965
|
+
break;
|
|
10966
|
+
}
|
|
10967
|
+
}
|
|
10968
|
+
summary.byAction = [...byAction.entries()].sort((left, right) => right[1] - left[1]);
|
|
10969
|
+
return summary;
|
|
10970
|
+
};
|
|
10971
|
+
return buildSummary();
|
|
10972
|
+
};
|
|
7709
10973
|
var summarizeVoiceOpsTaskQueue = (tasks, input = {}) => {
|
|
7710
10974
|
const buildSummary = async () => {
|
|
7711
10975
|
const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((task) => task.id) : []);
|
|
@@ -8135,6 +11399,108 @@ var createVoiceTraceSinkDeliveryWorkerLoop = (options) => {
|
|
|
8135
11399
|
tick
|
|
8136
11400
|
};
|
|
8137
11401
|
};
|
|
11402
|
+
var createVoiceHandoffDeliveryWorker = (options) => {
|
|
11403
|
+
const allowedStatuses = options.statuses ?? ["pending", "failed"];
|
|
11404
|
+
const leaseMs = Math.max(1, options.leaseMs ?? 30000);
|
|
11405
|
+
return {
|
|
11406
|
+
drain: async () => {
|
|
11407
|
+
const result = {
|
|
11408
|
+
alreadyProcessed: 0,
|
|
11409
|
+
attempted: 0,
|
|
11410
|
+
deadLettered: 0,
|
|
11411
|
+
delivered: 0,
|
|
11412
|
+
failed: 0,
|
|
11413
|
+
skipped: 0
|
|
11414
|
+
};
|
|
11415
|
+
const deliveries = [...await options.deliveries.list()].sort((left, right) => left.createdAt - right.createdAt);
|
|
11416
|
+
for (const delivery of deliveries) {
|
|
11417
|
+
if (!shouldProcessHandoffDeliveryStatus(delivery.deliveryStatus, allowedStatuses)) {
|
|
11418
|
+
continue;
|
|
11419
|
+
}
|
|
11420
|
+
if (shouldDeadLetterHandoffDelivery(delivery, options.maxFailures)) {
|
|
11421
|
+
await options.deadLetters?.set(delivery.id, delivery);
|
|
11422
|
+
await options.onDeadLetter?.(delivery);
|
|
11423
|
+
result.deadLettered += 1;
|
|
11424
|
+
continue;
|
|
11425
|
+
}
|
|
11426
|
+
const claimed = await options.leases.claim({
|
|
11427
|
+
leaseMs,
|
|
11428
|
+
taskId: delivery.id,
|
|
11429
|
+
workerId: options.workerId
|
|
11430
|
+
});
|
|
11431
|
+
if (!claimed) {
|
|
11432
|
+
continue;
|
|
11433
|
+
}
|
|
11434
|
+
try {
|
|
11435
|
+
const idempotencyKey = `${delivery.id}:handoff`;
|
|
11436
|
+
if (options.idempotency && await options.idempotency.has(idempotencyKey)) {
|
|
11437
|
+
result.alreadyProcessed += 1;
|
|
11438
|
+
continue;
|
|
11439
|
+
}
|
|
11440
|
+
result.attempted += 1;
|
|
11441
|
+
const updatedDelivery = await deliverVoiceHandoffDelivery({
|
|
11442
|
+
adapters: options.adapters,
|
|
11443
|
+
api: options.api,
|
|
11444
|
+
delivery,
|
|
11445
|
+
failMode: options.failMode
|
|
11446
|
+
});
|
|
11447
|
+
await options.deliveries.set(updatedDelivery.id, updatedDelivery);
|
|
11448
|
+
if (updatedDelivery.deliveryStatus === "delivered" || updatedDelivery.deliveryStatus === "skipped") {
|
|
11449
|
+
await options.idempotency?.set(idempotencyKey, {
|
|
11450
|
+
ttlSeconds: options.idempotencyTtlSeconds
|
|
11451
|
+
});
|
|
11452
|
+
}
|
|
11453
|
+
if (updatedDelivery.deliveryStatus === "delivered") {
|
|
11454
|
+
result.delivered += 1;
|
|
11455
|
+
} else if (updatedDelivery.deliveryStatus === "skipped") {
|
|
11456
|
+
result.skipped += 1;
|
|
11457
|
+
} else if (updatedDelivery.deliveryStatus === "failed") {
|
|
11458
|
+
result.failed += 1;
|
|
11459
|
+
if (shouldDeadLetterHandoffDelivery(updatedDelivery, options.maxFailures)) {
|
|
11460
|
+
await options.deadLetters?.set(updatedDelivery.id, updatedDelivery);
|
|
11461
|
+
await options.onDeadLetter?.(updatedDelivery);
|
|
11462
|
+
result.deadLettered += 1;
|
|
11463
|
+
}
|
|
11464
|
+
}
|
|
11465
|
+
} finally {
|
|
11466
|
+
await options.leases.release({
|
|
11467
|
+
taskId: delivery.id,
|
|
11468
|
+
workerId: options.workerId
|
|
11469
|
+
});
|
|
11470
|
+
}
|
|
11471
|
+
}
|
|
11472
|
+
return result;
|
|
11473
|
+
}
|
|
11474
|
+
};
|
|
11475
|
+
};
|
|
11476
|
+
var createVoiceHandoffDeliveryWorkerLoop = (options) => {
|
|
11477
|
+
const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 1000);
|
|
11478
|
+
let timer;
|
|
11479
|
+
let running = false;
|
|
11480
|
+
const tick = async () => options.worker.drain();
|
|
11481
|
+
return {
|
|
11482
|
+
isRunning: () => running,
|
|
11483
|
+
start: () => {
|
|
11484
|
+
if (timer) {
|
|
11485
|
+
return;
|
|
11486
|
+
}
|
|
11487
|
+
running = true;
|
|
11488
|
+
timer = setInterval(() => {
|
|
11489
|
+
tick().catch((error) => {
|
|
11490
|
+
options.onError?.(error);
|
|
11491
|
+
});
|
|
11492
|
+
}, pollIntervalMs);
|
|
11493
|
+
},
|
|
11494
|
+
stop: () => {
|
|
11495
|
+
if (timer) {
|
|
11496
|
+
clearInterval(timer);
|
|
11497
|
+
timer = undefined;
|
|
11498
|
+
}
|
|
11499
|
+
running = false;
|
|
11500
|
+
},
|
|
11501
|
+
tick
|
|
11502
|
+
};
|
|
11503
|
+
};
|
|
8138
11504
|
var createVoiceOpsTaskWorker = (options) => {
|
|
8139
11505
|
const leaseMs = Math.max(1, options.leaseMs ?? 30000);
|
|
8140
11506
|
const getTask = async (taskId) => {
|
|
@@ -8270,10 +11636,10 @@ var createVoiceOpsTaskProcessorWorker = (options) => ({
|
|
|
8270
11636
|
result.completed += 1;
|
|
8271
11637
|
} catch (error) {
|
|
8272
11638
|
await options.onError?.(error, task);
|
|
8273
|
-
const
|
|
11639
|
+
const errorMessage3 = error instanceof Error ? error.message : String(error);
|
|
8274
11640
|
const failedTask = failVoiceOpsTask(task, {
|
|
8275
11641
|
actor: task.claimedBy ?? "ops-worker",
|
|
8276
|
-
error:
|
|
11642
|
+
error: errorMessage3
|
|
8277
11643
|
});
|
|
8278
11644
|
if (shouldDeadLetterTask(failedTask, options.maxFailures)) {
|
|
8279
11645
|
const deadLetterTask = deadLetterVoiceOpsTask(failedTask, {
|
|
@@ -9086,7 +12452,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
|
|
|
9086
12452
|
import { Buffer as Buffer2 } from "buffer";
|
|
9087
12453
|
var TWILIO_MULAW_SAMPLE_RATE = 8000;
|
|
9088
12454
|
var VOICE_PCM_SAMPLE_RATE = 16000;
|
|
9089
|
-
var
|
|
12455
|
+
var escapeXml2 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
9090
12456
|
var normalizeOnTurn2 = (handler) => {
|
|
9091
12457
|
if (handler.length > 1) {
|
|
9092
12458
|
const directHandler = handler;
|
|
@@ -9282,8 +12648,8 @@ var createTwilioSocketAdapter = (socket, getState) => ({
|
|
|
9282
12648
|
}
|
|
9283
12649
|
});
|
|
9284
12650
|
var createTwilioVoiceResponse = (options) => {
|
|
9285
|
-
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${
|
|
9286
|
-
return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${
|
|
12651
|
+
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
|
|
12652
|
+
return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${escapeXml2(options.streamUrl)}"${options.track ? ` track="${escapeXml2(options.track)}"` : ""}${options.streamName ? ` name="${escapeXml2(options.streamName)}"` : ""}>${parameters}</Stream></Connect></Response>`;
|
|
9287
12653
|
};
|
|
9288
12654
|
var createTwilioMediaStreamBridge = (socket, options) => {
|
|
9289
12655
|
const runtimePreset = resolveVoiceRuntimePreset(options.preset);
|
|
@@ -9519,15 +12885,22 @@ export {
|
|
|
9519
12885
|
withVoiceOpsTaskId,
|
|
9520
12886
|
withVoiceIntegrationEventId,
|
|
9521
12887
|
voice,
|
|
12888
|
+
verifyVoiceOpsWebhookSignature,
|
|
9522
12889
|
transcodeTwilioInboundPayloadToPCM16,
|
|
9523
12890
|
transcodePCMToTwilioOutboundPayload,
|
|
9524
12891
|
summarizeVoiceTraceSinkDeliveries,
|
|
9525
12892
|
summarizeVoiceTrace,
|
|
12893
|
+
summarizeVoiceSessions,
|
|
12894
|
+
summarizeVoiceSessionReplay,
|
|
12895
|
+
summarizeVoiceProviderHealth,
|
|
9526
12896
|
summarizeVoiceOpsTasks,
|
|
9527
12897
|
summarizeVoiceOpsTaskQueue,
|
|
9528
12898
|
summarizeVoiceOpsTaskAnalytics,
|
|
9529
12899
|
summarizeVoiceIntegrationEvents,
|
|
12900
|
+
summarizeVoiceHandoffHealth,
|
|
12901
|
+
summarizeVoiceHandoffDeliveries,
|
|
9530
12902
|
summarizeVoiceAssistantRuns,
|
|
12903
|
+
summarizeVoiceAssistantHealth,
|
|
9531
12904
|
startVoiceOpsTask,
|
|
9532
12905
|
shapeTelephonyAssistantText,
|
|
9533
12906
|
selectVoiceTraceEventsForPrune,
|
|
@@ -9539,6 +12912,7 @@ export {
|
|
|
9539
12912
|
resolveVoiceOpsTaskAssignment,
|
|
9540
12913
|
resolveVoiceOpsTaskAgeBucket,
|
|
9541
12914
|
resolveVoiceOpsPreset,
|
|
12915
|
+
resolveVoiceDiagnosticsTraceFilter,
|
|
9542
12916
|
resolveVoiceAssistantMemoryNamespace,
|
|
9543
12917
|
resolveTurnDetectionConfig,
|
|
9544
12918
|
resolveAudioConditioningConfig,
|
|
@@ -9546,8 +12920,15 @@ export {
|
|
|
9546
12920
|
reopenVoiceOpsTask,
|
|
9547
12921
|
renderVoiceTraceMarkdown,
|
|
9548
12922
|
renderVoiceTraceHTML,
|
|
12923
|
+
renderVoiceSessionsHTML,
|
|
12924
|
+
renderVoiceResilienceHTML,
|
|
12925
|
+
renderVoiceQualityHTML,
|
|
12926
|
+
renderVoiceProviderHealthHTML,
|
|
12927
|
+
renderVoiceOpsConsoleHTML,
|
|
12928
|
+
renderVoiceHandoffHealthHTML,
|
|
9549
12929
|
renderVoiceCallReviewMarkdown,
|
|
9550
12930
|
renderVoiceCallReviewHTML,
|
|
12931
|
+
renderVoiceAssistantHealthHTML,
|
|
9551
12932
|
redactVoiceTraceText,
|
|
9552
12933
|
redactVoiceTraceEvents,
|
|
9553
12934
|
redactVoiceTraceEvent,
|
|
@@ -9555,6 +12936,7 @@ export {
|
|
|
9555
12936
|
pruneVoiceTraceEvents,
|
|
9556
12937
|
matchesVoiceOpsTaskAssignmentRule,
|
|
9557
12938
|
markVoiceOpsTaskSLABreached,
|
|
12939
|
+
listVoiceRoutingEvents,
|
|
9558
12940
|
listVoiceOpsTasks,
|
|
9559
12941
|
isVoiceOpsTaskOverdue,
|
|
9560
12942
|
heartbeatVoiceOpsTask,
|
|
@@ -9563,17 +12945,22 @@ export {
|
|
|
9563
12945
|
failVoiceOpsTask,
|
|
9564
12946
|
exportVoiceTrace,
|
|
9565
12947
|
evaluateVoiceTrace,
|
|
12948
|
+
evaluateVoiceQuality,
|
|
9566
12949
|
encodeTwilioMulawBase64,
|
|
9567
12950
|
deliverVoiceTraceEventsToSinks,
|
|
9568
12951
|
deliverVoiceIntegrationEventToSinks,
|
|
9569
12952
|
deliverVoiceIntegrationEvent,
|
|
12953
|
+
deliverVoiceHandoffDelivery,
|
|
12954
|
+
deliverVoiceHandoff,
|
|
9570
12955
|
decodeTwilioMulawBase64,
|
|
9571
12956
|
deadLetterVoiceOpsTask,
|
|
9572
12957
|
createVoiceZendeskTicketUpdateSink,
|
|
9573
12958
|
createVoiceZendeskTicketSyncSinks,
|
|
9574
12959
|
createVoiceZendeskTicketSink,
|
|
12960
|
+
createVoiceWebhookHandoffAdapter,
|
|
9575
12961
|
createVoiceWebhookDeliveryWorkerLoop,
|
|
9576
12962
|
createVoiceWebhookDeliveryWorker,
|
|
12963
|
+
createVoiceTwilioRedirectHandoffAdapter,
|
|
9577
12964
|
createVoiceTraceSinkStore,
|
|
9578
12965
|
createVoiceTraceSinkDeliveryWorkerLoop,
|
|
9579
12966
|
createVoiceTraceSinkDeliveryWorker,
|
|
@@ -9585,9 +12972,17 @@ export {
|
|
|
9585
12972
|
createVoiceTaskUpdatedEvent,
|
|
9586
12973
|
createVoiceTaskSLABreachedEvent,
|
|
9587
12974
|
createVoiceTaskCreatedEvent,
|
|
12975
|
+
createVoiceTTSProviderRouter,
|
|
12976
|
+
createVoiceSessionsJSONHandler,
|
|
12977
|
+
createVoiceSessionsHTMLHandler,
|
|
12978
|
+
createVoiceSessionReplayRoutes,
|
|
12979
|
+
createVoiceSessionReplayJSONHandler,
|
|
12980
|
+
createVoiceSessionReplayHTMLHandler,
|
|
9588
12981
|
createVoiceSessionRecord,
|
|
12982
|
+
createVoiceSessionListRoutes,
|
|
9589
12983
|
createVoiceSession,
|
|
9590
12984
|
createVoiceSTTRoutingCorrectionHandler,
|
|
12985
|
+
createVoiceSTTProviderRouter,
|
|
9591
12986
|
createVoiceSQLiteTraceSinkDeliveryStore,
|
|
9592
12987
|
createVoiceSQLiteTraceEventStore,
|
|
9593
12988
|
createVoiceSQLiteTaskStore,
|
|
@@ -9598,8 +12993,14 @@ export {
|
|
|
9598
12993
|
createVoiceSQLiteExternalObjectMapStore,
|
|
9599
12994
|
createVoiceS3ReviewStore,
|
|
9600
12995
|
createVoiceReviewSavedEvent,
|
|
12996
|
+
createVoiceResilienceRoutes,
|
|
9601
12997
|
createVoiceRedisTaskLeaseCoordinator,
|
|
9602
12998
|
createVoiceRedisIdempotencyStore,
|
|
12999
|
+
createVoiceQualityRoutes,
|
|
13000
|
+
createVoiceProviderRouter,
|
|
13001
|
+
createVoiceProviderHealthRoutes,
|
|
13002
|
+
createVoiceProviderHealthJSONHandler,
|
|
13003
|
+
createVoiceProviderHealthHTMLHandler,
|
|
9603
13004
|
createVoicePostgresTraceSinkDeliveryStore,
|
|
9604
13005
|
createVoicePostgresTraceEventStore,
|
|
9605
13006
|
createVoicePostgresTaskStore,
|
|
@@ -9608,13 +13009,18 @@ export {
|
|
|
9608
13009
|
createVoicePostgresReviewStore,
|
|
9609
13010
|
createVoicePostgresIntegrationEventStore,
|
|
9610
13011
|
createVoicePostgresExternalObjectMapStore,
|
|
13012
|
+
createVoiceOpsWebhookSink,
|
|
13013
|
+
createVoiceOpsWebhookReceiverRoutes,
|
|
13014
|
+
createVoiceOpsWebhookEnvelope,
|
|
9611
13015
|
createVoiceOpsTaskWorker,
|
|
9612
13016
|
createVoiceOpsTaskProcessorWorkerLoop,
|
|
9613
13017
|
createVoiceOpsTaskProcessorWorker,
|
|
9614
13018
|
createVoiceOpsRuntime,
|
|
13019
|
+
createVoiceOpsConsoleRoutes,
|
|
9615
13020
|
createVoiceMemoryTraceSinkDeliveryStore,
|
|
9616
13021
|
createVoiceMemoryTraceEventStore,
|
|
9617
13022
|
createVoiceMemoryStore,
|
|
13023
|
+
createVoiceMemoryHandoffDeliveryStore,
|
|
9618
13024
|
createVoiceMemoryAssistantMemoryStore,
|
|
9619
13025
|
createVoiceLinearIssueUpdateSink,
|
|
9620
13026
|
createVoiceLinearIssueSyncSinks,
|
|
@@ -9627,6 +13033,12 @@ export {
|
|
|
9627
13033
|
createVoiceHubSpotTaskSyncSinks,
|
|
9628
13034
|
createVoiceHubSpotTaskSink,
|
|
9629
13035
|
createVoiceHelpdeskTicketSink,
|
|
13036
|
+
createVoiceHandoffHealthRoutes,
|
|
13037
|
+
createVoiceHandoffHealthJSONHandler,
|
|
13038
|
+
createVoiceHandoffHealthHTMLHandler,
|
|
13039
|
+
createVoiceHandoffDeliveryWorkerLoop,
|
|
13040
|
+
createVoiceHandoffDeliveryWorker,
|
|
13041
|
+
createVoiceHandoffDeliveryRecord,
|
|
9630
13042
|
createVoiceFileTraceSinkDeliveryStore,
|
|
9631
13043
|
createVoiceFileTraceEventStore,
|
|
9632
13044
|
createVoiceFileTaskStore,
|
|
@@ -9639,6 +13051,7 @@ export {
|
|
|
9639
13051
|
createVoiceExternalObjectMapId,
|
|
9640
13052
|
createVoiceExternalObjectMap,
|
|
9641
13053
|
createVoiceExperiment,
|
|
13054
|
+
createVoiceDiagnosticsRoutes,
|
|
9642
13055
|
createVoiceCallReviewRecorder,
|
|
9643
13056
|
createVoiceCallReviewFromSession,
|
|
9644
13057
|
createVoiceCallReviewFromLiveTelephonyReport,
|
|
@@ -9646,6 +13059,9 @@ export {
|
|
|
9646
13059
|
createVoiceCRMActivitySink,
|
|
9647
13060
|
createVoiceAssistantMemoryRecord,
|
|
9648
13061
|
createVoiceAssistantMemoryHandle,
|
|
13062
|
+
createVoiceAssistantHealthRoutes,
|
|
13063
|
+
createVoiceAssistantHealthJSONHandler,
|
|
13064
|
+
createVoiceAssistantHealthHTMLHandler,
|
|
9649
13065
|
createVoiceAssistant,
|
|
9650
13066
|
createVoiceAgentTool,
|
|
9651
13067
|
createVoiceAgentSquad,
|
|
@@ -9658,18 +13074,25 @@ export {
|
|
|
9658
13074
|
createStoredVoiceCallReviewArtifact,
|
|
9659
13075
|
createRiskyTurnCorrectionHandler,
|
|
9660
13076
|
createPhraseHintCorrectionHandler,
|
|
13077
|
+
createOpenAIVoiceAssistantModel,
|
|
13078
|
+
createJSONVoiceAssistantModel,
|
|
9661
13079
|
createId,
|
|
13080
|
+
createGeminiVoiceAssistantModel,
|
|
9662
13081
|
createDomainPhraseHints,
|
|
9663
13082
|
createDomainLexicon,
|
|
13083
|
+
createAnthropicVoiceAssistantModel,
|
|
9664
13084
|
conditionAudioChunk,
|
|
9665
13085
|
completeVoiceOpsTask,
|
|
9666
13086
|
claimVoiceOpsTask,
|
|
9667
13087
|
buildVoiceTraceReplay,
|
|
9668
13088
|
buildVoiceOpsTaskFromSLABreach,
|
|
9669
13089
|
buildVoiceOpsTaskFromReview,
|
|
13090
|
+
buildVoiceOpsConsoleReport,
|
|
13091
|
+
buildVoiceDiagnosticsMarkdown,
|
|
9670
13092
|
assignVoiceOpsTask,
|
|
9671
13093
|
applyVoiceOpsTaskPolicy,
|
|
9672
13094
|
applyVoiceOpsTaskAssignmentRule,
|
|
13095
|
+
applyVoiceHandoffDeliveryResult,
|
|
9673
13096
|
applyRiskTieredPhraseHintCorrections,
|
|
9674
13097
|
applyPhraseHintCorrections,
|
|
9675
13098
|
TURN_PROFILE_DEFAULTS
|