@absolutejs/voice 0.0.22-beta.3 → 0.0.22-beta.31
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/assistant.d.ts +20 -0
- package/dist/assistantHealth.d.ts +81 -0
- package/dist/assistantMemory.d.ts +63 -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/fileStore.d.ts +5 -2
- package/dist/handoff.d.ts +54 -0
- package/dist/handoffHealth.d.ts +94 -0
- package/dist/index.d.ts +20 -4
- package/dist/index.js +2509 -21
- package/dist/modelAdapters.d.ts +93 -0
- package/dist/opsWebhook.d.ts +126 -0
- package/dist/providerHealth.d.ts +78 -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/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 +1 -0
- package/dist/testing/index.js +1310 -7
- 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",
|
|
@@ -5594,6 +6026,112 @@ var resolveVoiceOutcomeRecipe = (name, options = {}) => {
|
|
|
5594
6026
|
};
|
|
5595
6027
|
};
|
|
5596
6028
|
|
|
6029
|
+
// src/assistantMemory.ts
|
|
6030
|
+
var createMemoryId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
|
|
6031
|
+
var createVoiceAssistantMemoryRecord = (input) => {
|
|
6032
|
+
const now = Date.now();
|
|
6033
|
+
return {
|
|
6034
|
+
...input,
|
|
6035
|
+
createdAt: input.createdAt ?? input.updatedAt ?? now,
|
|
6036
|
+
updatedAt: input.updatedAt ?? now
|
|
6037
|
+
};
|
|
6038
|
+
};
|
|
6039
|
+
var createVoiceMemoryAssistantMemoryStore = () => {
|
|
6040
|
+
const records = new Map;
|
|
6041
|
+
return {
|
|
6042
|
+
delete: async (input) => {
|
|
6043
|
+
records.delete(createMemoryId(input));
|
|
6044
|
+
},
|
|
6045
|
+
get: async (input) => records.get(createMemoryId(input)),
|
|
6046
|
+
list: async (input) => [...records.values()].filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt),
|
|
6047
|
+
set: async (input) => {
|
|
6048
|
+
const id = createMemoryId(input);
|
|
6049
|
+
const existing = records.get(id);
|
|
6050
|
+
const record = createVoiceAssistantMemoryRecord({
|
|
6051
|
+
...input,
|
|
6052
|
+
createdAt: input.createdAt ?? existing?.createdAt,
|
|
6053
|
+
updatedAt: input.updatedAt
|
|
6054
|
+
});
|
|
6055
|
+
records.set(id, record);
|
|
6056
|
+
return record;
|
|
6057
|
+
}
|
|
6058
|
+
};
|
|
6059
|
+
};
|
|
6060
|
+
var resolveVoiceAssistantMemoryNamespace = async (input) => typeof input.memory.namespace === "function" ? await input.memory.namespace(input) : input.memory.namespace;
|
|
6061
|
+
var createVoiceAssistantMemoryHandle = async (input) => {
|
|
6062
|
+
const namespace = await resolveVoiceAssistantMemoryNamespace({
|
|
6063
|
+
assistantId: input.assistantId,
|
|
6064
|
+
context: input.context,
|
|
6065
|
+
memory: input.memory,
|
|
6066
|
+
session: input.session
|
|
6067
|
+
});
|
|
6068
|
+
const trace = async (event) => {
|
|
6069
|
+
await input.trace?.append({
|
|
6070
|
+
at: Date.now(),
|
|
6071
|
+
payload: {
|
|
6072
|
+
assistantId: input.assistantId,
|
|
6073
|
+
namespace,
|
|
6074
|
+
...event
|
|
6075
|
+
},
|
|
6076
|
+
scenarioId: input.session.scenarioId,
|
|
6077
|
+
sessionId: input.session.id,
|
|
6078
|
+
type: "assistant.memory"
|
|
6079
|
+
});
|
|
6080
|
+
};
|
|
6081
|
+
return {
|
|
6082
|
+
delete: async (key) => {
|
|
6083
|
+
await input.memory.store.delete({
|
|
6084
|
+
assistantId: input.assistantId,
|
|
6085
|
+
key,
|
|
6086
|
+
namespace
|
|
6087
|
+
});
|
|
6088
|
+
await trace({
|
|
6089
|
+
action: "delete",
|
|
6090
|
+
key
|
|
6091
|
+
});
|
|
6092
|
+
},
|
|
6093
|
+
get: async (key) => {
|
|
6094
|
+
const record = await input.memory.store.get({
|
|
6095
|
+
assistantId: input.assistantId,
|
|
6096
|
+
key,
|
|
6097
|
+
namespace
|
|
6098
|
+
});
|
|
6099
|
+
await trace({
|
|
6100
|
+
action: "get",
|
|
6101
|
+
found: Boolean(record),
|
|
6102
|
+
key
|
|
6103
|
+
});
|
|
6104
|
+
return record?.value;
|
|
6105
|
+
},
|
|
6106
|
+
list: async () => {
|
|
6107
|
+
const records = await input.memory.store.list({
|
|
6108
|
+
assistantId: input.assistantId,
|
|
6109
|
+
namespace
|
|
6110
|
+
});
|
|
6111
|
+
await trace({
|
|
6112
|
+
action: "list",
|
|
6113
|
+
count: records.length
|
|
6114
|
+
});
|
|
6115
|
+
return records;
|
|
6116
|
+
},
|
|
6117
|
+
namespace,
|
|
6118
|
+
set: async (key, value, metadata) => {
|
|
6119
|
+
const record = await input.memory.store.set({
|
|
6120
|
+
assistantId: input.assistantId,
|
|
6121
|
+
key,
|
|
6122
|
+
metadata,
|
|
6123
|
+
namespace,
|
|
6124
|
+
value
|
|
6125
|
+
});
|
|
6126
|
+
await trace({
|
|
6127
|
+
action: "set",
|
|
6128
|
+
key
|
|
6129
|
+
});
|
|
6130
|
+
return record;
|
|
6131
|
+
}
|
|
6132
|
+
};
|
|
6133
|
+
};
|
|
6134
|
+
|
|
5597
6135
|
// src/assistant.ts
|
|
5598
6136
|
var hashString = (value) => {
|
|
5599
6137
|
let hash = 2166136261;
|
|
@@ -5742,12 +6280,35 @@ var createVoiceAssistant = (options) => {
|
|
|
5742
6280
|
});
|
|
5743
6281
|
}
|
|
5744
6282
|
const onTurn = async (input) => {
|
|
6283
|
+
const memory = options.memory ? await createVoiceAssistantMemoryHandle({
|
|
6284
|
+
assistantId: options.id,
|
|
6285
|
+
context: input.context,
|
|
6286
|
+
memory: options.memory,
|
|
6287
|
+
session: input.session,
|
|
6288
|
+
trace: options.trace
|
|
6289
|
+
}) : undefined;
|
|
5745
6290
|
const guardrailInput = {
|
|
5746
6291
|
...input,
|
|
5747
|
-
assistantId: options.id
|
|
6292
|
+
assistantId: options.id,
|
|
6293
|
+
memory
|
|
5748
6294
|
};
|
|
6295
|
+
if (memory) {
|
|
6296
|
+
await options.memoryLifecycle?.beforeTurn?.({
|
|
6297
|
+
...input,
|
|
6298
|
+
assistantId: options.id,
|
|
6299
|
+
memory
|
|
6300
|
+
});
|
|
6301
|
+
}
|
|
5749
6302
|
const blocked = await options.guardrails?.beforeTurn?.(guardrailInput);
|
|
5750
6303
|
if (blocked) {
|
|
6304
|
+
if (memory) {
|
|
6305
|
+
await options.memoryLifecycle?.afterTurn?.({
|
|
6306
|
+
...input,
|
|
6307
|
+
assistantId: options.id,
|
|
6308
|
+
memory,
|
|
6309
|
+
result: blocked
|
|
6310
|
+
});
|
|
6311
|
+
}
|
|
5751
6312
|
await appendAssistantTrace({
|
|
5752
6313
|
assistantId: options.id,
|
|
5753
6314
|
event: {
|
|
@@ -5797,6 +6358,14 @@ var createVoiceAssistant = (options) => {
|
|
|
5797
6358
|
result
|
|
5798
6359
|
});
|
|
5799
6360
|
const finalResult = guarded ?? result;
|
|
6361
|
+
if (memory) {
|
|
6362
|
+
await options.memoryLifecycle?.afterTurn?.({
|
|
6363
|
+
...input,
|
|
6364
|
+
assistantId: options.id,
|
|
6365
|
+
memory,
|
|
6366
|
+
result: finalResult
|
|
6367
|
+
});
|
|
6368
|
+
}
|
|
5800
6369
|
if (guarded) {
|
|
5801
6370
|
await appendAssistantTrace({
|
|
5802
6371
|
assistantId: options.id,
|
|
@@ -5864,6 +6433,12 @@ var summarizeVoiceAssistantRuns = async (input) => {
|
|
|
5864
6433
|
escalationCount: 0,
|
|
5865
6434
|
experiments: {},
|
|
5866
6435
|
guardrailCount: 0,
|
|
6436
|
+
memory: {
|
|
6437
|
+
deletes: 0,
|
|
6438
|
+
gets: 0,
|
|
6439
|
+
lists: 0,
|
|
6440
|
+
sets: 0
|
|
6441
|
+
},
|
|
5867
6442
|
outcomes: {},
|
|
5868
6443
|
runCount: 0,
|
|
5869
6444
|
sessionIds: new Set,
|
|
@@ -5919,6 +6494,24 @@ var summarizeVoiceAssistantRuns = async (input) => {
|
|
|
5919
6494
|
const summary = getSummary(assistantId);
|
|
5920
6495
|
summary.guardrailCount += 1;
|
|
5921
6496
|
}
|
|
6497
|
+
for (const event of events.filter((event2) => event2.type === "assistant.memory")) {
|
|
6498
|
+
const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
|
|
6499
|
+
const summary = getSummary(assistantId);
|
|
6500
|
+
switch (event.payload.action) {
|
|
6501
|
+
case "delete":
|
|
6502
|
+
summary.memory.deletes += 1;
|
|
6503
|
+
break;
|
|
6504
|
+
case "get":
|
|
6505
|
+
summary.memory.gets += 1;
|
|
6506
|
+
break;
|
|
6507
|
+
case "list":
|
|
6508
|
+
summary.memory.lists += 1;
|
|
6509
|
+
break;
|
|
6510
|
+
case "set":
|
|
6511
|
+
summary.memory.sets += 1;
|
|
6512
|
+
break;
|
|
6513
|
+
}
|
|
6514
|
+
}
|
|
5922
6515
|
const assistants = [...byAssistant.values()].map(({ elapsedCount, elapsedTotal, sessionIds, ...summary }) => ({
|
|
5923
6516
|
...summary,
|
|
5924
6517
|
averageElapsedMs: elapsedCount > 0 ? Math.round(elapsedTotal / elapsedCount) : undefined,
|
|
@@ -5929,9 +6522,302 @@ var summarizeVoiceAssistantRuns = async (input) => {
|
|
|
5929
6522
|
totalRuns: assistantRuns.length
|
|
5930
6523
|
};
|
|
5931
6524
|
};
|
|
5932
|
-
// src/
|
|
5933
|
-
import {
|
|
5934
|
-
|
|
6525
|
+
// src/assistantHealth.ts
|
|
6526
|
+
import { Elysia as Elysia3 } from "elysia";
|
|
6527
|
+
|
|
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
|
+
};
|
|
6557
|
+
entries.set(provider, entry);
|
|
6558
|
+
return entry;
|
|
6559
|
+
};
|
|
6560
|
+
for (const provider of providers) {
|
|
6561
|
+
getEntry(provider);
|
|
6562
|
+
}
|
|
6563
|
+
const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
|
|
6564
|
+
for (const event of events) {
|
|
6565
|
+
if (event.type === "assistant.run") {
|
|
6566
|
+
if (hasProviderRouterEvents) {
|
|
6567
|
+
continue;
|
|
6568
|
+
}
|
|
6569
|
+
const provider2 = event.payload.variantId;
|
|
6570
|
+
if (!isAllowedProvider(provider2)) {
|
|
6571
|
+
continue;
|
|
6572
|
+
}
|
|
6573
|
+
const entry2 = getEntry(provider2);
|
|
6574
|
+
entry2.runCount += 1;
|
|
6575
|
+
const elapsedMs = getNumber(event.payload.elapsedMs);
|
|
6576
|
+
if (elapsedMs !== undefined) {
|
|
6577
|
+
entry2.elapsedCount += 1;
|
|
6578
|
+
entry2.elapsedTotal += elapsedMs;
|
|
6579
|
+
}
|
|
6580
|
+
continue;
|
|
6581
|
+
}
|
|
6582
|
+
if (event.type !== "session.error") {
|
|
6583
|
+
continue;
|
|
6584
|
+
}
|
|
6585
|
+
const provider = event.payload.provider;
|
|
6586
|
+
if (!isAllowedProvider(provider)) {
|
|
6587
|
+
continue;
|
|
6588
|
+
}
|
|
6589
|
+
const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
6590
|
+
const applyProviderHealth = () => {
|
|
6591
|
+
const entry2 = getEntry(provider);
|
|
6592
|
+
const providerHealth = event.payload.providerHealth;
|
|
6593
|
+
if (providerHealth && typeof providerHealth === "object") {
|
|
6594
|
+
const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
|
|
6595
|
+
if (suppressedUntil2 !== undefined) {
|
|
6596
|
+
entry2.suppressedUntil = suppressedUntil2;
|
|
6597
|
+
}
|
|
6598
|
+
}
|
|
6599
|
+
const suppressedUntil = getNumber(event.payload.suppressedUntil);
|
|
6600
|
+
if (suppressedUntil !== undefined) {
|
|
6601
|
+
entry2.suppressedUntil = suppressedUntil;
|
|
6602
|
+
}
|
|
6603
|
+
const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
|
|
6604
|
+
if (suppressionRemainingMs !== undefined) {
|
|
6605
|
+
entry2.suppressionRemainingMs = suppressionRemainingMs;
|
|
6606
|
+
}
|
|
6607
|
+
return entry2;
|
|
6608
|
+
};
|
|
6609
|
+
if (providerStatus === "success" || providerStatus === "fallback") {
|
|
6610
|
+
const entry2 = applyProviderHealth();
|
|
6611
|
+
entry2.runCount += 1;
|
|
6612
|
+
entry2.lastSuccessAt = event.at;
|
|
6613
|
+
if (providerStatus === "success") {
|
|
6614
|
+
entry2.lastError = undefined;
|
|
6615
|
+
entry2.rateLimited = false;
|
|
6616
|
+
entry2.suppressedUntil = undefined;
|
|
6617
|
+
entry2.suppressionRemainingMs = undefined;
|
|
6618
|
+
}
|
|
6619
|
+
const elapsedMs = getNumber(event.payload.elapsedMs);
|
|
6620
|
+
if (elapsedMs !== undefined) {
|
|
6621
|
+
entry2.elapsedCount += 1;
|
|
6622
|
+
entry2.elapsedTotal += elapsedMs;
|
|
6623
|
+
}
|
|
6624
|
+
const selectedProvider = event.payload.selectedProvider;
|
|
6625
|
+
if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
|
|
6626
|
+
getEntry(selectedProvider).fallbackCount += 1;
|
|
6627
|
+
}
|
|
6628
|
+
continue;
|
|
6629
|
+
}
|
|
6630
|
+
const entry = applyProviderHealth();
|
|
6631
|
+
entry.errorCount += 1;
|
|
6632
|
+
entry.lastError = getString(event.payload.error);
|
|
6633
|
+
entry.lastErrorAt = event.at;
|
|
6634
|
+
entry.rateLimited ||= event.payload.rateLimited === true;
|
|
6635
|
+
}
|
|
6636
|
+
const summaries = [...entries.values()].map((entry) => {
|
|
6637
|
+
const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
|
|
6638
|
+
const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
|
|
6639
|
+
const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
|
|
6640
|
+
const recoverable = hadSuppression && !activeSuppression;
|
|
6641
|
+
const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
|
|
6642
|
+
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";
|
|
6643
|
+
return {
|
|
6644
|
+
averageElapsedMs,
|
|
6645
|
+
errorCount: entry.errorCount,
|
|
6646
|
+
fallbackCount: entry.fallbackCount,
|
|
6647
|
+
lastError: entry.lastError,
|
|
6648
|
+
lastErrorAt: entry.lastErrorAt,
|
|
6649
|
+
lastSuccessAt: entry.lastSuccessAt,
|
|
6650
|
+
provider: entry.provider,
|
|
6651
|
+
rateLimited: entry.rateLimited,
|
|
6652
|
+
recommended: false,
|
|
6653
|
+
runCount: entry.runCount,
|
|
6654
|
+
status,
|
|
6655
|
+
suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
|
|
6656
|
+
suppressedUntil: entry.suppressedUntil
|
|
6657
|
+
};
|
|
6658
|
+
});
|
|
6659
|
+
const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
|
|
6660
|
+
if (recommended) {
|
|
6661
|
+
recommended.recommended = true;
|
|
6662
|
+
}
|
|
6663
|
+
return summaries;
|
|
6664
|
+
};
|
|
6665
|
+
var escapeHtml3 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6666
|
+
var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
|
|
6667
|
+
'<div class="voice-provider-health">',
|
|
6668
|
+
...providers.map((provider) => {
|
|
6669
|
+
const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
|
|
6670
|
+
return [
|
|
6671
|
+
`<article class="voice-provider-card ${escapeHtml3(provider.status)}">`,
|
|
6672
|
+
'<div class="voice-provider-card-header">',
|
|
6673
|
+
`<strong>${escapeHtml3(provider.provider)}</strong>`,
|
|
6674
|
+
`<span>${escapeHtml3(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
|
|
6675
|
+
"</div>",
|
|
6676
|
+
"<dl>",
|
|
6677
|
+
`<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
|
|
6678
|
+
`<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
|
|
6679
|
+
`<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
|
|
6680
|
+
`<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
|
|
6681
|
+
"</dl>",
|
|
6682
|
+
suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
|
|
6683
|
+
provider.lastError ? `<p>${escapeHtml3(provider.lastError)}</p>` : "",
|
|
6684
|
+
"</article>"
|
|
6685
|
+
].join("");
|
|
6686
|
+
}),
|
|
6687
|
+
"</div>"
|
|
6688
|
+
].join("");
|
|
6689
|
+
var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
|
|
6690
|
+
var createVoiceProviderHealthHTMLHandler = (options) => async () => {
|
|
6691
|
+
const providers = await summarizeVoiceProviderHealth(options);
|
|
6692
|
+
const render = options.render ?? renderVoiceProviderHealthHTML;
|
|
6693
|
+
const body = await render(providers);
|
|
6694
|
+
return new Response(body, {
|
|
6695
|
+
headers: {
|
|
6696
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6697
|
+
...options.headers
|
|
6698
|
+
}
|
|
6699
|
+
});
|
|
6700
|
+
};
|
|
6701
|
+
var createVoiceProviderHealthRoutes = (options) => {
|
|
6702
|
+
const path = options.path ?? "/api/provider-status";
|
|
6703
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
6704
|
+
const routes = new Elysia2({
|
|
6705
|
+
name: options.name ?? "absolutejs-voice-provider-health"
|
|
6706
|
+
}).get(path, createVoiceProviderHealthJSONHandler(options));
|
|
6707
|
+
if (htmlPath) {
|
|
6708
|
+
routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
|
|
6709
|
+
}
|
|
6710
|
+
return routes;
|
|
6711
|
+
};
|
|
6712
|
+
|
|
6713
|
+
// src/assistantHealth.ts
|
|
6714
|
+
var escapeHtml4 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6715
|
+
var renderCountMap = (values) => {
|
|
6716
|
+
const entries = Object.entries(values).sort((left, right) => right[1] - left[1]);
|
|
6717
|
+
if (entries.length === 0) {
|
|
6718
|
+
return '<p class="voice-assistant-health-empty">No data yet.</p>';
|
|
6719
|
+
}
|
|
6720
|
+
return [
|
|
6721
|
+
'<div class="voice-assistant-health-metrics">',
|
|
6722
|
+
...entries.map(([label, value]) => `<div><span>${escapeHtml4(label)}</span><strong>${String(value)}</strong></div>`),
|
|
6723
|
+
"</div>"
|
|
6724
|
+
].join("");
|
|
6725
|
+
};
|
|
6726
|
+
var getString2 = (value) => typeof value === "string" ? value : undefined;
|
|
6727
|
+
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) => {
|
|
6728
|
+
const failure = {
|
|
6729
|
+
at: event.at,
|
|
6730
|
+
assistantId: getString2(event.payload.assistantId),
|
|
6731
|
+
error: getString2(event.payload.error),
|
|
6732
|
+
provider: getString2(event.payload.provider),
|
|
6733
|
+
rateLimited: event.payload.rateLimited === true ? true : undefined,
|
|
6734
|
+
sessionId: event.sessionId,
|
|
6735
|
+
status: getString2(event.payload.providerStatus),
|
|
6736
|
+
turnId: event.turnId,
|
|
6737
|
+
type: event.type
|
|
6738
|
+
};
|
|
6739
|
+
const href = replayHref === false ? undefined : typeof replayHref === "function" ? replayHref(failure) : `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
|
|
6740
|
+
return {
|
|
6741
|
+
...failure,
|
|
6742
|
+
replayHref: href
|
|
6743
|
+
};
|
|
6744
|
+
});
|
|
6745
|
+
var summarizeVoiceAssistantHealth = async (options) => {
|
|
6746
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
6747
|
+
return {
|
|
6748
|
+
assistantRuns: await summarizeVoiceAssistantRuns({ events }),
|
|
6749
|
+
providerHealth: await summarizeVoiceProviderHealth({
|
|
6750
|
+
events,
|
|
6751
|
+
providers: options.providers
|
|
6752
|
+
}),
|
|
6753
|
+
recentFailures: getRecentFailures(events, options.maxFailures ?? 8, options.replayHref)
|
|
6754
|
+
};
|
|
6755
|
+
};
|
|
6756
|
+
var renderVoiceAssistantHealthHTML = (summary) => {
|
|
6757
|
+
const assistant = summary.assistantRuns.assistants[0];
|
|
6758
|
+
const failures = summary.recentFailures;
|
|
6759
|
+
return [
|
|
6760
|
+
'<div class="voice-assistant-health">',
|
|
6761
|
+
'<section class="voice-assistant-health-grid">',
|
|
6762
|
+
`<article><span>Runs</span><strong>${String(assistant?.runCount ?? 0)}</strong></article>`,
|
|
6763
|
+
`<article><span>Sessions</span><strong>${String(assistant?.sessions ?? 0)}</strong></article>`,
|
|
6764
|
+
`<article><span>Guardrails</span><strong>${String(assistant?.guardrailCount ?? 0)}</strong></article>`,
|
|
6765
|
+
`<article><span>Avg latency</span><strong>${String(assistant?.averageElapsedMs ?? 0)}ms</strong></article>`,
|
|
6766
|
+
"</section>",
|
|
6767
|
+
"<section>",
|
|
6768
|
+
"<h3>Provider Health</h3>",
|
|
6769
|
+
renderVoiceProviderHealthHTML(summary.providerHealth),
|
|
6770
|
+
"</section>",
|
|
6771
|
+
'<section class="voice-assistant-health-columns">',
|
|
6772
|
+
`<article><h3>Outcomes</h3>${renderCountMap(assistant?.outcomes ?? {})}</article>`,
|
|
6773
|
+
`<article><h3>Variants</h3>${renderCountMap(assistant?.variants ?? {})}</article>`,
|
|
6774
|
+
`<article><h3>Tools</h3>${renderCountMap(assistant?.toolCalls ?? {})}</article>`,
|
|
6775
|
+
`<article><h3>Artifact Plans</h3>${renderCountMap(assistant?.artifactPlans ?? {})}</article>`,
|
|
6776
|
+
"</section>",
|
|
6777
|
+
"<section>",
|
|
6778
|
+
"<h3>Recent Failures</h3>",
|
|
6779
|
+
failures.length === 0 ? '<p class="voice-assistant-health-empty">No failures yet.</p>' : [
|
|
6780
|
+
'<div class="voice-assistant-health-failures">',
|
|
6781
|
+
...failures.map((failure) => [
|
|
6782
|
+
"<article>",
|
|
6783
|
+
`<strong>${escapeHtml4(failure.provider ?? failure.assistantId ?? failure.type)}</strong>`,
|
|
6784
|
+
`<span>${escapeHtml4(failure.status ?? (failure.rateLimited ? "rate-limited" : "error"))}</span>`,
|
|
6785
|
+
failure.error ? `<p>${escapeHtml4(failure.error)}</p>` : "",
|
|
6786
|
+
`<small>${escapeHtml4(failure.sessionId)}${failure.turnId ? ` / ${escapeHtml4(failure.turnId)}` : ""}</small>`,
|
|
6787
|
+
failure.replayHref ? `<p><a href="${escapeHtml4(failure.replayHref)}">Open replay</a></p>` : "",
|
|
6788
|
+
"</article>"
|
|
6789
|
+
].join("")),
|
|
6790
|
+
"</div>"
|
|
6791
|
+
].join(""),
|
|
6792
|
+
"</section>",
|
|
6793
|
+
"</div>"
|
|
6794
|
+
].join("");
|
|
6795
|
+
};
|
|
6796
|
+
var createVoiceAssistantHealthJSONHandler = (options) => async () => summarizeVoiceAssistantHealth(options);
|
|
6797
|
+
var createVoiceAssistantHealthHTMLHandler = (options) => async () => {
|
|
6798
|
+
const summary = await summarizeVoiceAssistantHealth(options);
|
|
6799
|
+
const render = options.render ?? renderVoiceAssistantHealthHTML;
|
|
6800
|
+
const body = await render(summary);
|
|
6801
|
+
return new Response(body, {
|
|
6802
|
+
headers: {
|
|
6803
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6804
|
+
...options.headers
|
|
6805
|
+
}
|
|
6806
|
+
});
|
|
6807
|
+
};
|
|
6808
|
+
var createVoiceAssistantHealthRoutes = (options) => {
|
|
6809
|
+
const path = options.path ?? "/api/assistant-health";
|
|
6810
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
6811
|
+
const routes = new Elysia3({
|
|
6812
|
+
name: options.name ?? "absolutejs-voice-assistant-health"
|
|
6813
|
+
}).get(path, createVoiceAssistantHealthJSONHandler(options));
|
|
6814
|
+
if (htmlPath) {
|
|
6815
|
+
routes.get(htmlPath, createVoiceAssistantHealthHTMLHandler(options));
|
|
6816
|
+
}
|
|
6817
|
+
return routes;
|
|
6818
|
+
};
|
|
6819
|
+
// src/sessionReplay.ts
|
|
6820
|
+
import { Elysia as Elysia4 } from "elysia";
|
|
5935
6821
|
|
|
5936
6822
|
// src/trace.ts
|
|
5937
6823
|
var createVoiceTraceEventId = (event) => [
|
|
@@ -6035,7 +6921,7 @@ var sleep3 = async (delayMs) => {
|
|
|
6035
6921
|
}
|
|
6036
6922
|
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
6037
6923
|
};
|
|
6038
|
-
var
|
|
6924
|
+
var toHex4 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
6039
6925
|
var signVoiceTraceSinkBody = async (input) => {
|
|
6040
6926
|
const encoder = new TextEncoder;
|
|
6041
6927
|
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
@@ -6044,7 +6930,7 @@ var signVoiceTraceSinkBody = async (input) => {
|
|
|
6044
6930
|
}, false, ["sign"]);
|
|
6045
6931
|
const payload = encoder.encode(`${input.timestamp}.${input.body}`);
|
|
6046
6932
|
const signature = await crypto.subtle.sign("HMAC", key, payload);
|
|
6047
|
-
return `sha256=${
|
|
6933
|
+
return `sha256=${toHex4(new Uint8Array(signature))}`;
|
|
6048
6934
|
};
|
|
6049
6935
|
var createVoiceTraceSinkDeliveryError = (input) => {
|
|
6050
6936
|
if (input.response) {
|
|
@@ -6265,7 +7151,7 @@ var exportVoiceTrace = async (input) => {
|
|
|
6265
7151
|
};
|
|
6266
7152
|
};
|
|
6267
7153
|
var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
6268
|
-
var
|
|
7154
|
+
var escapeHtml5 = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6269
7155
|
var formatTraceValue = (value) => {
|
|
6270
7156
|
if (value === undefined || value === null) {
|
|
6271
7157
|
return "";
|
|
@@ -6543,10 +7429,10 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6543
7429
|
const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
|
|
6544
7430
|
return [
|
|
6545
7431
|
"<tr>",
|
|
6546
|
-
`<td>${
|
|
6547
|
-
`<td>${
|
|
6548
|
-
`<td>${
|
|
6549
|
-
`<td><code>${
|
|
7432
|
+
`<td>${escapeHtml5(String(offset))}</td>`,
|
|
7433
|
+
`<td>${escapeHtml5(event.type)}</td>`,
|
|
7434
|
+
`<td>${escapeHtml5(event.turnId ?? "")}</td>`,
|
|
7435
|
+
`<td><code>${escapeHtml5(JSON.stringify(event.payload))}</code></td>`,
|
|
6550
7436
|
"</tr>"
|
|
6551
7437
|
].join("");
|
|
6552
7438
|
}).join(`
|
|
@@ -6557,7 +7443,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6557
7443
|
"<head>",
|
|
6558
7444
|
'<meta charset="utf-8" />',
|
|
6559
7445
|
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
6560
|
-
`<title>${
|
|
7446
|
+
`<title>${escapeHtml5(options.title ?? "Voice Trace")}</title>`,
|
|
6561
7447
|
"<style>",
|
|
6562
7448
|
"body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
|
|
6563
7449
|
"main{max-width:1100px;margin:auto}",
|
|
@@ -6571,7 +7457,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6571
7457
|
"</style>",
|
|
6572
7458
|
"</head>",
|
|
6573
7459
|
"<body><main>",
|
|
6574
|
-
`<h1>${
|
|
7460
|
+
`<h1>${escapeHtml5(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
|
|
6575
7461
|
`<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
|
|
6576
7462
|
'<section class="summary">',
|
|
6577
7463
|
`<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
|
|
@@ -6585,7 +7471,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6585
7471
|
eventRows,
|
|
6586
7472
|
"</tbody></table>",
|
|
6587
7473
|
"<h2>Markdown Export</h2>",
|
|
6588
|
-
`<pre>${
|
|
7474
|
+
`<pre>${escapeHtml5(markdown)}</pre>`,
|
|
6589
7475
|
"</main></body></html>"
|
|
6590
7476
|
].join(`
|
|
6591
7477
|
`);
|
|
@@ -6597,7 +7483,250 @@ var buildVoiceTraceReplay = (events, options = {}) => ({
|
|
|
6597
7483
|
summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
|
|
6598
7484
|
});
|
|
6599
7485
|
|
|
7486
|
+
// src/sessionReplay.ts
|
|
7487
|
+
var getString3 = (value) => typeof value === "string" ? value : undefined;
|
|
7488
|
+
var escapeHtml6 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
7489
|
+
var increment2 = (record, key) => {
|
|
7490
|
+
record[key] = (record[key] ?? 0) + 1;
|
|
7491
|
+
};
|
|
7492
|
+
var buildReplayTurns = (events) => {
|
|
7493
|
+
const turns = new Map;
|
|
7494
|
+
const getTurn = (turnId) => {
|
|
7495
|
+
const existing = turns.get(turnId);
|
|
7496
|
+
if (existing) {
|
|
7497
|
+
return existing;
|
|
7498
|
+
}
|
|
7499
|
+
const turn = {
|
|
7500
|
+
assistantReplies: [],
|
|
7501
|
+
errors: [],
|
|
7502
|
+
id: turnId,
|
|
7503
|
+
modelCalls: [],
|
|
7504
|
+
tools: [],
|
|
7505
|
+
transcripts: []
|
|
7506
|
+
};
|
|
7507
|
+
turns.set(turnId, turn);
|
|
7508
|
+
return turn;
|
|
7509
|
+
};
|
|
7510
|
+
for (const event of events) {
|
|
7511
|
+
const turnId = event.turnId ?? "session";
|
|
7512
|
+
const turn = getTurn(turnId);
|
|
7513
|
+
switch (event.type) {
|
|
7514
|
+
case "turn.transcript":
|
|
7515
|
+
turn.transcripts.push({
|
|
7516
|
+
isFinal: event.payload.isFinal === true,
|
|
7517
|
+
text: getString3(event.payload.text)
|
|
7518
|
+
});
|
|
7519
|
+
break;
|
|
7520
|
+
case "turn.committed":
|
|
7521
|
+
turn.committedText = getString3(event.payload.text);
|
|
7522
|
+
break;
|
|
7523
|
+
case "turn.assistant": {
|
|
7524
|
+
const text = getString3(event.payload.text);
|
|
7525
|
+
if (text) {
|
|
7526
|
+
turn.assistantReplies.push(text);
|
|
7527
|
+
}
|
|
7528
|
+
break;
|
|
7529
|
+
}
|
|
7530
|
+
case "agent.model":
|
|
7531
|
+
case "assistant.run":
|
|
7532
|
+
turn.modelCalls.push(event.payload);
|
|
7533
|
+
break;
|
|
7534
|
+
case "agent.tool":
|
|
7535
|
+
turn.tools.push(event.payload);
|
|
7536
|
+
break;
|
|
7537
|
+
case "session.error":
|
|
7538
|
+
turn.errors.push(event.payload);
|
|
7539
|
+
break;
|
|
7540
|
+
}
|
|
7541
|
+
}
|
|
7542
|
+
return [...turns.values()];
|
|
7543
|
+
};
|
|
7544
|
+
var summarizeVoiceSessionReplay = async (options) => {
|
|
7545
|
+
const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
|
|
7546
|
+
const events = filterVoiceTraceEvents(sourceEvents, {
|
|
7547
|
+
sessionId: options.sessionId
|
|
7548
|
+
});
|
|
7549
|
+
const replay = buildVoiceTraceReplay(events, {
|
|
7550
|
+
evaluation: options.evaluation,
|
|
7551
|
+
redact: options.redact,
|
|
7552
|
+
title: options.title ?? `Voice Session ${options.sessionId}`
|
|
7553
|
+
});
|
|
7554
|
+
const startedAt = replay.summary.startedAt;
|
|
7555
|
+
return {
|
|
7556
|
+
evaluation: replay.evaluation,
|
|
7557
|
+
events,
|
|
7558
|
+
html: replay.html,
|
|
7559
|
+
markdown: replay.markdown,
|
|
7560
|
+
sessionId: options.sessionId,
|
|
7561
|
+
summary: replay.summary,
|
|
7562
|
+
timeline: events.map((event) => ({
|
|
7563
|
+
at: event.at,
|
|
7564
|
+
offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
|
|
7565
|
+
payload: event.payload,
|
|
7566
|
+
turnId: event.turnId,
|
|
7567
|
+
type: event.type
|
|
7568
|
+
})),
|
|
7569
|
+
turns: buildReplayTurns(events)
|
|
7570
|
+
};
|
|
7571
|
+
};
|
|
7572
|
+
var summarizeVoiceSessions = async (options = {}) => {
|
|
7573
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
7574
|
+
const grouped = new Map;
|
|
7575
|
+
for (const event of events) {
|
|
7576
|
+
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
7577
|
+
}
|
|
7578
|
+
const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
|
|
7579
|
+
const sorted = filterVoiceTraceEvents(sessionEvents);
|
|
7580
|
+
const summary = buildVoiceTraceReplay(sorted, {
|
|
7581
|
+
evaluation: {
|
|
7582
|
+
requireAssistantReply: false,
|
|
7583
|
+
requireCompletedCall: false,
|
|
7584
|
+
requireTranscript: false,
|
|
7585
|
+
requireTurn: false
|
|
7586
|
+
}
|
|
7587
|
+
}).summary;
|
|
7588
|
+
const providerErrors = {};
|
|
7589
|
+
const providers = new Set;
|
|
7590
|
+
let latestOutcome;
|
|
7591
|
+
let errorCount = 0;
|
|
7592
|
+
for (const event of sorted) {
|
|
7593
|
+
const provider = getString3(event.payload.provider);
|
|
7594
|
+
if (provider) {
|
|
7595
|
+
providers.add(provider);
|
|
7596
|
+
}
|
|
7597
|
+
if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
|
|
7598
|
+
errorCount += 1;
|
|
7599
|
+
increment2(providerErrors, provider ?? "unknown");
|
|
7600
|
+
}
|
|
7601
|
+
const outcome = getString3(event.payload.outcome);
|
|
7602
|
+
if (outcome) {
|
|
7603
|
+
latestOutcome = outcome;
|
|
7604
|
+
}
|
|
7605
|
+
}
|
|
7606
|
+
const item = {
|
|
7607
|
+
endedAt: summary.endedAt,
|
|
7608
|
+
errorCount,
|
|
7609
|
+
eventCount: summary.eventCount,
|
|
7610
|
+
latestOutcome,
|
|
7611
|
+
providerErrors,
|
|
7612
|
+
providers: [...providers].sort(),
|
|
7613
|
+
sessionId,
|
|
7614
|
+
startedAt: summary.startedAt,
|
|
7615
|
+
status: errorCount > 0 ? "failed" : "healthy",
|
|
7616
|
+
transcriptCount: summary.transcriptCount,
|
|
7617
|
+
turnCount: summary.turnCount
|
|
7618
|
+
};
|
|
7619
|
+
const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
|
|
7620
|
+
return {
|
|
7621
|
+
...item,
|
|
7622
|
+
replayHref
|
|
7623
|
+
};
|
|
7624
|
+
});
|
|
7625
|
+
const search = options.q?.trim().toLowerCase();
|
|
7626
|
+
return sessions.filter((session) => {
|
|
7627
|
+
if (options.status && options.status !== "all" && session.status !== options.status) {
|
|
7628
|
+
return false;
|
|
7629
|
+
}
|
|
7630
|
+
if (options.provider && !session.providers.includes(options.provider)) {
|
|
7631
|
+
return false;
|
|
7632
|
+
}
|
|
7633
|
+
if (!search) {
|
|
7634
|
+
return true;
|
|
7635
|
+
}
|
|
7636
|
+
return [
|
|
7637
|
+
session.sessionId,
|
|
7638
|
+
session.latestOutcome,
|
|
7639
|
+
session.status,
|
|
7640
|
+
...session.providers
|
|
7641
|
+
].some((value) => value?.toLowerCase().includes(search));
|
|
7642
|
+
}).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
|
|
7643
|
+
};
|
|
7644
|
+
var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
|
|
7645
|
+
'<div class="voice-sessions-list">',
|
|
7646
|
+
...sessions.map((session) => [
|
|
7647
|
+
`<article class="voice-session-card ${escapeHtml6(session.status)}">`,
|
|
7648
|
+
'<div class="voice-session-card-header">',
|
|
7649
|
+
`<strong>${escapeHtml6(session.sessionId)}</strong>`,
|
|
7650
|
+
`<span>${escapeHtml6(session.status)}</span>`,
|
|
7651
|
+
"</div>",
|
|
7652
|
+
"<dl>",
|
|
7653
|
+
`<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
|
|
7654
|
+
`<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
|
|
7655
|
+
`<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
|
|
7656
|
+
`<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
|
|
7657
|
+
"</dl>",
|
|
7658
|
+
session.latestOutcome ? `<p>Outcome: ${escapeHtml6(session.latestOutcome)}</p>` : "",
|
|
7659
|
+
session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml6).join(", ")}</p>` : "",
|
|
7660
|
+
session.replayHref ? `<p><a href="${escapeHtml6(session.replayHref)}">Open replay</a></p>` : "",
|
|
7661
|
+
"</article>"
|
|
7662
|
+
].join("")),
|
|
7663
|
+
"</div>"
|
|
7664
|
+
].join("");
|
|
7665
|
+
var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
|
|
7666
|
+
...options,
|
|
7667
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
7668
|
+
provider: query?.provider ?? options.provider,
|
|
7669
|
+
q: query?.q ?? options.q,
|
|
7670
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
7671
|
+
});
|
|
7672
|
+
var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
|
|
7673
|
+
const sessions = await summarizeVoiceSessions({
|
|
7674
|
+
...options,
|
|
7675
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
7676
|
+
provider: query?.provider ?? options.provider,
|
|
7677
|
+
q: query?.q ?? options.q,
|
|
7678
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
7679
|
+
});
|
|
7680
|
+
const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
|
|
7681
|
+
return new Response(body, {
|
|
7682
|
+
headers: {
|
|
7683
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7684
|
+
...options.headers
|
|
7685
|
+
}
|
|
7686
|
+
});
|
|
7687
|
+
};
|
|
7688
|
+
var createVoiceSessionListRoutes = (options = {}) => {
|
|
7689
|
+
const path = options.path ?? "/api/voice-sessions";
|
|
7690
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
7691
|
+
const routes = new Elysia4({
|
|
7692
|
+
name: options.name ?? "absolutejs-voice-session-list"
|
|
7693
|
+
}).get(path, createVoiceSessionsJSONHandler(options));
|
|
7694
|
+
if (htmlPath) {
|
|
7695
|
+
routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
|
|
7696
|
+
}
|
|
7697
|
+
return routes;
|
|
7698
|
+
};
|
|
7699
|
+
var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
|
|
7700
|
+
...options,
|
|
7701
|
+
sessionId: params.sessionId ?? ""
|
|
7702
|
+
});
|
|
7703
|
+
var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
|
|
7704
|
+
const replay = await summarizeVoiceSessionReplay({
|
|
7705
|
+
...options,
|
|
7706
|
+
sessionId: params.sessionId ?? ""
|
|
7707
|
+
});
|
|
7708
|
+
const body = await (options.render?.(replay) ?? replay.html);
|
|
7709
|
+
return new Response(body, {
|
|
7710
|
+
headers: {
|
|
7711
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7712
|
+
...options.headers
|
|
7713
|
+
}
|
|
7714
|
+
});
|
|
7715
|
+
};
|
|
7716
|
+
var createVoiceSessionReplayRoutes = (options) => {
|
|
7717
|
+
const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
|
|
7718
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
7719
|
+
const routes = new Elysia4({
|
|
7720
|
+
name: options.name ?? "absolutejs-voice-session-replay"
|
|
7721
|
+
}).get(path, createVoiceSessionReplayJSONHandler(options));
|
|
7722
|
+
if (htmlPath) {
|
|
7723
|
+
routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
|
|
7724
|
+
}
|
|
7725
|
+
return routes;
|
|
7726
|
+
};
|
|
6600
7727
|
// src/fileStore.ts
|
|
7728
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
7729
|
+
import { join } from "path";
|
|
6601
7730
|
var listJsonFiles = async (directory) => {
|
|
6602
7731
|
try {
|
|
6603
7732
|
const entries = await readdir(directory, {
|
|
@@ -6613,6 +7742,7 @@ var listJsonFiles = async (directory) => {
|
|
|
6613
7742
|
};
|
|
6614
7743
|
var encodeStoreId = (id) => `${encodeURIComponent(id)}.json`;
|
|
6615
7744
|
var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
|
|
7745
|
+
var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
|
|
6616
7746
|
var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
6617
7747
|
var writeJsonFile = async (path, value, options) => {
|
|
6618
7748
|
await mkdir(options.directory, {
|
|
@@ -6832,6 +7962,40 @@ var createVoiceFileTraceSinkDeliveryStore = (options) => {
|
|
|
6832
7962
|
};
|
|
6833
7963
|
return { get, list, remove, set };
|
|
6834
7964
|
};
|
|
7965
|
+
var createVoiceFileAssistantMemoryStore = (options) => {
|
|
7966
|
+
const get = async (input) => {
|
|
7967
|
+
const path = resolveFilePath(options.directory, createMemoryStoreId(input));
|
|
7968
|
+
try {
|
|
7969
|
+
return await readJsonFile(path);
|
|
7970
|
+
} catch (error) {
|
|
7971
|
+
if (error.code === "ENOENT") {
|
|
7972
|
+
return;
|
|
7973
|
+
}
|
|
7974
|
+
throw error;
|
|
7975
|
+
}
|
|
7976
|
+
};
|
|
7977
|
+
const list = async (input) => {
|
|
7978
|
+
const files = await listJsonFiles(options.directory);
|
|
7979
|
+
const records = await Promise.all(files.map((file) => readJsonFile(file)));
|
|
7980
|
+
return records.filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt);
|
|
7981
|
+
};
|
|
7982
|
+
const set = async (input) => {
|
|
7983
|
+
const existing = await get(input);
|
|
7984
|
+
const record = createVoiceAssistantMemoryRecord({
|
|
7985
|
+
...input,
|
|
7986
|
+
createdAt: input.createdAt ?? existing?.createdAt,
|
|
7987
|
+
updatedAt: input.updatedAt
|
|
7988
|
+
});
|
|
7989
|
+
await writeJsonFile(resolveFilePath(options.directory, createMemoryStoreId(record)), record, options);
|
|
7990
|
+
return record;
|
|
7991
|
+
};
|
|
7992
|
+
const remove = async (input) => {
|
|
7993
|
+
await rm(resolveFilePath(options.directory, createMemoryStoreId(input)), {
|
|
7994
|
+
force: true
|
|
7995
|
+
});
|
|
7996
|
+
};
|
|
7997
|
+
return { delete: remove, get, list, set };
|
|
7998
|
+
};
|
|
6835
7999
|
var createVoiceFileRuntimeStorage = (options) => ({
|
|
6836
8000
|
events: createVoiceFileIntegrationEventStore({
|
|
6837
8001
|
...options,
|
|
@@ -6841,6 +8005,10 @@ var createVoiceFileRuntimeStorage = (options) => ({
|
|
|
6841
8005
|
...options,
|
|
6842
8006
|
directory: join(options.directory, "external-objects")
|
|
6843
8007
|
}),
|
|
8008
|
+
memories: createVoiceFileAssistantMemoryStore({
|
|
8009
|
+
...options,
|
|
8010
|
+
directory: join(options.directory, "memories")
|
|
8011
|
+
}),
|
|
6844
8012
|
reviews: createVoiceFileReviewStore({
|
|
6845
8013
|
...options,
|
|
6846
8014
|
directory: join(options.directory, "reviews")
|
|
@@ -6873,6 +8041,777 @@ var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectM
|
|
|
6873
8041
|
sourceId: mapping.sourceId,
|
|
6874
8042
|
sourceType: mapping.sourceType
|
|
6875
8043
|
});
|
|
8044
|
+
// src/modelAdapters.ts
|
|
8045
|
+
var OUTPUT_SCHEMA = {
|
|
8046
|
+
additionalProperties: false,
|
|
8047
|
+
properties: {
|
|
8048
|
+
assistantText: {
|
|
8049
|
+
type: "string"
|
|
8050
|
+
},
|
|
8051
|
+
complete: {
|
|
8052
|
+
type: "boolean"
|
|
8053
|
+
},
|
|
8054
|
+
escalate: {
|
|
8055
|
+
additionalProperties: false,
|
|
8056
|
+
properties: {
|
|
8057
|
+
metadata: {
|
|
8058
|
+
additionalProperties: true,
|
|
8059
|
+
type: "object"
|
|
8060
|
+
},
|
|
8061
|
+
reason: {
|
|
8062
|
+
type: "string"
|
|
8063
|
+
}
|
|
8064
|
+
},
|
|
8065
|
+
required: ["reason"],
|
|
8066
|
+
type: "object"
|
|
8067
|
+
},
|
|
8068
|
+
noAnswer: {
|
|
8069
|
+
additionalProperties: false,
|
|
8070
|
+
properties: {
|
|
8071
|
+
metadata: {
|
|
8072
|
+
additionalProperties: true,
|
|
8073
|
+
type: "object"
|
|
8074
|
+
}
|
|
8075
|
+
},
|
|
8076
|
+
type: "object"
|
|
8077
|
+
},
|
|
8078
|
+
result: {
|
|
8079
|
+
additionalProperties: true,
|
|
8080
|
+
type: "object"
|
|
8081
|
+
},
|
|
8082
|
+
transfer: {
|
|
8083
|
+
additionalProperties: false,
|
|
8084
|
+
properties: {
|
|
8085
|
+
metadata: {
|
|
8086
|
+
additionalProperties: true,
|
|
8087
|
+
type: "object"
|
|
8088
|
+
},
|
|
8089
|
+
reason: {
|
|
8090
|
+
type: "string"
|
|
8091
|
+
},
|
|
8092
|
+
target: {
|
|
8093
|
+
type: "string"
|
|
8094
|
+
}
|
|
8095
|
+
},
|
|
8096
|
+
required: ["target"],
|
|
8097
|
+
type: "object"
|
|
8098
|
+
},
|
|
8099
|
+
voicemail: {
|
|
8100
|
+
additionalProperties: false,
|
|
8101
|
+
properties: {
|
|
8102
|
+
metadata: {
|
|
8103
|
+
additionalProperties: true,
|
|
8104
|
+
type: "object"
|
|
8105
|
+
}
|
|
8106
|
+
},
|
|
8107
|
+
type: "object"
|
|
8108
|
+
}
|
|
8109
|
+
},
|
|
8110
|
+
type: "object"
|
|
8111
|
+
};
|
|
8112
|
+
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.";
|
|
8113
|
+
var stripJSONCodeFence = (value) => {
|
|
8114
|
+
const trimmed = value.trim();
|
|
8115
|
+
const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
8116
|
+
return match?.[1]?.trim() ?? value;
|
|
8117
|
+
};
|
|
8118
|
+
var parseJSON = (value) => {
|
|
8119
|
+
try {
|
|
8120
|
+
const parsed = JSON.parse(stripJSONCodeFence(value));
|
|
8121
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
8122
|
+
} catch {
|
|
8123
|
+
return {
|
|
8124
|
+
assistantText: value
|
|
8125
|
+
};
|
|
8126
|
+
}
|
|
8127
|
+
};
|
|
8128
|
+
var parseJSONValue = (value) => {
|
|
8129
|
+
try {
|
|
8130
|
+
return JSON.parse(value);
|
|
8131
|
+
} catch {
|
|
8132
|
+
return value;
|
|
8133
|
+
}
|
|
8134
|
+
};
|
|
8135
|
+
var getMessageToolCalls = (message) => {
|
|
8136
|
+
const toolCalls = message.metadata?.toolCalls;
|
|
8137
|
+
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
8138
|
+
};
|
|
8139
|
+
var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
|
|
8140
|
+
var sleep4 = (ms) => new Promise((resolve2) => {
|
|
8141
|
+
setTimeout(resolve2, ms);
|
|
8142
|
+
});
|
|
8143
|
+
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
8144
|
+
var defaultIsRateLimitError = (error) => /(\b429\b|rate limit|quota|too many requests)/i.test(errorMessage(error));
|
|
8145
|
+
var normalizeRouteOutput = (output) => {
|
|
8146
|
+
const result = {};
|
|
8147
|
+
if (typeof output.assistantText === "string") {
|
|
8148
|
+
result.assistantText = output.assistantText;
|
|
8149
|
+
}
|
|
8150
|
+
if (typeof output.complete === "boolean") {
|
|
8151
|
+
result.complete = output.complete;
|
|
8152
|
+
}
|
|
8153
|
+
if (output.result !== undefined) {
|
|
8154
|
+
result.result = output.result;
|
|
8155
|
+
}
|
|
8156
|
+
if (output.transfer && typeof output.transfer === "object") {
|
|
8157
|
+
const transfer = output.transfer;
|
|
8158
|
+
if (typeof transfer.target === "string") {
|
|
8159
|
+
result.transfer = {
|
|
8160
|
+
metadata: transfer.metadata && typeof transfer.metadata === "object" ? transfer.metadata : undefined,
|
|
8161
|
+
reason: typeof transfer.reason === "string" ? transfer.reason : undefined,
|
|
8162
|
+
target: transfer.target
|
|
8163
|
+
};
|
|
8164
|
+
}
|
|
8165
|
+
}
|
|
8166
|
+
if (output.escalate && typeof output.escalate === "object") {
|
|
8167
|
+
const escalate = output.escalate;
|
|
8168
|
+
if (typeof escalate.reason === "string") {
|
|
8169
|
+
result.escalate = {
|
|
8170
|
+
metadata: escalate.metadata && typeof escalate.metadata === "object" ? escalate.metadata : undefined,
|
|
8171
|
+
reason: escalate.reason
|
|
8172
|
+
};
|
|
8173
|
+
}
|
|
8174
|
+
}
|
|
8175
|
+
if (output.voicemail && typeof output.voicemail === "object") {
|
|
8176
|
+
const voicemail = output.voicemail;
|
|
8177
|
+
result.voicemail = {
|
|
8178
|
+
metadata: voicemail.metadata && typeof voicemail.metadata === "object" ? voicemail.metadata : undefined
|
|
8179
|
+
};
|
|
8180
|
+
}
|
|
8181
|
+
if (output.noAnswer && typeof output.noAnswer === "object") {
|
|
8182
|
+
const noAnswer = output.noAnswer;
|
|
8183
|
+
result.noAnswer = {
|
|
8184
|
+
metadata: noAnswer.metadata && typeof noAnswer.metadata === "object" ? noAnswer.metadata : undefined
|
|
8185
|
+
};
|
|
8186
|
+
}
|
|
8187
|
+
return result;
|
|
8188
|
+
};
|
|
8189
|
+
var createJSONVoiceAssistantModel = (options) => ({
|
|
8190
|
+
generate: async (input) => {
|
|
8191
|
+
const output = await options.generate(input);
|
|
8192
|
+
if ("assistantText" in output || "toolCalls" in output || "complete" in output || "transfer" in output || "escalate" in output) {
|
|
8193
|
+
return output;
|
|
8194
|
+
}
|
|
8195
|
+
return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
|
|
8196
|
+
}
|
|
8197
|
+
});
|
|
8198
|
+
var createVoiceProviderRouter = (options) => {
|
|
8199
|
+
const providerIds = Object.keys(options.providers);
|
|
8200
|
+
const firstProvider = providerIds[0];
|
|
8201
|
+
const policy = typeof options.policy === "string" ? {
|
|
8202
|
+
strategy: options.policy
|
|
8203
|
+
} : options.policy;
|
|
8204
|
+
const strategy = policy?.strategy ?? "prefer-selected";
|
|
8205
|
+
const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
|
|
8206
|
+
const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
|
|
8207
|
+
const healthState = new Map;
|
|
8208
|
+
const now = () => healthOptions?.now?.() ?? Date.now();
|
|
8209
|
+
const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
|
|
8210
|
+
const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
|
|
8211
|
+
const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
|
|
8212
|
+
const getHealth = (provider) => {
|
|
8213
|
+
const existing = healthState.get(provider);
|
|
8214
|
+
if (existing) {
|
|
8215
|
+
return existing;
|
|
8216
|
+
}
|
|
8217
|
+
const next = {
|
|
8218
|
+
consecutiveFailures: 0,
|
|
8219
|
+
provider,
|
|
8220
|
+
status: "healthy"
|
|
8221
|
+
};
|
|
8222
|
+
healthState.set(provider, next);
|
|
8223
|
+
return next;
|
|
8224
|
+
};
|
|
8225
|
+
const cloneHealth = (provider) => {
|
|
8226
|
+
if (!healthOptions) {
|
|
8227
|
+
return;
|
|
8228
|
+
}
|
|
8229
|
+
return {
|
|
8230
|
+
...getHealth(provider)
|
|
8231
|
+
};
|
|
8232
|
+
};
|
|
8233
|
+
const getSuppressionRemainingMs = (provider) => {
|
|
8234
|
+
if (!healthOptions) {
|
|
8235
|
+
return;
|
|
8236
|
+
}
|
|
8237
|
+
const suppressedUntil = getHealth(provider).suppressedUntil;
|
|
8238
|
+
return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
|
|
8239
|
+
};
|
|
8240
|
+
const isSuppressed = (provider) => {
|
|
8241
|
+
if (!healthOptions) {
|
|
8242
|
+
return false;
|
|
8243
|
+
}
|
|
8244
|
+
const health = getHealth(provider);
|
|
8245
|
+
return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
|
|
8246
|
+
};
|
|
8247
|
+
const recordProviderSuccess = (provider) => {
|
|
8248
|
+
if (!healthOptions) {
|
|
8249
|
+
return;
|
|
8250
|
+
}
|
|
8251
|
+
const health = getHealth(provider);
|
|
8252
|
+
health.consecutiveFailures = 0;
|
|
8253
|
+
health.status = "healthy";
|
|
8254
|
+
health.suppressedUntil = undefined;
|
|
8255
|
+
return cloneHealth(provider);
|
|
8256
|
+
};
|
|
8257
|
+
const recordProviderError = (provider, isProviderError, rateLimited) => {
|
|
8258
|
+
if (!healthOptions || !isProviderError) {
|
|
8259
|
+
return cloneHealth(provider);
|
|
8260
|
+
}
|
|
8261
|
+
const currentTime = now();
|
|
8262
|
+
const health = getHealth(provider);
|
|
8263
|
+
health.consecutiveFailures += 1;
|
|
8264
|
+
health.lastFailureAt = currentTime;
|
|
8265
|
+
if (rateLimited) {
|
|
8266
|
+
health.lastRateLimitedAt = currentTime;
|
|
8267
|
+
}
|
|
8268
|
+
if (rateLimited || health.consecutiveFailures >= failureThreshold) {
|
|
8269
|
+
health.status = "suppressed";
|
|
8270
|
+
health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
|
|
8271
|
+
}
|
|
8272
|
+
return cloneHealth(provider);
|
|
8273
|
+
};
|
|
8274
|
+
const resolveAllowedProviders = async (input) => {
|
|
8275
|
+
const allowProviders = policy?.allowProviders ?? options.allowProviders;
|
|
8276
|
+
const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
|
|
8277
|
+
return new Set(allowed ?? providerIds);
|
|
8278
|
+
};
|
|
8279
|
+
const sortProviders = (providers) => {
|
|
8280
|
+
if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
|
|
8281
|
+
return providers;
|
|
8282
|
+
}
|
|
8283
|
+
return [...providers].sort((left, right) => {
|
|
8284
|
+
const leftProfile = options.providerProfiles?.[left];
|
|
8285
|
+
const rightProfile = options.providerProfiles?.[right];
|
|
8286
|
+
const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
8287
|
+
const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
8288
|
+
return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
|
|
8289
|
+
});
|
|
8290
|
+
};
|
|
8291
|
+
const resolveOrder = async (input) => {
|
|
8292
|
+
const selectedProvider = await options.selectProvider?.(input);
|
|
8293
|
+
const allowedProviders = await resolveAllowedProviders(input);
|
|
8294
|
+
const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
|
|
8295
|
+
const rankedProviders = sortProviders([
|
|
8296
|
+
...fallbackOrder ?? providerIds
|
|
8297
|
+
]).filter((provider) => allowedProviders.has(provider));
|
|
8298
|
+
const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
|
|
8299
|
+
const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
|
|
8300
|
+
const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
|
|
8301
|
+
const seen = new Set;
|
|
8302
|
+
const order = [];
|
|
8303
|
+
const candidates = strategy === "ordered" ? candidateRankedProviders : [
|
|
8304
|
+
preferred,
|
|
8305
|
+
...candidateRankedProviders,
|
|
8306
|
+
...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
|
|
8307
|
+
];
|
|
8308
|
+
for (const provider of candidates) {
|
|
8309
|
+
if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
|
|
8310
|
+
continue;
|
|
8311
|
+
}
|
|
8312
|
+
seen.add(provider);
|
|
8313
|
+
order.push(provider);
|
|
8314
|
+
}
|
|
8315
|
+
return {
|
|
8316
|
+
order,
|
|
8317
|
+
selectedProvider: preferred
|
|
8318
|
+
};
|
|
8319
|
+
};
|
|
8320
|
+
const emit = async (event, input) => {
|
|
8321
|
+
await options.onProviderEvent?.(event, input);
|
|
8322
|
+
};
|
|
8323
|
+
return {
|
|
8324
|
+
generate: async (input) => {
|
|
8325
|
+
const { order, selectedProvider } = await resolveOrder(input);
|
|
8326
|
+
if (!selectedProvider || order.length === 0) {
|
|
8327
|
+
throw new Error("Voice provider router has no available providers.");
|
|
8328
|
+
}
|
|
8329
|
+
let lastError;
|
|
8330
|
+
for (const [index, provider] of order.entries()) {
|
|
8331
|
+
const model = options.providers[provider];
|
|
8332
|
+
if (!model) {
|
|
8333
|
+
continue;
|
|
8334
|
+
}
|
|
8335
|
+
const startedAt = Date.now();
|
|
8336
|
+
try {
|
|
8337
|
+
const output = await model.generate(input);
|
|
8338
|
+
const providerHealth = recordProviderSuccess(provider);
|
|
8339
|
+
await emit({
|
|
8340
|
+
at: Date.now(),
|
|
8341
|
+
elapsedMs: Date.now() - startedAt,
|
|
8342
|
+
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
8343
|
+
provider,
|
|
8344
|
+
providerHealth,
|
|
8345
|
+
recovered: provider !== selectedProvider,
|
|
8346
|
+
selectedProvider,
|
|
8347
|
+
status: provider === selectedProvider ? "success" : "fallback"
|
|
8348
|
+
}, input);
|
|
8349
|
+
return output;
|
|
8350
|
+
} catch (error) {
|
|
8351
|
+
lastError = error;
|
|
8352
|
+
const hasNextProvider = index < order.length - 1;
|
|
8353
|
+
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
8354
|
+
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
8355
|
+
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
8356
|
+
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
8357
|
+
const nextProvider = hasNextProvider ? order[index + 1] : undefined;
|
|
8358
|
+
await emit({
|
|
8359
|
+
at: Date.now(),
|
|
8360
|
+
elapsedMs: Date.now() - startedAt,
|
|
8361
|
+
error: errorMessage(error),
|
|
8362
|
+
fallbackProvider: shouldFallback ? nextProvider : undefined,
|
|
8363
|
+
provider,
|
|
8364
|
+
providerHealth,
|
|
8365
|
+
rateLimited,
|
|
8366
|
+
selectedProvider,
|
|
8367
|
+
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
8368
|
+
suppressedUntil: providerHealth?.suppressedUntil,
|
|
8369
|
+
status: "error"
|
|
8370
|
+
}, input);
|
|
8371
|
+
if (!hasNextProvider || !shouldFallback) {
|
|
8372
|
+
throw error;
|
|
8373
|
+
}
|
|
8374
|
+
}
|
|
8375
|
+
}
|
|
8376
|
+
throw lastError ?? new Error("Voice provider router did not run a provider.");
|
|
8377
|
+
}
|
|
8378
|
+
};
|
|
8379
|
+
};
|
|
8380
|
+
var messageToOpenAIInput = (message) => {
|
|
8381
|
+
if (message.role === "tool") {
|
|
8382
|
+
return [
|
|
8383
|
+
{
|
|
8384
|
+
call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
|
|
8385
|
+
output: message.content,
|
|
8386
|
+
type: "function_call_output"
|
|
8387
|
+
}
|
|
8388
|
+
];
|
|
8389
|
+
}
|
|
8390
|
+
const toolCalls = getMessageToolCalls(message);
|
|
8391
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
8392
|
+
return toolCalls.map((toolCall) => ({
|
|
8393
|
+
arguments: JSON.stringify(toolCall.args),
|
|
8394
|
+
call_id: toolCall.id ?? crypto.randomUUID(),
|
|
8395
|
+
name: toolCall.name,
|
|
8396
|
+
type: "function_call"
|
|
8397
|
+
}));
|
|
8398
|
+
}
|
|
8399
|
+
return [
|
|
8400
|
+
{
|
|
8401
|
+
content: message.content,
|
|
8402
|
+
role: message.role === "system" ? "developer" : message.role
|
|
8403
|
+
}
|
|
8404
|
+
];
|
|
8405
|
+
};
|
|
8406
|
+
var messagesToOpenAIInput = (messages) => messages.flatMap(messageToOpenAIInput);
|
|
8407
|
+
var messageToAnthropicMessage = (message) => {
|
|
8408
|
+
if (message.role === "system") {
|
|
8409
|
+
return;
|
|
8410
|
+
}
|
|
8411
|
+
if (message.role === "tool") {
|
|
8412
|
+
if (!message.toolCallId) {
|
|
8413
|
+
return {
|
|
8414
|
+
content: `Tool result from ${message.name ?? "tool"}: ${message.content}`,
|
|
8415
|
+
role: "user"
|
|
8416
|
+
};
|
|
8417
|
+
}
|
|
8418
|
+
return {
|
|
8419
|
+
content: [
|
|
8420
|
+
{
|
|
8421
|
+
content: message.content,
|
|
8422
|
+
tool_use_id: message.toolCallId,
|
|
8423
|
+
type: "tool_result"
|
|
8424
|
+
}
|
|
8425
|
+
],
|
|
8426
|
+
role: "user"
|
|
8427
|
+
};
|
|
8428
|
+
}
|
|
8429
|
+
const toolCalls = getMessageToolCalls(message);
|
|
8430
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
8431
|
+
return {
|
|
8432
|
+
content: [
|
|
8433
|
+
...message.content ? [
|
|
8434
|
+
{
|
|
8435
|
+
text: message.content,
|
|
8436
|
+
type: "text"
|
|
8437
|
+
}
|
|
8438
|
+
] : [],
|
|
8439
|
+
...toolCalls.map((toolCall) => ({
|
|
8440
|
+
id: toolCall.id ?? crypto.randomUUID(),
|
|
8441
|
+
input: toolCall.args,
|
|
8442
|
+
name: toolCall.name,
|
|
8443
|
+
type: "tool_use"
|
|
8444
|
+
}))
|
|
8445
|
+
],
|
|
8446
|
+
role: "assistant"
|
|
8447
|
+
};
|
|
8448
|
+
}
|
|
8449
|
+
return {
|
|
8450
|
+
content: message.content,
|
|
8451
|
+
role: message.role
|
|
8452
|
+
};
|
|
8453
|
+
};
|
|
8454
|
+
var toGeminiSchema = (schema) => {
|
|
8455
|
+
const next = {};
|
|
8456
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
8457
|
+
if (key === "additionalProperties") {
|
|
8458
|
+
continue;
|
|
8459
|
+
}
|
|
8460
|
+
if (key === "type" && typeof value === "string") {
|
|
8461
|
+
next[key] = value.toUpperCase();
|
|
8462
|
+
continue;
|
|
8463
|
+
}
|
|
8464
|
+
if (Array.isArray(value)) {
|
|
8465
|
+
next[key] = value.map((item) => item && typeof item === "object" ? toGeminiSchema(item) : item);
|
|
8466
|
+
continue;
|
|
8467
|
+
}
|
|
8468
|
+
if (value && typeof value === "object") {
|
|
8469
|
+
next[key] = toGeminiSchema(value);
|
|
8470
|
+
continue;
|
|
8471
|
+
}
|
|
8472
|
+
next[key] = value;
|
|
8473
|
+
}
|
|
8474
|
+
return next;
|
|
8475
|
+
};
|
|
8476
|
+
var messageToGeminiContent = (message) => {
|
|
8477
|
+
if (message.role === "system") {
|
|
8478
|
+
return;
|
|
8479
|
+
}
|
|
8480
|
+
if (message.role === "tool") {
|
|
8481
|
+
return {
|
|
8482
|
+
parts: [
|
|
8483
|
+
{
|
|
8484
|
+
functionResponse: {
|
|
8485
|
+
id: message.toolCallId,
|
|
8486
|
+
name: message.name ?? "tool",
|
|
8487
|
+
response: {
|
|
8488
|
+
result: parseJSONValue(message.content)
|
|
8489
|
+
}
|
|
8490
|
+
}
|
|
8491
|
+
}
|
|
8492
|
+
],
|
|
8493
|
+
role: "user"
|
|
8494
|
+
};
|
|
8495
|
+
}
|
|
8496
|
+
const toolCalls = getMessageToolCalls(message);
|
|
8497
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
8498
|
+
return {
|
|
8499
|
+
parts: [
|
|
8500
|
+
...message.content ? [
|
|
8501
|
+
{
|
|
8502
|
+
text: message.content
|
|
8503
|
+
}
|
|
8504
|
+
] : [],
|
|
8505
|
+
...toolCalls.map((toolCall) => ({
|
|
8506
|
+
functionCall: {
|
|
8507
|
+
args: toolCall.args,
|
|
8508
|
+
id: toolCall.id,
|
|
8509
|
+
name: toolCall.name
|
|
8510
|
+
}
|
|
8511
|
+
}))
|
|
8512
|
+
],
|
|
8513
|
+
role: "model"
|
|
8514
|
+
};
|
|
8515
|
+
}
|
|
8516
|
+
return {
|
|
8517
|
+
parts: [
|
|
8518
|
+
{
|
|
8519
|
+
text: message.content
|
|
8520
|
+
}
|
|
8521
|
+
],
|
|
8522
|
+
role: message.role === "assistant" ? "model" : "user"
|
|
8523
|
+
};
|
|
8524
|
+
};
|
|
8525
|
+
var extractText = (response) => {
|
|
8526
|
+
if (typeof response.output_text === "string") {
|
|
8527
|
+
return response.output_text;
|
|
8528
|
+
}
|
|
8529
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
8530
|
+
for (const item of output) {
|
|
8531
|
+
if (!item || typeof item !== "object") {
|
|
8532
|
+
continue;
|
|
8533
|
+
}
|
|
8534
|
+
const record = item;
|
|
8535
|
+
const content = Array.isArray(record.content) ? record.content : [];
|
|
8536
|
+
for (const contentItem of content) {
|
|
8537
|
+
if (!contentItem || typeof contentItem !== "object") {
|
|
8538
|
+
continue;
|
|
8539
|
+
}
|
|
8540
|
+
const contentRecord = contentItem;
|
|
8541
|
+
if (typeof contentRecord.text === "string") {
|
|
8542
|
+
return contentRecord.text;
|
|
8543
|
+
}
|
|
8544
|
+
}
|
|
8545
|
+
}
|
|
8546
|
+
return "";
|
|
8547
|
+
};
|
|
8548
|
+
var extractToolCalls = (response) => {
|
|
8549
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
8550
|
+
const toolCalls = [];
|
|
8551
|
+
for (const item of output) {
|
|
8552
|
+
if (!item || typeof item !== "object") {
|
|
8553
|
+
continue;
|
|
8554
|
+
}
|
|
8555
|
+
const record = item;
|
|
8556
|
+
if (record.type !== "function_call" || typeof record.name !== "string") {
|
|
8557
|
+
continue;
|
|
8558
|
+
}
|
|
8559
|
+
const args = typeof record.arguments === "string" ? parseJSON(record.arguments) : {};
|
|
8560
|
+
toolCalls.push({
|
|
8561
|
+
args,
|
|
8562
|
+
id: typeof record.call_id === "string" ? record.call_id : typeof record.id === "string" ? record.id : undefined,
|
|
8563
|
+
name: record.name
|
|
8564
|
+
});
|
|
8565
|
+
}
|
|
8566
|
+
return toolCalls;
|
|
8567
|
+
};
|
|
8568
|
+
var createOpenAIVoiceAssistantModel = (options) => {
|
|
8569
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8570
|
+
const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
|
|
8571
|
+
const model = options.model ?? "gpt-4.1-mini";
|
|
8572
|
+
return {
|
|
8573
|
+
generate: async (input) => {
|
|
8574
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
|
|
8575
|
+
body: JSON.stringify({
|
|
8576
|
+
input: messagesToOpenAIInput(input.messages),
|
|
8577
|
+
instructions: [
|
|
8578
|
+
input.system,
|
|
8579
|
+
"Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools."
|
|
8580
|
+
].filter(Boolean).join(`
|
|
8581
|
+
|
|
8582
|
+
`),
|
|
8583
|
+
max_output_tokens: options.maxOutputTokens,
|
|
8584
|
+
model,
|
|
8585
|
+
temperature: options.temperature,
|
|
8586
|
+
text: {
|
|
8587
|
+
format: {
|
|
8588
|
+
name: "voice_route_result",
|
|
8589
|
+
schema: OUTPUT_SCHEMA,
|
|
8590
|
+
strict: false,
|
|
8591
|
+
type: "json_schema"
|
|
8592
|
+
}
|
|
8593
|
+
},
|
|
8594
|
+
tool_choice: input.tools.length ? "auto" : "none",
|
|
8595
|
+
tools: input.tools.map((tool) => ({
|
|
8596
|
+
description: tool.description,
|
|
8597
|
+
name: tool.name,
|
|
8598
|
+
parameters: tool.parameters ?? {
|
|
8599
|
+
additionalProperties: true,
|
|
8600
|
+
type: "object"
|
|
8601
|
+
},
|
|
8602
|
+
strict: false,
|
|
8603
|
+
type: "function"
|
|
8604
|
+
}))
|
|
8605
|
+
}),
|
|
8606
|
+
headers: {
|
|
8607
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
8608
|
+
"content-type": "application/json"
|
|
8609
|
+
},
|
|
8610
|
+
method: "POST"
|
|
8611
|
+
});
|
|
8612
|
+
if (!response.ok) {
|
|
8613
|
+
throw createHTTPError("OpenAI", response);
|
|
8614
|
+
}
|
|
8615
|
+
const body = await response.json();
|
|
8616
|
+
if (body.usage && typeof body.usage === "object") {
|
|
8617
|
+
await options.onUsage?.(body.usage);
|
|
8618
|
+
}
|
|
8619
|
+
const toolCalls = extractToolCalls(body);
|
|
8620
|
+
if (toolCalls.length) {
|
|
8621
|
+
return {
|
|
8622
|
+
toolCalls
|
|
8623
|
+
};
|
|
8624
|
+
}
|
|
8625
|
+
return normalizeRouteOutput(parseJSON(extractText(body)));
|
|
8626
|
+
}
|
|
8627
|
+
};
|
|
8628
|
+
};
|
|
8629
|
+
var extractAnthropicText = (response) => {
|
|
8630
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
8631
|
+
return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
|
|
8632
|
+
`);
|
|
8633
|
+
};
|
|
8634
|
+
var extractAnthropicToolCalls = (response) => {
|
|
8635
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
8636
|
+
const toolCalls = [];
|
|
8637
|
+
for (const item of content) {
|
|
8638
|
+
if (!item || typeof item !== "object") {
|
|
8639
|
+
continue;
|
|
8640
|
+
}
|
|
8641
|
+
const record = item;
|
|
8642
|
+
if (record.type !== "tool_use" || typeof record.name !== "string") {
|
|
8643
|
+
continue;
|
|
8644
|
+
}
|
|
8645
|
+
toolCalls.push({
|
|
8646
|
+
args: record.input && typeof record.input === "object" ? record.input : {},
|
|
8647
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
8648
|
+
name: record.name
|
|
8649
|
+
});
|
|
8650
|
+
}
|
|
8651
|
+
return toolCalls;
|
|
8652
|
+
};
|
|
8653
|
+
var createAnthropicVoiceAssistantModel = (options) => {
|
|
8654
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8655
|
+
const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
|
|
8656
|
+
const model = options.model ?? "claude-sonnet-4-5";
|
|
8657
|
+
return {
|
|
8658
|
+
generate: async (input) => {
|
|
8659
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
|
|
8660
|
+
body: JSON.stringify({
|
|
8661
|
+
max_tokens: options.maxOutputTokens ?? 1024,
|
|
8662
|
+
messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
|
|
8663
|
+
model,
|
|
8664
|
+
system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
8665
|
+
|
|
8666
|
+
`),
|
|
8667
|
+
temperature: options.temperature,
|
|
8668
|
+
tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
|
|
8669
|
+
tools: input.tools.map((tool) => ({
|
|
8670
|
+
description: tool.description,
|
|
8671
|
+
input_schema: tool.parameters ?? {
|
|
8672
|
+
additionalProperties: true,
|
|
8673
|
+
type: "object"
|
|
8674
|
+
},
|
|
8675
|
+
name: tool.name
|
|
8676
|
+
}))
|
|
8677
|
+
}),
|
|
8678
|
+
headers: {
|
|
8679
|
+
"anthropic-version": options.version ?? "2023-06-01",
|
|
8680
|
+
"content-type": "application/json",
|
|
8681
|
+
"x-api-key": options.apiKey
|
|
8682
|
+
},
|
|
8683
|
+
method: "POST"
|
|
8684
|
+
});
|
|
8685
|
+
if (!response.ok) {
|
|
8686
|
+
throw createHTTPError("Anthropic", response);
|
|
8687
|
+
}
|
|
8688
|
+
const body = await response.json();
|
|
8689
|
+
if (body.usage && typeof body.usage === "object") {
|
|
8690
|
+
await options.onUsage?.(body.usage);
|
|
8691
|
+
}
|
|
8692
|
+
const toolCalls = extractAnthropicToolCalls(body);
|
|
8693
|
+
if (toolCalls.length) {
|
|
8694
|
+
return {
|
|
8695
|
+
assistantText: extractAnthropicText(body) || undefined,
|
|
8696
|
+
toolCalls
|
|
8697
|
+
};
|
|
8698
|
+
}
|
|
8699
|
+
return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
|
|
8700
|
+
}
|
|
8701
|
+
};
|
|
8702
|
+
};
|
|
8703
|
+
var extractGeminiCandidateParts = (response) => {
|
|
8704
|
+
const candidates = Array.isArray(response.candidates) ? response.candidates : [];
|
|
8705
|
+
const first = candidates[0];
|
|
8706
|
+
if (!first || typeof first !== "object") {
|
|
8707
|
+
return [];
|
|
8708
|
+
}
|
|
8709
|
+
const content = first.content;
|
|
8710
|
+
if (!content || typeof content !== "object") {
|
|
8711
|
+
return [];
|
|
8712
|
+
}
|
|
8713
|
+
const parts = content.parts;
|
|
8714
|
+
return Array.isArray(parts) ? parts : [];
|
|
8715
|
+
};
|
|
8716
|
+
var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
8717
|
+
`);
|
|
8718
|
+
var extractGeminiToolCalls = (response) => {
|
|
8719
|
+
const toolCalls = [];
|
|
8720
|
+
for (const part of extractGeminiCandidateParts(response)) {
|
|
8721
|
+
if (!part || typeof part !== "object") {
|
|
8722
|
+
continue;
|
|
8723
|
+
}
|
|
8724
|
+
const functionCall = part.functionCall;
|
|
8725
|
+
if (!functionCall || typeof functionCall !== "object") {
|
|
8726
|
+
continue;
|
|
8727
|
+
}
|
|
8728
|
+
const record = functionCall;
|
|
8729
|
+
if (typeof record.name !== "string") {
|
|
8730
|
+
continue;
|
|
8731
|
+
}
|
|
8732
|
+
toolCalls.push({
|
|
8733
|
+
args: record.args && typeof record.args === "object" ? record.args : {},
|
|
8734
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
8735
|
+
name: record.name
|
|
8736
|
+
});
|
|
8737
|
+
}
|
|
8738
|
+
return toolCalls;
|
|
8739
|
+
};
|
|
8740
|
+
var createGeminiVoiceAssistantModel = (options) => {
|
|
8741
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8742
|
+
const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
8743
|
+
const model = options.model ?? "gemini-2.5-flash";
|
|
8744
|
+
const maxRetries = Math.max(0, options.maxRetries ?? 2);
|
|
8745
|
+
return {
|
|
8746
|
+
generate: async (input) => {
|
|
8747
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
|
|
8748
|
+
let response;
|
|
8749
|
+
for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
|
|
8750
|
+
response = await fetchImpl(endpoint, {
|
|
8751
|
+
body: JSON.stringify({
|
|
8752
|
+
contents: input.messages.map(messageToGeminiContent).filter(Boolean),
|
|
8753
|
+
generationConfig: {
|
|
8754
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
8755
|
+
...input.tools.length ? {} : {
|
|
8756
|
+
responseMimeType: "application/json",
|
|
8757
|
+
responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
|
|
8758
|
+
},
|
|
8759
|
+
temperature: options.temperature
|
|
8760
|
+
},
|
|
8761
|
+
systemInstruction: {
|
|
8762
|
+
parts: [
|
|
8763
|
+
{
|
|
8764
|
+
text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
8765
|
+
|
|
8766
|
+
`)
|
|
8767
|
+
}
|
|
8768
|
+
]
|
|
8769
|
+
},
|
|
8770
|
+
tools: input.tools.length ? [
|
|
8771
|
+
{
|
|
8772
|
+
functionDeclarations: input.tools.map((tool) => ({
|
|
8773
|
+
description: tool.description,
|
|
8774
|
+
name: tool.name,
|
|
8775
|
+
parameters: toGeminiSchema(tool.parameters ?? {
|
|
8776
|
+
additionalProperties: true,
|
|
8777
|
+
type: "object"
|
|
8778
|
+
})
|
|
8779
|
+
}))
|
|
8780
|
+
}
|
|
8781
|
+
] : undefined
|
|
8782
|
+
}),
|
|
8783
|
+
headers: {
|
|
8784
|
+
"content-type": "application/json"
|
|
8785
|
+
},
|
|
8786
|
+
method: "POST"
|
|
8787
|
+
});
|
|
8788
|
+
if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
|
|
8789
|
+
break;
|
|
8790
|
+
}
|
|
8791
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
8792
|
+
await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
|
|
8793
|
+
}
|
|
8794
|
+
if (!response) {
|
|
8795
|
+
throw new Error("Gemini voice assistant model failed: no response");
|
|
8796
|
+
}
|
|
8797
|
+
if (!response.ok) {
|
|
8798
|
+
throw createHTTPError("Gemini", response);
|
|
8799
|
+
}
|
|
8800
|
+
const body = await response.json();
|
|
8801
|
+
if (body.usageMetadata && typeof body.usageMetadata === "object") {
|
|
8802
|
+
await options.onUsage?.(body.usageMetadata);
|
|
8803
|
+
}
|
|
8804
|
+
const toolCalls = extractGeminiToolCalls(body);
|
|
8805
|
+
if (toolCalls.length) {
|
|
8806
|
+
return {
|
|
8807
|
+
assistantText: extractGeminiText(body) || undefined,
|
|
8808
|
+
toolCalls
|
|
8809
|
+
};
|
|
8810
|
+
}
|
|
8811
|
+
return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
|
|
8812
|
+
}
|
|
8813
|
+
};
|
|
8814
|
+
};
|
|
6876
8815
|
// src/sqliteStore.ts
|
|
6877
8816
|
import { Database } from "bun:sqlite";
|
|
6878
8817
|
var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
|
|
@@ -7354,6 +9293,361 @@ var createVoiceMemoryStore = () => {
|
|
|
7354
9293
|
};
|
|
7355
9294
|
return { get, getOrCreate, list, remove, set };
|
|
7356
9295
|
};
|
|
9296
|
+
// src/opsWebhook.ts
|
|
9297
|
+
import { Elysia as Elysia5 } from "elysia";
|
|
9298
|
+
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
9299
|
+
var signVoiceOpsWebhookBody = async (input) => {
|
|
9300
|
+
const encoder = new TextEncoder;
|
|
9301
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
9302
|
+
hash: "SHA-256",
|
|
9303
|
+
name: "HMAC"
|
|
9304
|
+
}, false, ["sign"]);
|
|
9305
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
|
|
9306
|
+
return `sha256=${toHex5(new Uint8Array(signature))}`;
|
|
9307
|
+
};
|
|
9308
|
+
var timingSafeEqual = (left, right) => {
|
|
9309
|
+
const encoder = new TextEncoder;
|
|
9310
|
+
const leftBytes = encoder.encode(left);
|
|
9311
|
+
const rightBytes = encoder.encode(right);
|
|
9312
|
+
if (leftBytes.length !== rightBytes.length) {
|
|
9313
|
+
return false;
|
|
9314
|
+
}
|
|
9315
|
+
let diff = 0;
|
|
9316
|
+
for (let index = 0;index < leftBytes.length; index += 1) {
|
|
9317
|
+
diff |= leftBytes[index] ^ rightBytes[index];
|
|
9318
|
+
}
|
|
9319
|
+
return diff === 0;
|
|
9320
|
+
};
|
|
9321
|
+
var resolveWebhookLink = async (resolver, event) => {
|
|
9322
|
+
if (typeof resolver === "function") {
|
|
9323
|
+
return resolver({
|
|
9324
|
+
event
|
|
9325
|
+
});
|
|
9326
|
+
}
|
|
9327
|
+
return resolver;
|
|
9328
|
+
};
|
|
9329
|
+
var joinBaseUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
9330
|
+
var asString = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9331
|
+
var buildVoiceOpsWebhookEntity = (event) => ({
|
|
9332
|
+
disposition: asString(event.payload.disposition),
|
|
9333
|
+
outcome: asString(event.payload.outcome),
|
|
9334
|
+
priority: asString(event.payload.priority),
|
|
9335
|
+
queue: asString(event.payload.queue),
|
|
9336
|
+
reviewId: asString(event.payload.reviewId),
|
|
9337
|
+
scenarioId: asString(event.payload.scenarioId),
|
|
9338
|
+
sessionId: asString(event.payload.sessionId),
|
|
9339
|
+
status: asString(event.payload.status),
|
|
9340
|
+
target: asString(event.payload.target),
|
|
9341
|
+
taskId: asString(event.payload.taskId)
|
|
9342
|
+
});
|
|
9343
|
+
var createVoiceOpsWebhookEnvelope = async (input) => {
|
|
9344
|
+
const entity = buildVoiceOpsWebhookEntity(input.event);
|
|
9345
|
+
const replayHref = await resolveWebhookLink(input.replayHref, input.event) ?? (input.baseUrl && entity.sessionId ? joinBaseUrl(input.baseUrl, `/api/voice-sessions/${encodeURIComponent(entity.sessionId)}/replay`) : undefined);
|
|
9346
|
+
const links = {
|
|
9347
|
+
event: await resolveWebhookLink(input.eventHref, input.event),
|
|
9348
|
+
replay: replayHref,
|
|
9349
|
+
review: await resolveWebhookLink(input.reviewHref, input.event),
|
|
9350
|
+
task: await resolveWebhookLink(input.taskHref, input.event)
|
|
9351
|
+
};
|
|
9352
|
+
return {
|
|
9353
|
+
entity,
|
|
9354
|
+
event: {
|
|
9355
|
+
createdAt: input.event.createdAt,
|
|
9356
|
+
id: input.event.id,
|
|
9357
|
+
payload: input.event.payload,
|
|
9358
|
+
type: input.event.type
|
|
9359
|
+
},
|
|
9360
|
+
links: links.event || links.replay || links.review || links.task ? links : undefined,
|
|
9361
|
+
schemaVersion: 1,
|
|
9362
|
+
source: "absolutejs-voice"
|
|
9363
|
+
};
|
|
9364
|
+
};
|
|
9365
|
+
var createVoiceOpsWebhookSink = (options) => createVoiceIntegrationHTTPSink({
|
|
9366
|
+
...options,
|
|
9367
|
+
body: ({ event }) => createVoiceOpsWebhookEnvelope({
|
|
9368
|
+
baseUrl: options.baseUrl,
|
|
9369
|
+
event,
|
|
9370
|
+
eventHref: options.eventHref,
|
|
9371
|
+
replayHref: options.replayHref,
|
|
9372
|
+
reviewHref: options.reviewHref,
|
|
9373
|
+
taskHref: options.taskHref
|
|
9374
|
+
}),
|
|
9375
|
+
kind: options.kind ?? "ops-webhook"
|
|
9376
|
+
});
|
|
9377
|
+
var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
9378
|
+
if (!input.secret) {
|
|
9379
|
+
return {
|
|
9380
|
+
ok: false,
|
|
9381
|
+
reason: "missing-secret"
|
|
9382
|
+
};
|
|
9383
|
+
}
|
|
9384
|
+
if (!input.signature) {
|
|
9385
|
+
return {
|
|
9386
|
+
ok: false,
|
|
9387
|
+
reason: "missing-signature"
|
|
9388
|
+
};
|
|
9389
|
+
}
|
|
9390
|
+
if (!input.signature.startsWith("sha256=")) {
|
|
9391
|
+
return {
|
|
9392
|
+
ok: false,
|
|
9393
|
+
reason: "unsupported-algorithm"
|
|
9394
|
+
};
|
|
9395
|
+
}
|
|
9396
|
+
if (!input.timestamp) {
|
|
9397
|
+
return {
|
|
9398
|
+
ok: false,
|
|
9399
|
+
reason: "missing-timestamp"
|
|
9400
|
+
};
|
|
9401
|
+
}
|
|
9402
|
+
const timestampMs = Number(input.timestamp);
|
|
9403
|
+
const toleranceMs = Math.max(0, input.toleranceMs ?? 5 * 60 * 1000);
|
|
9404
|
+
if (!Number.isFinite(timestampMs) || toleranceMs > 0 && Math.abs((input.now ?? Date.now()) - timestampMs) > toleranceMs) {
|
|
9405
|
+
return {
|
|
9406
|
+
ok: false,
|
|
9407
|
+
reason: "stale-timestamp"
|
|
9408
|
+
};
|
|
9409
|
+
}
|
|
9410
|
+
const expected = await signVoiceOpsWebhookBody({
|
|
9411
|
+
body: input.body,
|
|
9412
|
+
secret: input.secret,
|
|
9413
|
+
timestamp: input.timestamp
|
|
9414
|
+
});
|
|
9415
|
+
if (!timingSafeEqual(expected, input.signature)) {
|
|
9416
|
+
return {
|
|
9417
|
+
ok: false,
|
|
9418
|
+
reason: "invalid-signature"
|
|
9419
|
+
};
|
|
9420
|
+
}
|
|
9421
|
+
return {
|
|
9422
|
+
ok: true
|
|
9423
|
+
};
|
|
9424
|
+
};
|
|
9425
|
+
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
9426
|
+
const path = options.path ?? "/api/voice-ops/webhook";
|
|
9427
|
+
return new Elysia5().post(path, async ({ body, request, set }) => {
|
|
9428
|
+
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
9429
|
+
if (options.signingSecret) {
|
|
9430
|
+
const verification = await verifyVoiceOpsWebhookSignature({
|
|
9431
|
+
body: bodyText,
|
|
9432
|
+
secret: options.signingSecret,
|
|
9433
|
+
signature: request.headers.get("x-absolutejs-signature"),
|
|
9434
|
+
timestamp: request.headers.get("x-absolutejs-timestamp"),
|
|
9435
|
+
toleranceMs: options.toleranceMs
|
|
9436
|
+
});
|
|
9437
|
+
if (!verification.ok) {
|
|
9438
|
+
set.status = 401;
|
|
9439
|
+
return {
|
|
9440
|
+
ok: false,
|
|
9441
|
+
reason: verification.reason
|
|
9442
|
+
};
|
|
9443
|
+
}
|
|
9444
|
+
}
|
|
9445
|
+
const envelope = JSON.parse(bodyText);
|
|
9446
|
+
await options.onEnvelope?.({
|
|
9447
|
+
envelope,
|
|
9448
|
+
request
|
|
9449
|
+
});
|
|
9450
|
+
return {
|
|
9451
|
+
eventId: envelope.event?.id,
|
|
9452
|
+
ok: true,
|
|
9453
|
+
type: envelope.event?.type
|
|
9454
|
+
};
|
|
9455
|
+
}, {
|
|
9456
|
+
parse: "text"
|
|
9457
|
+
});
|
|
9458
|
+
};
|
|
9459
|
+
// src/handoffHealth.ts
|
|
9460
|
+
import { Elysia as Elysia6 } from "elysia";
|
|
9461
|
+
var escapeHtml7 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9462
|
+
var getString4 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9463
|
+
var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
|
|
9464
|
+
var increment3 = (record, key) => {
|
|
9465
|
+
record[key] = (record[key] ?? 0) + 1;
|
|
9466
|
+
};
|
|
9467
|
+
var normalizeDelivery = (adapterId, value) => {
|
|
9468
|
+
const record = value && typeof value === "object" ? value : {};
|
|
9469
|
+
return {
|
|
9470
|
+
adapterId: getString4(record.adapterId) ?? adapterId,
|
|
9471
|
+
adapterKind: getString4(record.adapterKind),
|
|
9472
|
+
deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
|
|
9473
|
+
deliveredTo: getString4(record.deliveredTo),
|
|
9474
|
+
error: getString4(record.error),
|
|
9475
|
+
status: isStatus(record.status) ? record.status : "failed"
|
|
9476
|
+
};
|
|
9477
|
+
};
|
|
9478
|
+
var normalizeDeliveries = (payload) => {
|
|
9479
|
+
const deliveries = payload.deliveries;
|
|
9480
|
+
if (!deliveries || typeof deliveries !== "object") {
|
|
9481
|
+
return [];
|
|
9482
|
+
}
|
|
9483
|
+
return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
|
|
9484
|
+
};
|
|
9485
|
+
var resolveReplayHref = (event, replayHref) => {
|
|
9486
|
+
if (replayHref === false) {
|
|
9487
|
+
return;
|
|
9488
|
+
}
|
|
9489
|
+
if (typeof replayHref === "function") {
|
|
9490
|
+
return replayHref(event);
|
|
9491
|
+
}
|
|
9492
|
+
return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
|
|
9493
|
+
};
|
|
9494
|
+
var summarizeVoiceHandoffHealth = async (options = {}) => {
|
|
9495
|
+
const sourceEvents = options.events ?? await options.store?.list() ?? [];
|
|
9496
|
+
const search = options.q?.trim().toLowerCase();
|
|
9497
|
+
const byAction = {};
|
|
9498
|
+
const byAdapter = {};
|
|
9499
|
+
const byStatus = {
|
|
9500
|
+
delivered: 0,
|
|
9501
|
+
failed: 0,
|
|
9502
|
+
skipped: 0
|
|
9503
|
+
};
|
|
9504
|
+
const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
|
|
9505
|
+
const status = isStatus(event.payload.status) ? event.payload.status : "failed";
|
|
9506
|
+
const deliveries = normalizeDeliveries(event.payload);
|
|
9507
|
+
const item = {
|
|
9508
|
+
action: getString4(event.payload.action),
|
|
9509
|
+
at: event.at,
|
|
9510
|
+
deliveries,
|
|
9511
|
+
reason: getString4(event.payload.reason),
|
|
9512
|
+
sessionId: event.sessionId,
|
|
9513
|
+
status,
|
|
9514
|
+
target: getString4(event.payload.target)
|
|
9515
|
+
};
|
|
9516
|
+
return {
|
|
9517
|
+
...item,
|
|
9518
|
+
replayHref: resolveReplayHref(item, options.replayHref)
|
|
9519
|
+
};
|
|
9520
|
+
}).filter((event) => {
|
|
9521
|
+
if (options.status && options.status !== "all" && event.status !== options.status) {
|
|
9522
|
+
return false;
|
|
9523
|
+
}
|
|
9524
|
+
if (!search) {
|
|
9525
|
+
return true;
|
|
9526
|
+
}
|
|
9527
|
+
return [
|
|
9528
|
+
event.action,
|
|
9529
|
+
event.reason,
|
|
9530
|
+
event.sessionId,
|
|
9531
|
+
event.status,
|
|
9532
|
+
event.target,
|
|
9533
|
+
...event.deliveries.flatMap((delivery) => [
|
|
9534
|
+
delivery.adapterId,
|
|
9535
|
+
delivery.adapterKind,
|
|
9536
|
+
delivery.deliveredTo,
|
|
9537
|
+
delivery.error,
|
|
9538
|
+
delivery.status
|
|
9539
|
+
])
|
|
9540
|
+
].some((value) => value?.toLowerCase().includes(search));
|
|
9541
|
+
}).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
|
|
9542
|
+
for (const event of events) {
|
|
9543
|
+
byStatus[event.status] += 1;
|
|
9544
|
+
if (event.action) {
|
|
9545
|
+
increment3(byAction, event.action);
|
|
9546
|
+
}
|
|
9547
|
+
for (const delivery of event.deliveries) {
|
|
9548
|
+
byAdapter[delivery.adapterId] ??= {
|
|
9549
|
+
delivered: 0,
|
|
9550
|
+
failed: 0,
|
|
9551
|
+
skipped: 0
|
|
9552
|
+
};
|
|
9553
|
+
byAdapter[delivery.adapterId][delivery.status] += 1;
|
|
9554
|
+
}
|
|
9555
|
+
}
|
|
9556
|
+
return {
|
|
9557
|
+
byAction,
|
|
9558
|
+
byAdapter,
|
|
9559
|
+
byStatus,
|
|
9560
|
+
events,
|
|
9561
|
+
failed: byStatus.failed,
|
|
9562
|
+
total: events.length
|
|
9563
|
+
};
|
|
9564
|
+
};
|
|
9565
|
+
var renderMetricGrid = (summary) => [
|
|
9566
|
+
'<section class="voice-handoff-health-grid">',
|
|
9567
|
+
`<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
|
|
9568
|
+
`<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
|
|
9569
|
+
`<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
|
|
9570
|
+
`<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
|
|
9571
|
+
"</section>"
|
|
9572
|
+
].join("");
|
|
9573
|
+
var renderActionSummary = (summary) => {
|
|
9574
|
+
const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
|
|
9575
|
+
const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
|
|
9576
|
+
return [
|
|
9577
|
+
'<section class="voice-handoff-health-columns">',
|
|
9578
|
+
"<article><h3>Actions</h3>",
|
|
9579
|
+
actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml7(action)}: ${String(count)}</li>`).join("")}</ul>`,
|
|
9580
|
+
"</article>",
|
|
9581
|
+
"<article><h3>Adapters</h3>",
|
|
9582
|
+
adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml7(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
|
|
9583
|
+
"</article>",
|
|
9584
|
+
"</section>"
|
|
9585
|
+
].join("");
|
|
9586
|
+
};
|
|
9587
|
+
var renderVoiceHandoffHealthHTML = (summary) => [
|
|
9588
|
+
'<div class="voice-handoff-health">',
|
|
9589
|
+
renderMetricGrid(summary),
|
|
9590
|
+
renderActionSummary(summary),
|
|
9591
|
+
"<section>",
|
|
9592
|
+
"<h3>Recent Handoffs</h3>",
|
|
9593
|
+
summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
|
|
9594
|
+
'<div class="voice-handoff-health-events">',
|
|
9595
|
+
...summary.events.map((event) => [
|
|
9596
|
+
`<article class="${escapeHtml7(event.status)}">`,
|
|
9597
|
+
'<div class="voice-handoff-health-event-header">',
|
|
9598
|
+
`<strong>${escapeHtml7(event.action ?? "handoff")}</strong>`,
|
|
9599
|
+
`<span>${escapeHtml7(event.status)}</span>`,
|
|
9600
|
+
"</div>",
|
|
9601
|
+
`<p><small>${escapeHtml7(event.sessionId)}</small></p>`,
|
|
9602
|
+
event.target ? `<p>Target: ${escapeHtml7(event.target)}</p>` : "",
|
|
9603
|
+
event.reason ? `<p>Reason: ${escapeHtml7(event.reason)}</p>` : "",
|
|
9604
|
+
event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
|
|
9605
|
+
"<li>",
|
|
9606
|
+
`${escapeHtml7(delivery.adapterId)}: ${escapeHtml7(delivery.status)}`,
|
|
9607
|
+
delivery.deliveredTo ? ` to ${escapeHtml7(delivery.deliveredTo)}` : "",
|
|
9608
|
+
delivery.error ? ` (${escapeHtml7(delivery.error)})` : "",
|
|
9609
|
+
"</li>"
|
|
9610
|
+
].join("")).join("")}</ul>` : "",
|
|
9611
|
+
event.replayHref ? `<p><a href="${escapeHtml7(event.replayHref)}">Open replay</a></p>` : "",
|
|
9612
|
+
"</article>"
|
|
9613
|
+
].join("")),
|
|
9614
|
+
"</div>"
|
|
9615
|
+
].join(""),
|
|
9616
|
+
"</section>",
|
|
9617
|
+
"</div>"
|
|
9618
|
+
].join("");
|
|
9619
|
+
var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
|
|
9620
|
+
...options,
|
|
9621
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
9622
|
+
q: query?.q ?? options.q,
|
|
9623
|
+
status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
|
|
9624
|
+
});
|
|
9625
|
+
var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
|
|
9626
|
+
const summary = await summarizeVoiceHandoffHealth({
|
|
9627
|
+
...options,
|
|
9628
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
9629
|
+
q: query?.q ?? options.q,
|
|
9630
|
+
status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
|
|
9631
|
+
});
|
|
9632
|
+
const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
|
|
9633
|
+
return new Response(body, {
|
|
9634
|
+
headers: {
|
|
9635
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9636
|
+
...options.headers
|
|
9637
|
+
}
|
|
9638
|
+
});
|
|
9639
|
+
};
|
|
9640
|
+
var createVoiceHandoffHealthRoutes = (options = {}) => {
|
|
9641
|
+
const path = options.path ?? "/api/voice-handoffs";
|
|
9642
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
9643
|
+
const routes = new Elysia6({
|
|
9644
|
+
name: options.name ?? "absolutejs-voice-handoff-health"
|
|
9645
|
+
}).get(path, createVoiceHandoffHealthJSONHandler(options));
|
|
9646
|
+
if (htmlPath) {
|
|
9647
|
+
routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
|
|
9648
|
+
}
|
|
9649
|
+
return routes;
|
|
9650
|
+
};
|
|
7357
9651
|
// src/queue.ts
|
|
7358
9652
|
var releaseLeaseScript = `
|
|
7359
9653
|
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
|
@@ -7425,6 +9719,8 @@ var shouldDeadLetterSinkEvent = (event, sinks, maxFailures) => typeof maxFailure
|
|
|
7425
9719
|
var shouldDeadLetterTask = (task, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (task.processingAttempts ?? 0) >= maxFailures;
|
|
7426
9720
|
var shouldProcessTraceDeliveryStatus = (status, allowed) => allowed.includes(status);
|
|
7427
9721
|
var shouldDeadLetterTraceDelivery = (delivery, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (delivery.deliveryAttempts ?? 0) >= maxFailures;
|
|
9722
|
+
var shouldProcessHandoffDeliveryStatus = (status, allowed) => allowed.includes(status);
|
|
9723
|
+
var shouldDeadLetterHandoffDelivery = (delivery, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (delivery.deliveryAttempts ?? 0) >= maxFailures;
|
|
7428
9724
|
var summarizeVoiceIntegrationEvents = (events, input = {}) => {
|
|
7429
9725
|
const buildSummary = async () => {
|
|
7430
9726
|
const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((event) => event.id) : []);
|
|
@@ -7506,6 +9802,48 @@ var summarizeVoiceTraceSinkDeliveries = (deliveries, input = {}) => {
|
|
|
7506
9802
|
};
|
|
7507
9803
|
return buildSummary();
|
|
7508
9804
|
};
|
|
9805
|
+
var summarizeVoiceHandoffDeliveries = (deliveries, input = {}) => {
|
|
9806
|
+
const buildSummary = async () => {
|
|
9807
|
+
const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((delivery) => delivery.id) : []);
|
|
9808
|
+
const byAction = new Map;
|
|
9809
|
+
const summary = {
|
|
9810
|
+
byAction: [],
|
|
9811
|
+
deadLettered: 0,
|
|
9812
|
+
delivered: 0,
|
|
9813
|
+
failed: 0,
|
|
9814
|
+
pending: 0,
|
|
9815
|
+
retryEligible: 0,
|
|
9816
|
+
skipped: 0,
|
|
9817
|
+
total: deliveries.length
|
|
9818
|
+
};
|
|
9819
|
+
for (const delivery of deliveries) {
|
|
9820
|
+
byAction.set(delivery.action, (byAction.get(delivery.action) ?? 0) + 1);
|
|
9821
|
+
if (deadLetterIds.has(delivery.id)) {
|
|
9822
|
+
summary.deadLettered += 1;
|
|
9823
|
+
}
|
|
9824
|
+
switch (delivery.deliveryStatus) {
|
|
9825
|
+
case "delivered":
|
|
9826
|
+
summary.delivered += 1;
|
|
9827
|
+
break;
|
|
9828
|
+
case "failed":
|
|
9829
|
+
summary.failed += 1;
|
|
9830
|
+
if ((delivery.deliveryAttempts ?? 0) > 0) {
|
|
9831
|
+
summary.retryEligible += 1;
|
|
9832
|
+
}
|
|
9833
|
+
break;
|
|
9834
|
+
case "skipped":
|
|
9835
|
+
summary.skipped += 1;
|
|
9836
|
+
break;
|
|
9837
|
+
case "pending":
|
|
9838
|
+
summary.pending += 1;
|
|
9839
|
+
break;
|
|
9840
|
+
}
|
|
9841
|
+
}
|
|
9842
|
+
summary.byAction = [...byAction.entries()].sort((left, right) => right[1] - left[1]);
|
|
9843
|
+
return summary;
|
|
9844
|
+
};
|
|
9845
|
+
return buildSummary();
|
|
9846
|
+
};
|
|
7509
9847
|
var summarizeVoiceOpsTaskQueue = (tasks, input = {}) => {
|
|
7510
9848
|
const buildSummary = async () => {
|
|
7511
9849
|
const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((task) => task.id) : []);
|
|
@@ -7935,6 +10273,108 @@ var createVoiceTraceSinkDeliveryWorkerLoop = (options) => {
|
|
|
7935
10273
|
tick
|
|
7936
10274
|
};
|
|
7937
10275
|
};
|
|
10276
|
+
var createVoiceHandoffDeliveryWorker = (options) => {
|
|
10277
|
+
const allowedStatuses = options.statuses ?? ["pending", "failed"];
|
|
10278
|
+
const leaseMs = Math.max(1, options.leaseMs ?? 30000);
|
|
10279
|
+
return {
|
|
10280
|
+
drain: async () => {
|
|
10281
|
+
const result = {
|
|
10282
|
+
alreadyProcessed: 0,
|
|
10283
|
+
attempted: 0,
|
|
10284
|
+
deadLettered: 0,
|
|
10285
|
+
delivered: 0,
|
|
10286
|
+
failed: 0,
|
|
10287
|
+
skipped: 0
|
|
10288
|
+
};
|
|
10289
|
+
const deliveries = [...await options.deliveries.list()].sort((left, right) => left.createdAt - right.createdAt);
|
|
10290
|
+
for (const delivery of deliveries) {
|
|
10291
|
+
if (!shouldProcessHandoffDeliveryStatus(delivery.deliveryStatus, allowedStatuses)) {
|
|
10292
|
+
continue;
|
|
10293
|
+
}
|
|
10294
|
+
if (shouldDeadLetterHandoffDelivery(delivery, options.maxFailures)) {
|
|
10295
|
+
await options.deadLetters?.set(delivery.id, delivery);
|
|
10296
|
+
await options.onDeadLetter?.(delivery);
|
|
10297
|
+
result.deadLettered += 1;
|
|
10298
|
+
continue;
|
|
10299
|
+
}
|
|
10300
|
+
const claimed = await options.leases.claim({
|
|
10301
|
+
leaseMs,
|
|
10302
|
+
taskId: delivery.id,
|
|
10303
|
+
workerId: options.workerId
|
|
10304
|
+
});
|
|
10305
|
+
if (!claimed) {
|
|
10306
|
+
continue;
|
|
10307
|
+
}
|
|
10308
|
+
try {
|
|
10309
|
+
const idempotencyKey = `${delivery.id}:handoff`;
|
|
10310
|
+
if (options.idempotency && await options.idempotency.has(idempotencyKey)) {
|
|
10311
|
+
result.alreadyProcessed += 1;
|
|
10312
|
+
continue;
|
|
10313
|
+
}
|
|
10314
|
+
result.attempted += 1;
|
|
10315
|
+
const updatedDelivery = await deliverVoiceHandoffDelivery({
|
|
10316
|
+
adapters: options.adapters,
|
|
10317
|
+
api: options.api,
|
|
10318
|
+
delivery,
|
|
10319
|
+
failMode: options.failMode
|
|
10320
|
+
});
|
|
10321
|
+
await options.deliveries.set(updatedDelivery.id, updatedDelivery);
|
|
10322
|
+
if (updatedDelivery.deliveryStatus === "delivered" || updatedDelivery.deliveryStatus === "skipped") {
|
|
10323
|
+
await options.idempotency?.set(idempotencyKey, {
|
|
10324
|
+
ttlSeconds: options.idempotencyTtlSeconds
|
|
10325
|
+
});
|
|
10326
|
+
}
|
|
10327
|
+
if (updatedDelivery.deliveryStatus === "delivered") {
|
|
10328
|
+
result.delivered += 1;
|
|
10329
|
+
} else if (updatedDelivery.deliveryStatus === "skipped") {
|
|
10330
|
+
result.skipped += 1;
|
|
10331
|
+
} else if (updatedDelivery.deliveryStatus === "failed") {
|
|
10332
|
+
result.failed += 1;
|
|
10333
|
+
if (shouldDeadLetterHandoffDelivery(updatedDelivery, options.maxFailures)) {
|
|
10334
|
+
await options.deadLetters?.set(updatedDelivery.id, updatedDelivery);
|
|
10335
|
+
await options.onDeadLetter?.(updatedDelivery);
|
|
10336
|
+
result.deadLettered += 1;
|
|
10337
|
+
}
|
|
10338
|
+
}
|
|
10339
|
+
} finally {
|
|
10340
|
+
await options.leases.release({
|
|
10341
|
+
taskId: delivery.id,
|
|
10342
|
+
workerId: options.workerId
|
|
10343
|
+
});
|
|
10344
|
+
}
|
|
10345
|
+
}
|
|
10346
|
+
return result;
|
|
10347
|
+
}
|
|
10348
|
+
};
|
|
10349
|
+
};
|
|
10350
|
+
var createVoiceHandoffDeliveryWorkerLoop = (options) => {
|
|
10351
|
+
const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 1000);
|
|
10352
|
+
let timer;
|
|
10353
|
+
let running = false;
|
|
10354
|
+
const tick = async () => options.worker.drain();
|
|
10355
|
+
return {
|
|
10356
|
+
isRunning: () => running,
|
|
10357
|
+
start: () => {
|
|
10358
|
+
if (timer) {
|
|
10359
|
+
return;
|
|
10360
|
+
}
|
|
10361
|
+
running = true;
|
|
10362
|
+
timer = setInterval(() => {
|
|
10363
|
+
tick().catch((error) => {
|
|
10364
|
+
options.onError?.(error);
|
|
10365
|
+
});
|
|
10366
|
+
}, pollIntervalMs);
|
|
10367
|
+
},
|
|
10368
|
+
stop: () => {
|
|
10369
|
+
if (timer) {
|
|
10370
|
+
clearInterval(timer);
|
|
10371
|
+
timer = undefined;
|
|
10372
|
+
}
|
|
10373
|
+
running = false;
|
|
10374
|
+
},
|
|
10375
|
+
tick
|
|
10376
|
+
};
|
|
10377
|
+
};
|
|
7938
10378
|
var createVoiceOpsTaskWorker = (options) => {
|
|
7939
10379
|
const leaseMs = Math.max(1, options.leaseMs ?? 30000);
|
|
7940
10380
|
const getTask = async (taskId) => {
|
|
@@ -8070,10 +10510,10 @@ var createVoiceOpsTaskProcessorWorker = (options) => ({
|
|
|
8070
10510
|
result.completed += 1;
|
|
8071
10511
|
} catch (error) {
|
|
8072
10512
|
await options.onError?.(error, task);
|
|
8073
|
-
const
|
|
10513
|
+
const errorMessage2 = error instanceof Error ? error.message : String(error);
|
|
8074
10514
|
const failedTask = failVoiceOpsTask(task, {
|
|
8075
10515
|
actor: task.claimedBy ?? "ops-worker",
|
|
8076
|
-
error:
|
|
10516
|
+
error: errorMessage2
|
|
8077
10517
|
});
|
|
8078
10518
|
if (shouldDeadLetterTask(failedTask, options.maxFailures)) {
|
|
8079
10519
|
const deadLetterTask = deadLetterVoiceOpsTask(failedTask, {
|
|
@@ -8886,7 +11326,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
|
|
|
8886
11326
|
import { Buffer as Buffer2 } from "buffer";
|
|
8887
11327
|
var TWILIO_MULAW_SAMPLE_RATE = 8000;
|
|
8888
11328
|
var VOICE_PCM_SAMPLE_RATE = 16000;
|
|
8889
|
-
var
|
|
11329
|
+
var escapeXml2 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
8890
11330
|
var normalizeOnTurn2 = (handler) => {
|
|
8891
11331
|
if (handler.length > 1) {
|
|
8892
11332
|
const directHandler = handler;
|
|
@@ -9082,8 +11522,8 @@ var createTwilioSocketAdapter = (socket, getState) => ({
|
|
|
9082
11522
|
}
|
|
9083
11523
|
});
|
|
9084
11524
|
var createTwilioVoiceResponse = (options) => {
|
|
9085
|
-
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${
|
|
9086
|
-
return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${
|
|
11525
|
+
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
|
|
11526
|
+
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>`;
|
|
9087
11527
|
};
|
|
9088
11528
|
var createTwilioMediaStreamBridge = (socket, options) => {
|
|
9089
11529
|
const runtimePreset = resolveVoiceRuntimePreset(options.preset);
|
|
@@ -9319,15 +11759,22 @@ export {
|
|
|
9319
11759
|
withVoiceOpsTaskId,
|
|
9320
11760
|
withVoiceIntegrationEventId,
|
|
9321
11761
|
voice,
|
|
11762
|
+
verifyVoiceOpsWebhookSignature,
|
|
9322
11763
|
transcodeTwilioInboundPayloadToPCM16,
|
|
9323
11764
|
transcodePCMToTwilioOutboundPayload,
|
|
9324
11765
|
summarizeVoiceTraceSinkDeliveries,
|
|
9325
11766
|
summarizeVoiceTrace,
|
|
11767
|
+
summarizeVoiceSessions,
|
|
11768
|
+
summarizeVoiceSessionReplay,
|
|
11769
|
+
summarizeVoiceProviderHealth,
|
|
9326
11770
|
summarizeVoiceOpsTasks,
|
|
9327
11771
|
summarizeVoiceOpsTaskQueue,
|
|
9328
11772
|
summarizeVoiceOpsTaskAnalytics,
|
|
9329
11773
|
summarizeVoiceIntegrationEvents,
|
|
11774
|
+
summarizeVoiceHandoffHealth,
|
|
11775
|
+
summarizeVoiceHandoffDeliveries,
|
|
9330
11776
|
summarizeVoiceAssistantRuns,
|
|
11777
|
+
summarizeVoiceAssistantHealth,
|
|
9331
11778
|
startVoiceOpsTask,
|
|
9332
11779
|
shapeTelephonyAssistantText,
|
|
9333
11780
|
selectVoiceTraceEventsForPrune,
|
|
@@ -9339,14 +11786,19 @@ export {
|
|
|
9339
11786
|
resolveVoiceOpsTaskAssignment,
|
|
9340
11787
|
resolveVoiceOpsTaskAgeBucket,
|
|
9341
11788
|
resolveVoiceOpsPreset,
|
|
11789
|
+
resolveVoiceAssistantMemoryNamespace,
|
|
9342
11790
|
resolveTurnDetectionConfig,
|
|
9343
11791
|
resolveAudioConditioningConfig,
|
|
9344
11792
|
requeueVoiceOpsTask,
|
|
9345
11793
|
reopenVoiceOpsTask,
|
|
9346
11794
|
renderVoiceTraceMarkdown,
|
|
9347
11795
|
renderVoiceTraceHTML,
|
|
11796
|
+
renderVoiceSessionsHTML,
|
|
11797
|
+
renderVoiceProviderHealthHTML,
|
|
11798
|
+
renderVoiceHandoffHealthHTML,
|
|
9348
11799
|
renderVoiceCallReviewMarkdown,
|
|
9349
11800
|
renderVoiceCallReviewHTML,
|
|
11801
|
+
renderVoiceAssistantHealthHTML,
|
|
9350
11802
|
redactVoiceTraceText,
|
|
9351
11803
|
redactVoiceTraceEvents,
|
|
9352
11804
|
redactVoiceTraceEvent,
|
|
@@ -9366,13 +11818,17 @@ export {
|
|
|
9366
11818
|
deliverVoiceTraceEventsToSinks,
|
|
9367
11819
|
deliverVoiceIntegrationEventToSinks,
|
|
9368
11820
|
deliverVoiceIntegrationEvent,
|
|
11821
|
+
deliverVoiceHandoffDelivery,
|
|
11822
|
+
deliverVoiceHandoff,
|
|
9369
11823
|
decodeTwilioMulawBase64,
|
|
9370
11824
|
deadLetterVoiceOpsTask,
|
|
9371
11825
|
createVoiceZendeskTicketUpdateSink,
|
|
9372
11826
|
createVoiceZendeskTicketSyncSinks,
|
|
9373
11827
|
createVoiceZendeskTicketSink,
|
|
11828
|
+
createVoiceWebhookHandoffAdapter,
|
|
9374
11829
|
createVoiceWebhookDeliveryWorkerLoop,
|
|
9375
11830
|
createVoiceWebhookDeliveryWorker,
|
|
11831
|
+
createVoiceTwilioRedirectHandoffAdapter,
|
|
9376
11832
|
createVoiceTraceSinkStore,
|
|
9377
11833
|
createVoiceTraceSinkDeliveryWorkerLoop,
|
|
9378
11834
|
createVoiceTraceSinkDeliveryWorker,
|
|
@@ -9384,7 +11840,13 @@ export {
|
|
|
9384
11840
|
createVoiceTaskUpdatedEvent,
|
|
9385
11841
|
createVoiceTaskSLABreachedEvent,
|
|
9386
11842
|
createVoiceTaskCreatedEvent,
|
|
11843
|
+
createVoiceSessionsJSONHandler,
|
|
11844
|
+
createVoiceSessionsHTMLHandler,
|
|
11845
|
+
createVoiceSessionReplayRoutes,
|
|
11846
|
+
createVoiceSessionReplayJSONHandler,
|
|
11847
|
+
createVoiceSessionReplayHTMLHandler,
|
|
9387
11848
|
createVoiceSessionRecord,
|
|
11849
|
+
createVoiceSessionListRoutes,
|
|
9388
11850
|
createVoiceSession,
|
|
9389
11851
|
createVoiceSTTRoutingCorrectionHandler,
|
|
9390
11852
|
createVoiceSQLiteTraceSinkDeliveryStore,
|
|
@@ -9399,6 +11861,10 @@ export {
|
|
|
9399
11861
|
createVoiceReviewSavedEvent,
|
|
9400
11862
|
createVoiceRedisTaskLeaseCoordinator,
|
|
9401
11863
|
createVoiceRedisIdempotencyStore,
|
|
11864
|
+
createVoiceProviderRouter,
|
|
11865
|
+
createVoiceProviderHealthRoutes,
|
|
11866
|
+
createVoiceProviderHealthJSONHandler,
|
|
11867
|
+
createVoiceProviderHealthHTMLHandler,
|
|
9402
11868
|
createVoicePostgresTraceSinkDeliveryStore,
|
|
9403
11869
|
createVoicePostgresTraceEventStore,
|
|
9404
11870
|
createVoicePostgresTaskStore,
|
|
@@ -9407,6 +11873,9 @@ export {
|
|
|
9407
11873
|
createVoicePostgresReviewStore,
|
|
9408
11874
|
createVoicePostgresIntegrationEventStore,
|
|
9409
11875
|
createVoicePostgresExternalObjectMapStore,
|
|
11876
|
+
createVoiceOpsWebhookSink,
|
|
11877
|
+
createVoiceOpsWebhookReceiverRoutes,
|
|
11878
|
+
createVoiceOpsWebhookEnvelope,
|
|
9410
11879
|
createVoiceOpsTaskWorker,
|
|
9411
11880
|
createVoiceOpsTaskProcessorWorkerLoop,
|
|
9412
11881
|
createVoiceOpsTaskProcessorWorker,
|
|
@@ -9414,6 +11883,8 @@ export {
|
|
|
9414
11883
|
createVoiceMemoryTraceSinkDeliveryStore,
|
|
9415
11884
|
createVoiceMemoryTraceEventStore,
|
|
9416
11885
|
createVoiceMemoryStore,
|
|
11886
|
+
createVoiceMemoryHandoffDeliveryStore,
|
|
11887
|
+
createVoiceMemoryAssistantMemoryStore,
|
|
9417
11888
|
createVoiceLinearIssueUpdateSink,
|
|
9418
11889
|
createVoiceLinearIssueSyncSinks,
|
|
9419
11890
|
createVoiceLinearIssueSink,
|
|
@@ -9425,6 +11896,12 @@ export {
|
|
|
9425
11896
|
createVoiceHubSpotTaskSyncSinks,
|
|
9426
11897
|
createVoiceHubSpotTaskSink,
|
|
9427
11898
|
createVoiceHelpdeskTicketSink,
|
|
11899
|
+
createVoiceHandoffHealthRoutes,
|
|
11900
|
+
createVoiceHandoffHealthJSONHandler,
|
|
11901
|
+
createVoiceHandoffHealthHTMLHandler,
|
|
11902
|
+
createVoiceHandoffDeliveryWorkerLoop,
|
|
11903
|
+
createVoiceHandoffDeliveryWorker,
|
|
11904
|
+
createVoiceHandoffDeliveryRecord,
|
|
9428
11905
|
createVoiceFileTraceSinkDeliveryStore,
|
|
9429
11906
|
createVoiceFileTraceEventStore,
|
|
9430
11907
|
createVoiceFileTaskStore,
|
|
@@ -9433,6 +11910,7 @@ export {
|
|
|
9433
11910
|
createVoiceFileReviewStore,
|
|
9434
11911
|
createVoiceFileIntegrationEventStore,
|
|
9435
11912
|
createVoiceFileExternalObjectMapStore,
|
|
11913
|
+
createVoiceFileAssistantMemoryStore,
|
|
9436
11914
|
createVoiceExternalObjectMapId,
|
|
9437
11915
|
createVoiceExternalObjectMap,
|
|
9438
11916
|
createVoiceExperiment,
|
|
@@ -9441,6 +11919,11 @@ export {
|
|
|
9441
11919
|
createVoiceCallReviewFromLiveTelephonyReport,
|
|
9442
11920
|
createVoiceCallCompletedEvent,
|
|
9443
11921
|
createVoiceCRMActivitySink,
|
|
11922
|
+
createVoiceAssistantMemoryRecord,
|
|
11923
|
+
createVoiceAssistantMemoryHandle,
|
|
11924
|
+
createVoiceAssistantHealthRoutes,
|
|
11925
|
+
createVoiceAssistantHealthJSONHandler,
|
|
11926
|
+
createVoiceAssistantHealthHTMLHandler,
|
|
9444
11927
|
createVoiceAssistant,
|
|
9445
11928
|
createVoiceAgentTool,
|
|
9446
11929
|
createVoiceAgentSquad,
|
|
@@ -9453,9 +11936,13 @@ export {
|
|
|
9453
11936
|
createStoredVoiceCallReviewArtifact,
|
|
9454
11937
|
createRiskyTurnCorrectionHandler,
|
|
9455
11938
|
createPhraseHintCorrectionHandler,
|
|
11939
|
+
createOpenAIVoiceAssistantModel,
|
|
11940
|
+
createJSONVoiceAssistantModel,
|
|
9456
11941
|
createId,
|
|
11942
|
+
createGeminiVoiceAssistantModel,
|
|
9457
11943
|
createDomainPhraseHints,
|
|
9458
11944
|
createDomainLexicon,
|
|
11945
|
+
createAnthropicVoiceAssistantModel,
|
|
9459
11946
|
conditionAudioChunk,
|
|
9460
11947
|
completeVoiceOpsTask,
|
|
9461
11948
|
claimVoiceOpsTask,
|
|
@@ -9465,6 +11952,7 @@ export {
|
|
|
9465
11952
|
assignVoiceOpsTask,
|
|
9466
11953
|
applyVoiceOpsTaskPolicy,
|
|
9467
11954
|
applyVoiceOpsTaskAssignmentRule,
|
|
11955
|
+
applyVoiceHandoffDeliveryResult,
|
|
9468
11956
|
applyRiskTieredPhraseHintCorrections,
|
|
9469
11957
|
applyPhraseHintCorrections,
|
|
9470
11958
|
TURN_PROFILE_DEFAULTS
|