@absolutejs/voice 0.0.22-beta.3 → 0.0.22-beta.30
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 +40 -0
- package/dist/handoffHealth.d.ts +94 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.js +2379 -138
- package/dist/modelAdapters.d.ts +93 -0
- package/dist/opsWebhook.d.ts +126 -0
- package/dist/providerHealth.d.ts +78 -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 +1216 -7
- package/dist/testing/providerSimulator.d.ts +44 -0
- package/dist/trace.d.ts +1 -1
- package/dist/types.d.ts +54 -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,214 @@ 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 defaultWebhookBody = (input) => ({
|
|
3023
|
+
action: input.action,
|
|
3024
|
+
metadata: input.metadata,
|
|
3025
|
+
reason: input.reason,
|
|
3026
|
+
result: input.result,
|
|
3027
|
+
session: {
|
|
3028
|
+
id: input.session.id,
|
|
3029
|
+
scenarioId: input.session.scenarioId,
|
|
3030
|
+
status: input.session.status
|
|
3031
|
+
},
|
|
3032
|
+
source: "absolutejs-voice",
|
|
3033
|
+
target: input.target
|
|
3034
|
+
});
|
|
3035
|
+
var deliverVoiceHandoff = async (input) => {
|
|
3036
|
+
if (!input.config || input.config.adapters.length === 0) {
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
const deliveries = {};
|
|
3040
|
+
for (const adapter of input.config.adapters) {
|
|
3041
|
+
if (adapter.actions && !adapter.actions.includes(input.handoff.action)) {
|
|
3042
|
+
deliveries[adapter.id] = createSkippedDelivery(adapter);
|
|
3043
|
+
continue;
|
|
3044
|
+
}
|
|
3045
|
+
try {
|
|
3046
|
+
const result = await adapter.handoff(input.handoff);
|
|
3047
|
+
deliveries[adapter.id] = {
|
|
3048
|
+
...result,
|
|
3049
|
+
adapterId: adapter.id,
|
|
3050
|
+
adapterKind: adapter.kind
|
|
3051
|
+
};
|
|
3052
|
+
} catch (error) {
|
|
3053
|
+
deliveries[adapter.id] = {
|
|
3054
|
+
adapterId: adapter.id,
|
|
3055
|
+
adapterKind: adapter.kind,
|
|
3056
|
+
error: toErrorMessage2(error),
|
|
3057
|
+
status: "failed"
|
|
3058
|
+
};
|
|
3059
|
+
if (input.config.failMode === "throw") {
|
|
3060
|
+
throw error;
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
return {
|
|
3065
|
+
action: input.handoff.action,
|
|
3066
|
+
deliveries,
|
|
3067
|
+
status: aggregateHandoffStatus(deliveries)
|
|
3068
|
+
};
|
|
3069
|
+
};
|
|
3070
|
+
var createVoiceWebhookHandoffAdapter = (options) => ({
|
|
3071
|
+
actions: options.actions,
|
|
3072
|
+
handoff: async (input) => {
|
|
3073
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
3074
|
+
if (typeof fetchImpl !== "function") {
|
|
3075
|
+
return {
|
|
3076
|
+
deliveredTo: options.url,
|
|
3077
|
+
error: "Handoff delivery failed: fetch is not available in this runtime.",
|
|
3078
|
+
status: "failed"
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
const body = JSON.stringify(await options.body?.(input) ?? defaultWebhookBody(input));
|
|
3082
|
+
const headers = {
|
|
3083
|
+
"content-type": "application/json",
|
|
3084
|
+
...options.headers
|
|
3085
|
+
};
|
|
3086
|
+
if (options.signingSecret) {
|
|
3087
|
+
const timestamp = String(Date.now());
|
|
3088
|
+
headers["x-absolutejs-timestamp"] = timestamp;
|
|
3089
|
+
headers["x-absolutejs-signature"] = await signHandoffBody({
|
|
3090
|
+
body,
|
|
3091
|
+
secret: options.signingSecret,
|
|
3092
|
+
timestamp
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
|
|
3096
|
+
const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
3097
|
+
try {
|
|
3098
|
+
const response = await fetchImpl(options.url, {
|
|
3099
|
+
body,
|
|
3100
|
+
headers,
|
|
3101
|
+
method: options.method ?? "POST",
|
|
3102
|
+
signal: controller?.signal
|
|
3103
|
+
});
|
|
3104
|
+
if (!response.ok) {
|
|
3105
|
+
return {
|
|
3106
|
+
deliveredTo: options.url,
|
|
3107
|
+
error: `Handoff delivery failed with response ${response.status}.`,
|
|
3108
|
+
status: "failed"
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
return {
|
|
3112
|
+
deliveredAt: Date.now(),
|
|
3113
|
+
deliveredTo: options.url,
|
|
3114
|
+
status: "delivered"
|
|
3115
|
+
};
|
|
3116
|
+
} finally {
|
|
3117
|
+
if (timeout) {
|
|
3118
|
+
clearTimeout(timeout);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
},
|
|
3122
|
+
id: options.id,
|
|
3123
|
+
kind: options.kind ?? "webhook"
|
|
3124
|
+
});
|
|
3125
|
+
var escapeXml = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
3126
|
+
var defaultTwilioTransferTwiML = (input) => {
|
|
3127
|
+
if (!input.target) {
|
|
3128
|
+
return "<Response><Hangup /></Response>";
|
|
3129
|
+
}
|
|
3130
|
+
return `<Response><Dial>${escapeXml(input.target)}</Dial></Response>`;
|
|
3131
|
+
};
|
|
3132
|
+
var resolveTwilioCallSid = async (resolver, input) => {
|
|
3133
|
+
if (typeof resolver === "function") {
|
|
3134
|
+
return resolver(input);
|
|
3135
|
+
}
|
|
3136
|
+
if (typeof resolver === "string" && resolver.length > 0) {
|
|
3137
|
+
return resolver;
|
|
3138
|
+
}
|
|
3139
|
+
const metadataSid = typeof input.metadata?.callSid === "string" ? input.metadata.callSid : undefined;
|
|
3140
|
+
const sessionMetadata = input.session.metadata && typeof input.session.metadata === "object" ? input.session.metadata : undefined;
|
|
3141
|
+
const sessionSid = typeof sessionMetadata?.callSid === "string" ? sessionMetadata.callSid : undefined;
|
|
3142
|
+
return metadataSid ?? sessionSid;
|
|
3143
|
+
};
|
|
3144
|
+
var createVoiceTwilioRedirectHandoffAdapter = (options) => ({
|
|
3145
|
+
actions: options.actions ?? ["transfer"],
|
|
3146
|
+
handoff: async (input) => {
|
|
3147
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
3148
|
+
const callSid = await resolveTwilioCallSid(options.callSid, input);
|
|
3149
|
+
if (!callSid) {
|
|
3150
|
+
return {
|
|
3151
|
+
error: "Twilio handoff requires a callSid.",
|
|
3152
|
+
status: "failed"
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3155
|
+
if (typeof fetchImpl !== "function") {
|
|
3156
|
+
return {
|
|
3157
|
+
error: "Twilio handoff failed: fetch is not available in this runtime.",
|
|
3158
|
+
status: "failed"
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(options.accountSid)}/Calls/${encodeURIComponent(callSid)}.json`;
|
|
3162
|
+
const body = new URLSearchParams({
|
|
3163
|
+
Twiml: await (options.buildTwiML?.(input) ?? defaultTwilioTransferTwiML(input))
|
|
3164
|
+
});
|
|
3165
|
+
const auth = btoa(`${options.accountSid}:${options.authToken}`);
|
|
3166
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
|
|
3167
|
+
const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
3168
|
+
try {
|
|
3169
|
+
const response = await fetchImpl(url, {
|
|
3170
|
+
body,
|
|
3171
|
+
headers: {
|
|
3172
|
+
authorization: `Basic ${auth}`,
|
|
3173
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
3174
|
+
},
|
|
3175
|
+
method: "POST",
|
|
3176
|
+
signal: controller?.signal
|
|
3177
|
+
});
|
|
3178
|
+
if (!response.ok) {
|
|
3179
|
+
return {
|
|
3180
|
+
deliveredTo: url,
|
|
3181
|
+
error: `Twilio handoff failed with response ${response.status}.`,
|
|
3182
|
+
status: "failed"
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
return {
|
|
3186
|
+
deliveredAt: Date.now(),
|
|
3187
|
+
deliveredTo: url,
|
|
3188
|
+
metadata: {
|
|
3189
|
+
callSid
|
|
3190
|
+
},
|
|
3191
|
+
status: "delivered"
|
|
3192
|
+
};
|
|
3193
|
+
} finally {
|
|
3194
|
+
if (timeout) {
|
|
3195
|
+
clearTimeout(timeout);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
},
|
|
3199
|
+
id: options.id ?? "twilio-redirect",
|
|
3200
|
+
kind: "twilio-redirect"
|
|
3201
|
+
});
|
|
3202
|
+
|
|
2995
3203
|
// src/turnDetection.ts
|
|
2996
3204
|
var DEFAULT_SILENCE_MS = 700;
|
|
2997
3205
|
var DEFAULT_SPEECH_THRESHOLD = 0.015;
|
|
@@ -3288,6 +3496,7 @@ var pushCallLifecycleEvent = (session, input) => {
|
|
|
3288
3496
|
}
|
|
3289
3497
|
return lifecycle;
|
|
3290
3498
|
};
|
|
3499
|
+
var getLatestCallLifecycleEvent = (session) => session.call?.events.at(-1);
|
|
3291
3500
|
var createVoiceSession = (options) => {
|
|
3292
3501
|
const logger = resolveLogger(options.logger);
|
|
3293
3502
|
const reconnect = {
|
|
@@ -3388,6 +3597,45 @@ var createVoiceSession = (options) => {
|
|
|
3388
3597
|
});
|
|
3389
3598
|
}
|
|
3390
3599
|
};
|
|
3600
|
+
const sendCallLifecycle = async (session) => {
|
|
3601
|
+
const event = getLatestCallLifecycleEvent(session);
|
|
3602
|
+
if (!event) {
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3605
|
+
await send({
|
|
3606
|
+
event,
|
|
3607
|
+
sessionId: options.id,
|
|
3608
|
+
type: "call_lifecycle"
|
|
3609
|
+
});
|
|
3610
|
+
};
|
|
3611
|
+
const runHandoff = async (input) => {
|
|
3612
|
+
const result = await deliverVoiceHandoff({
|
|
3613
|
+
config: options.handoff,
|
|
3614
|
+
handoff: {
|
|
3615
|
+
action: input.action,
|
|
3616
|
+
api,
|
|
3617
|
+
context: options.context,
|
|
3618
|
+
metadata: input.metadata,
|
|
3619
|
+
reason: input.reason,
|
|
3620
|
+
result: input.result,
|
|
3621
|
+
session: input.session,
|
|
3622
|
+
target: input.target
|
|
3623
|
+
}
|
|
3624
|
+
});
|
|
3625
|
+
if (!result) {
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
await appendTrace({
|
|
3629
|
+
metadata: input.metadata,
|
|
3630
|
+
payload: {
|
|
3631
|
+
...result,
|
|
3632
|
+
reason: input.reason,
|
|
3633
|
+
target: input.target
|
|
3634
|
+
},
|
|
3635
|
+
session: input.session,
|
|
3636
|
+
type: "call.handoff"
|
|
3637
|
+
});
|
|
3638
|
+
};
|
|
3391
3639
|
const readSession = async () => options.store.getOrCreate(options.id);
|
|
3392
3640
|
const writeSession = async (mutate) => {
|
|
3393
3641
|
const session = await options.store.getOrCreate(options.id);
|
|
@@ -3578,6 +3826,7 @@ var createVoiceSession = (options) => {
|
|
|
3578
3826
|
await appendTrace({
|
|
3579
3827
|
payload: {
|
|
3580
3828
|
disposition,
|
|
3829
|
+
metadata: input.metadata,
|
|
3581
3830
|
reason: input.reason,
|
|
3582
3831
|
target: input.target,
|
|
3583
3832
|
type: "end"
|
|
@@ -3585,6 +3834,7 @@ var createVoiceSession = (options) => {
|
|
|
3585
3834
|
session,
|
|
3586
3835
|
type: "call.lifecycle"
|
|
3587
3836
|
});
|
|
3837
|
+
await sendCallLifecycle(session);
|
|
3588
3838
|
await send({
|
|
3589
3839
|
sessionId: options.id,
|
|
3590
3840
|
type: "complete"
|
|
@@ -3664,6 +3914,15 @@ var createVoiceSession = (options) => {
|
|
|
3664
3914
|
session,
|
|
3665
3915
|
type: "call.lifecycle"
|
|
3666
3916
|
});
|
|
3917
|
+
await sendCallLifecycle(session);
|
|
3918
|
+
await runHandoff({
|
|
3919
|
+
action: "transfer",
|
|
3920
|
+
metadata: input.metadata,
|
|
3921
|
+
reason: input.reason,
|
|
3922
|
+
result: input.result,
|
|
3923
|
+
session,
|
|
3924
|
+
target: input.target
|
|
3925
|
+
});
|
|
3667
3926
|
await completeInternal(input.result, {
|
|
3668
3927
|
disposition: "transferred",
|
|
3669
3928
|
invokeOnComplete: false,
|
|
@@ -3689,6 +3948,14 @@ var createVoiceSession = (options) => {
|
|
|
3689
3948
|
session,
|
|
3690
3949
|
type: "call.lifecycle"
|
|
3691
3950
|
});
|
|
3951
|
+
await sendCallLifecycle(session);
|
|
3952
|
+
await runHandoff({
|
|
3953
|
+
action: "escalate",
|
|
3954
|
+
metadata: input.metadata,
|
|
3955
|
+
reason: input.reason,
|
|
3956
|
+
result: input.result,
|
|
3957
|
+
session
|
|
3958
|
+
});
|
|
3692
3959
|
await completeInternal(input.result, {
|
|
3693
3960
|
disposition: "escalated",
|
|
3694
3961
|
invokeOnComplete: false,
|
|
@@ -3711,6 +3978,13 @@ var createVoiceSession = (options) => {
|
|
|
3711
3978
|
session,
|
|
3712
3979
|
type: "call.lifecycle"
|
|
3713
3980
|
});
|
|
3981
|
+
await sendCallLifecycle(session);
|
|
3982
|
+
await runHandoff({
|
|
3983
|
+
action: "no-answer",
|
|
3984
|
+
metadata: input?.metadata,
|
|
3985
|
+
result: input?.result,
|
|
3986
|
+
session
|
|
3987
|
+
});
|
|
3714
3988
|
await completeInternal(input?.result, {
|
|
3715
3989
|
disposition: "no-answer",
|
|
3716
3990
|
invokeOnComplete: false,
|
|
@@ -3732,6 +4006,13 @@ var createVoiceSession = (options) => {
|
|
|
3732
4006
|
session,
|
|
3733
4007
|
type: "call.lifecycle"
|
|
3734
4008
|
});
|
|
4009
|
+
await sendCallLifecycle(session);
|
|
4010
|
+
await runHandoff({
|
|
4011
|
+
action: "voicemail",
|
|
4012
|
+
metadata: input?.metadata,
|
|
4013
|
+
result: input?.result,
|
|
4014
|
+
session
|
|
4015
|
+
});
|
|
3735
4016
|
await completeInternal(input?.result, {
|
|
3736
4017
|
disposition: "voicemail",
|
|
3737
4018
|
invokeOnComplete: false,
|
|
@@ -4518,6 +4799,7 @@ var createVoiceSession = (options) => {
|
|
|
4518
4799
|
session,
|
|
4519
4800
|
type: "call.lifecycle"
|
|
4520
4801
|
});
|
|
4802
|
+
await sendCallLifecycle(session);
|
|
4521
4803
|
}
|
|
4522
4804
|
await send({
|
|
4523
4805
|
sessionId: options.id,
|
|
@@ -4755,6 +5037,14 @@ var isVoiceClientMessage = (value) => {
|
|
|
4755
5037
|
return false;
|
|
4756
5038
|
}
|
|
4757
5039
|
switch (value.type) {
|
|
5040
|
+
case "call_control":
|
|
5041
|
+
if (!("action" in value)) {
|
|
5042
|
+
return false;
|
|
5043
|
+
}
|
|
5044
|
+
if (value.action !== "complete" && value.action !== "escalate" && value.action !== "no-answer" && value.action !== "transfer" && value.action !== "voicemail") {
|
|
5045
|
+
return false;
|
|
5046
|
+
}
|
|
5047
|
+
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
5048
|
case "close":
|
|
4759
5049
|
return true;
|
|
4760
5050
|
case "end_turn":
|
|
@@ -4895,6 +5185,7 @@ var voice = (config) => {
|
|
|
4895
5185
|
audioConditioning: sessionOptions.audioConditioning,
|
|
4896
5186
|
context,
|
|
4897
5187
|
id: sessionId,
|
|
5188
|
+
handoff: config.handoff,
|
|
4898
5189
|
languageStrategy: config.languageStrategy,
|
|
4899
5190
|
lexicon,
|
|
4900
5191
|
logger: sessionOptions.logger,
|
|
@@ -5006,6 +5297,42 @@ var voice = (config) => {
|
|
|
5006
5297
|
await current.close(message.reason);
|
|
5007
5298
|
runtime.activeSessions.delete(sessionState.sessionId);
|
|
5008
5299
|
}
|
|
5300
|
+
if (message.type === "call_control" && current) {
|
|
5301
|
+
if (message.action === "transfer") {
|
|
5302
|
+
if (message.target) {
|
|
5303
|
+
await current.transfer({
|
|
5304
|
+
metadata: message.metadata,
|
|
5305
|
+
reason: message.reason,
|
|
5306
|
+
target: message.target
|
|
5307
|
+
});
|
|
5308
|
+
} else {
|
|
5309
|
+
ws.send(JSON.stringify({
|
|
5310
|
+
message: "call_control transfer requires target",
|
|
5311
|
+
recoverable: true,
|
|
5312
|
+
type: "error"
|
|
5313
|
+
}));
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
if (message.action === "escalate") {
|
|
5317
|
+
await current.escalate({
|
|
5318
|
+
metadata: message.metadata,
|
|
5319
|
+
reason: message.reason ?? "client-requested-escalation"
|
|
5320
|
+
});
|
|
5321
|
+
}
|
|
5322
|
+
if (message.action === "voicemail") {
|
|
5323
|
+
await current.markVoicemail({
|
|
5324
|
+
metadata: message.metadata
|
|
5325
|
+
});
|
|
5326
|
+
}
|
|
5327
|
+
if (message.action === "no-answer") {
|
|
5328
|
+
await current.markNoAnswer({
|
|
5329
|
+
metadata: message.metadata
|
|
5330
|
+
});
|
|
5331
|
+
}
|
|
5332
|
+
if (message.action === "complete") {
|
|
5333
|
+
await current.complete();
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5009
5336
|
if (message.type === "start" && message.sessionId && message.sessionId !== sessionState.sessionId) {
|
|
5010
5337
|
const currentSession = runtime.activeSessions.get(sessionState.sessionId);
|
|
5011
5338
|
if (currentSession) {
|
|
@@ -5054,7 +5381,7 @@ var voice = (config) => {
|
|
|
5054
5381
|
};
|
|
5055
5382
|
// src/agent.ts
|
|
5056
5383
|
var normalizeText3 = (value) => typeof value === "string" ? value.trim() : "";
|
|
5057
|
-
var
|
|
5384
|
+
var toErrorMessage3 = (error) => error instanceof Error ? error.message : String(error);
|
|
5058
5385
|
var createHistoryMessages = (session, turn) => {
|
|
5059
5386
|
const messages = [];
|
|
5060
5387
|
for (const previousTurn of session.turns) {
|
|
@@ -5150,6 +5477,17 @@ var createVoiceAgent = (options) => {
|
|
|
5150
5477
|
if (output.assistantText?.trim()) {
|
|
5151
5478
|
messages.push({
|
|
5152
5479
|
content: output.assistantText,
|
|
5480
|
+
metadata: output.toolCalls?.length ? {
|
|
5481
|
+
toolCalls: output.toolCalls
|
|
5482
|
+
} : undefined,
|
|
5483
|
+
role: "assistant"
|
|
5484
|
+
});
|
|
5485
|
+
} else if (output.toolCalls?.length) {
|
|
5486
|
+
messages.push({
|
|
5487
|
+
content: "",
|
|
5488
|
+
metadata: {
|
|
5489
|
+
toolCalls: output.toolCalls
|
|
5490
|
+
},
|
|
5153
5491
|
role: "assistant"
|
|
5154
5492
|
});
|
|
5155
5493
|
}
|
|
@@ -5224,7 +5562,7 @@ var createVoiceAgent = (options) => {
|
|
|
5224
5562
|
toolCallId: toolCall.id
|
|
5225
5563
|
});
|
|
5226
5564
|
} catch (error) {
|
|
5227
|
-
const errorMessage =
|
|
5565
|
+
const errorMessage = toErrorMessage3(error);
|
|
5228
5566
|
toolResults.push({
|
|
5229
5567
|
error: errorMessage,
|
|
5230
5568
|
status: "error",
|
|
@@ -5594,6 +5932,112 @@ var resolveVoiceOutcomeRecipe = (name, options = {}) => {
|
|
|
5594
5932
|
};
|
|
5595
5933
|
};
|
|
5596
5934
|
|
|
5935
|
+
// src/assistantMemory.ts
|
|
5936
|
+
var createMemoryId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
|
|
5937
|
+
var createVoiceAssistantMemoryRecord = (input) => {
|
|
5938
|
+
const now = Date.now();
|
|
5939
|
+
return {
|
|
5940
|
+
...input,
|
|
5941
|
+
createdAt: input.createdAt ?? input.updatedAt ?? now,
|
|
5942
|
+
updatedAt: input.updatedAt ?? now
|
|
5943
|
+
};
|
|
5944
|
+
};
|
|
5945
|
+
var createVoiceMemoryAssistantMemoryStore = () => {
|
|
5946
|
+
const records = new Map;
|
|
5947
|
+
return {
|
|
5948
|
+
delete: async (input) => {
|
|
5949
|
+
records.delete(createMemoryId(input));
|
|
5950
|
+
},
|
|
5951
|
+
get: async (input) => records.get(createMemoryId(input)),
|
|
5952
|
+
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),
|
|
5953
|
+
set: async (input) => {
|
|
5954
|
+
const id = createMemoryId(input);
|
|
5955
|
+
const existing = records.get(id);
|
|
5956
|
+
const record = createVoiceAssistantMemoryRecord({
|
|
5957
|
+
...input,
|
|
5958
|
+
createdAt: input.createdAt ?? existing?.createdAt,
|
|
5959
|
+
updatedAt: input.updatedAt
|
|
5960
|
+
});
|
|
5961
|
+
records.set(id, record);
|
|
5962
|
+
return record;
|
|
5963
|
+
}
|
|
5964
|
+
};
|
|
5965
|
+
};
|
|
5966
|
+
var resolveVoiceAssistantMemoryNamespace = async (input) => typeof input.memory.namespace === "function" ? await input.memory.namespace(input) : input.memory.namespace;
|
|
5967
|
+
var createVoiceAssistantMemoryHandle = async (input) => {
|
|
5968
|
+
const namespace = await resolveVoiceAssistantMemoryNamespace({
|
|
5969
|
+
assistantId: input.assistantId,
|
|
5970
|
+
context: input.context,
|
|
5971
|
+
memory: input.memory,
|
|
5972
|
+
session: input.session
|
|
5973
|
+
});
|
|
5974
|
+
const trace = async (event) => {
|
|
5975
|
+
await input.trace?.append({
|
|
5976
|
+
at: Date.now(),
|
|
5977
|
+
payload: {
|
|
5978
|
+
assistantId: input.assistantId,
|
|
5979
|
+
namespace,
|
|
5980
|
+
...event
|
|
5981
|
+
},
|
|
5982
|
+
scenarioId: input.session.scenarioId,
|
|
5983
|
+
sessionId: input.session.id,
|
|
5984
|
+
type: "assistant.memory"
|
|
5985
|
+
});
|
|
5986
|
+
};
|
|
5987
|
+
return {
|
|
5988
|
+
delete: async (key) => {
|
|
5989
|
+
await input.memory.store.delete({
|
|
5990
|
+
assistantId: input.assistantId,
|
|
5991
|
+
key,
|
|
5992
|
+
namespace
|
|
5993
|
+
});
|
|
5994
|
+
await trace({
|
|
5995
|
+
action: "delete",
|
|
5996
|
+
key
|
|
5997
|
+
});
|
|
5998
|
+
},
|
|
5999
|
+
get: async (key) => {
|
|
6000
|
+
const record = await input.memory.store.get({
|
|
6001
|
+
assistantId: input.assistantId,
|
|
6002
|
+
key,
|
|
6003
|
+
namespace
|
|
6004
|
+
});
|
|
6005
|
+
await trace({
|
|
6006
|
+
action: "get",
|
|
6007
|
+
found: Boolean(record),
|
|
6008
|
+
key
|
|
6009
|
+
});
|
|
6010
|
+
return record?.value;
|
|
6011
|
+
},
|
|
6012
|
+
list: async () => {
|
|
6013
|
+
const records = await input.memory.store.list({
|
|
6014
|
+
assistantId: input.assistantId,
|
|
6015
|
+
namespace
|
|
6016
|
+
});
|
|
6017
|
+
await trace({
|
|
6018
|
+
action: "list",
|
|
6019
|
+
count: records.length
|
|
6020
|
+
});
|
|
6021
|
+
return records;
|
|
6022
|
+
},
|
|
6023
|
+
namespace,
|
|
6024
|
+
set: async (key, value, metadata) => {
|
|
6025
|
+
const record = await input.memory.store.set({
|
|
6026
|
+
assistantId: input.assistantId,
|
|
6027
|
+
key,
|
|
6028
|
+
metadata,
|
|
6029
|
+
namespace,
|
|
6030
|
+
value
|
|
6031
|
+
});
|
|
6032
|
+
await trace({
|
|
6033
|
+
action: "set",
|
|
6034
|
+
key
|
|
6035
|
+
});
|
|
6036
|
+
return record;
|
|
6037
|
+
}
|
|
6038
|
+
};
|
|
6039
|
+
};
|
|
6040
|
+
|
|
5597
6041
|
// src/assistant.ts
|
|
5598
6042
|
var hashString = (value) => {
|
|
5599
6043
|
let hash = 2166136261;
|
|
@@ -5742,12 +6186,35 @@ var createVoiceAssistant = (options) => {
|
|
|
5742
6186
|
});
|
|
5743
6187
|
}
|
|
5744
6188
|
const onTurn = async (input) => {
|
|
6189
|
+
const memory = options.memory ? await createVoiceAssistantMemoryHandle({
|
|
6190
|
+
assistantId: options.id,
|
|
6191
|
+
context: input.context,
|
|
6192
|
+
memory: options.memory,
|
|
6193
|
+
session: input.session,
|
|
6194
|
+
trace: options.trace
|
|
6195
|
+
}) : undefined;
|
|
5745
6196
|
const guardrailInput = {
|
|
5746
6197
|
...input,
|
|
5747
|
-
assistantId: options.id
|
|
6198
|
+
assistantId: options.id,
|
|
6199
|
+
memory
|
|
5748
6200
|
};
|
|
6201
|
+
if (memory) {
|
|
6202
|
+
await options.memoryLifecycle?.beforeTurn?.({
|
|
6203
|
+
...input,
|
|
6204
|
+
assistantId: options.id,
|
|
6205
|
+
memory
|
|
6206
|
+
});
|
|
6207
|
+
}
|
|
5749
6208
|
const blocked = await options.guardrails?.beforeTurn?.(guardrailInput);
|
|
5750
6209
|
if (blocked) {
|
|
6210
|
+
if (memory) {
|
|
6211
|
+
await options.memoryLifecycle?.afterTurn?.({
|
|
6212
|
+
...input,
|
|
6213
|
+
assistantId: options.id,
|
|
6214
|
+
memory,
|
|
6215
|
+
result: blocked
|
|
6216
|
+
});
|
|
6217
|
+
}
|
|
5751
6218
|
await appendAssistantTrace({
|
|
5752
6219
|
assistantId: options.id,
|
|
5753
6220
|
event: {
|
|
@@ -5797,6 +6264,14 @@ var createVoiceAssistant = (options) => {
|
|
|
5797
6264
|
result
|
|
5798
6265
|
});
|
|
5799
6266
|
const finalResult = guarded ?? result;
|
|
6267
|
+
if (memory) {
|
|
6268
|
+
await options.memoryLifecycle?.afterTurn?.({
|
|
6269
|
+
...input,
|
|
6270
|
+
assistantId: options.id,
|
|
6271
|
+
memory,
|
|
6272
|
+
result: finalResult
|
|
6273
|
+
});
|
|
6274
|
+
}
|
|
5800
6275
|
if (guarded) {
|
|
5801
6276
|
await appendAssistantTrace({
|
|
5802
6277
|
assistantId: options.id,
|
|
@@ -5864,6 +6339,12 @@ var summarizeVoiceAssistantRuns = async (input) => {
|
|
|
5864
6339
|
escalationCount: 0,
|
|
5865
6340
|
experiments: {},
|
|
5866
6341
|
guardrailCount: 0,
|
|
6342
|
+
memory: {
|
|
6343
|
+
deletes: 0,
|
|
6344
|
+
gets: 0,
|
|
6345
|
+
lists: 0,
|
|
6346
|
+
sets: 0
|
|
6347
|
+
},
|
|
5867
6348
|
outcomes: {},
|
|
5868
6349
|
runCount: 0,
|
|
5869
6350
|
sessionIds: new Set,
|
|
@@ -5919,6 +6400,24 @@ var summarizeVoiceAssistantRuns = async (input) => {
|
|
|
5919
6400
|
const summary = getSummary(assistantId);
|
|
5920
6401
|
summary.guardrailCount += 1;
|
|
5921
6402
|
}
|
|
6403
|
+
for (const event of events.filter((event2) => event2.type === "assistant.memory")) {
|
|
6404
|
+
const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
|
|
6405
|
+
const summary = getSummary(assistantId);
|
|
6406
|
+
switch (event.payload.action) {
|
|
6407
|
+
case "delete":
|
|
6408
|
+
summary.memory.deletes += 1;
|
|
6409
|
+
break;
|
|
6410
|
+
case "get":
|
|
6411
|
+
summary.memory.gets += 1;
|
|
6412
|
+
break;
|
|
6413
|
+
case "list":
|
|
6414
|
+
summary.memory.lists += 1;
|
|
6415
|
+
break;
|
|
6416
|
+
case "set":
|
|
6417
|
+
summary.memory.sets += 1;
|
|
6418
|
+
break;
|
|
6419
|
+
}
|
|
6420
|
+
}
|
|
5922
6421
|
const assistants = [...byAssistant.values()].map(({ elapsedCount, elapsedTotal, sessionIds, ...summary }) => ({
|
|
5923
6422
|
...summary,
|
|
5924
6423
|
averageElapsedMs: elapsedCount > 0 ? Math.round(elapsedTotal / elapsedCount) : undefined,
|
|
@@ -5929,38 +6428,331 @@ var summarizeVoiceAssistantRuns = async (input) => {
|
|
|
5929
6428
|
totalRuns: assistantRuns.length
|
|
5930
6429
|
};
|
|
5931
6430
|
};
|
|
5932
|
-
// src/
|
|
5933
|
-
import {
|
|
5934
|
-
import { join } from "path";
|
|
6431
|
+
// src/assistantHealth.ts
|
|
6432
|
+
import { Elysia as Elysia3 } from "elysia";
|
|
5935
6433
|
|
|
5936
|
-
// src/
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
}
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
6434
|
+
// src/providerHealth.ts
|
|
6435
|
+
import { Elysia as Elysia2 } from "elysia";
|
|
6436
|
+
var getString = (value) => typeof value === "string" ? value : undefined;
|
|
6437
|
+
var getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
6438
|
+
var isProviderStatus = (value) => value === "success" || value === "fallback" || value === "error";
|
|
6439
|
+
var summarizeVoiceProviderHealth = async (input) => {
|
|
6440
|
+
const options = Array.isArray(input) ? { events: input } : input;
|
|
6441
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
6442
|
+
const providers = options.providers ?? [];
|
|
6443
|
+
const providerSet = new Set(providers);
|
|
6444
|
+
const now = options.now ?? Date.now();
|
|
6445
|
+
const entries = new Map;
|
|
6446
|
+
const isAllowedProvider = (value) => typeof value === "string" && (providerSet.size === 0 || providerSet.has(value));
|
|
6447
|
+
const getEntry = (provider) => {
|
|
6448
|
+
const existing = entries.get(provider);
|
|
6449
|
+
if (existing) {
|
|
6450
|
+
return existing;
|
|
6451
|
+
}
|
|
6452
|
+
const entry = {
|
|
6453
|
+
elapsedCount: 0,
|
|
6454
|
+
elapsedTotal: 0,
|
|
6455
|
+
errorCount: 0,
|
|
6456
|
+
fallbackCount: 0,
|
|
6457
|
+
provider,
|
|
6458
|
+
rateLimited: false,
|
|
6459
|
+
recommended: false,
|
|
6460
|
+
runCount: 0,
|
|
6461
|
+
status: "idle"
|
|
6462
|
+
};
|
|
6463
|
+
entries.set(provider, entry);
|
|
6464
|
+
return entry;
|
|
6465
|
+
};
|
|
6466
|
+
for (const provider of providers) {
|
|
6467
|
+
getEntry(provider);
|
|
6468
|
+
}
|
|
6469
|
+
const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
|
|
6470
|
+
for (const event of events) {
|
|
6471
|
+
if (event.type === "assistant.run") {
|
|
6472
|
+
if (hasProviderRouterEvents) {
|
|
6473
|
+
continue;
|
|
6474
|
+
}
|
|
6475
|
+
const provider2 = event.payload.variantId;
|
|
6476
|
+
if (!isAllowedProvider(provider2)) {
|
|
6477
|
+
continue;
|
|
6478
|
+
}
|
|
6479
|
+
const entry2 = getEntry(provider2);
|
|
6480
|
+
entry2.runCount += 1;
|
|
6481
|
+
const elapsedMs = getNumber(event.payload.elapsedMs);
|
|
6482
|
+
if (elapsedMs !== undefined) {
|
|
6483
|
+
entry2.elapsedCount += 1;
|
|
6484
|
+
entry2.elapsedTotal += elapsedMs;
|
|
6485
|
+
}
|
|
6486
|
+
continue;
|
|
6487
|
+
}
|
|
6488
|
+
if (event.type !== "session.error") {
|
|
6489
|
+
continue;
|
|
6490
|
+
}
|
|
6491
|
+
const provider = event.payload.provider;
|
|
6492
|
+
if (!isAllowedProvider(provider)) {
|
|
6493
|
+
continue;
|
|
6494
|
+
}
|
|
6495
|
+
const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
6496
|
+
const applyProviderHealth = () => {
|
|
6497
|
+
const entry2 = getEntry(provider);
|
|
6498
|
+
const providerHealth = event.payload.providerHealth;
|
|
6499
|
+
if (providerHealth && typeof providerHealth === "object") {
|
|
6500
|
+
const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
|
|
6501
|
+
if (suppressedUntil2 !== undefined) {
|
|
6502
|
+
entry2.suppressedUntil = suppressedUntil2;
|
|
6503
|
+
}
|
|
6504
|
+
}
|
|
6505
|
+
const suppressedUntil = getNumber(event.payload.suppressedUntil);
|
|
6506
|
+
if (suppressedUntil !== undefined) {
|
|
6507
|
+
entry2.suppressedUntil = suppressedUntil;
|
|
6508
|
+
}
|
|
6509
|
+
const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
|
|
6510
|
+
if (suppressionRemainingMs !== undefined) {
|
|
6511
|
+
entry2.suppressionRemainingMs = suppressionRemainingMs;
|
|
6512
|
+
}
|
|
6513
|
+
return entry2;
|
|
6514
|
+
};
|
|
6515
|
+
if (providerStatus === "success" || providerStatus === "fallback") {
|
|
6516
|
+
const entry2 = applyProviderHealth();
|
|
6517
|
+
entry2.runCount += 1;
|
|
6518
|
+
entry2.lastSuccessAt = event.at;
|
|
6519
|
+
if (providerStatus === "success") {
|
|
6520
|
+
entry2.lastError = undefined;
|
|
6521
|
+
entry2.rateLimited = false;
|
|
6522
|
+
entry2.suppressedUntil = undefined;
|
|
6523
|
+
entry2.suppressionRemainingMs = undefined;
|
|
6524
|
+
}
|
|
6525
|
+
const elapsedMs = getNumber(event.payload.elapsedMs);
|
|
6526
|
+
if (elapsedMs !== undefined) {
|
|
6527
|
+
entry2.elapsedCount += 1;
|
|
6528
|
+
entry2.elapsedTotal += elapsedMs;
|
|
6529
|
+
}
|
|
6530
|
+
const selectedProvider = event.payload.selectedProvider;
|
|
6531
|
+
if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
|
|
6532
|
+
getEntry(selectedProvider).fallbackCount += 1;
|
|
6533
|
+
}
|
|
6534
|
+
continue;
|
|
6535
|
+
}
|
|
6536
|
+
const entry = applyProviderHealth();
|
|
6537
|
+
entry.errorCount += 1;
|
|
6538
|
+
entry.lastError = getString(event.payload.error);
|
|
6539
|
+
entry.lastErrorAt = event.at;
|
|
6540
|
+
entry.rateLimited ||= event.payload.rateLimited === true;
|
|
6541
|
+
}
|
|
6542
|
+
const summaries = [...entries.values()].map((entry) => {
|
|
6543
|
+
const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
|
|
6544
|
+
const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
|
|
6545
|
+
const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
|
|
6546
|
+
const recoverable = hadSuppression && !activeSuppression;
|
|
6547
|
+
const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
|
|
6548
|
+
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";
|
|
6549
|
+
return {
|
|
6550
|
+
averageElapsedMs,
|
|
6551
|
+
errorCount: entry.errorCount,
|
|
6552
|
+
fallbackCount: entry.fallbackCount,
|
|
6553
|
+
lastError: entry.lastError,
|
|
6554
|
+
lastErrorAt: entry.lastErrorAt,
|
|
6555
|
+
lastSuccessAt: entry.lastSuccessAt,
|
|
6556
|
+
provider: entry.provider,
|
|
6557
|
+
rateLimited: entry.rateLimited,
|
|
6558
|
+
recommended: false,
|
|
6559
|
+
runCount: entry.runCount,
|
|
6560
|
+
status,
|
|
6561
|
+
suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
|
|
6562
|
+
suppressedUntil: entry.suppressedUntil
|
|
6563
|
+
};
|
|
6564
|
+
});
|
|
6565
|
+
const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
|
|
6566
|
+
if (recommended) {
|
|
6567
|
+
recommended.recommended = true;
|
|
6568
|
+
}
|
|
6569
|
+
return summaries;
|
|
6570
|
+
};
|
|
6571
|
+
var escapeHtml3 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6572
|
+
var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
|
|
6573
|
+
'<div class="voice-provider-health">',
|
|
6574
|
+
...providers.map((provider) => {
|
|
6575
|
+
const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
|
|
6576
|
+
return [
|
|
6577
|
+
`<article class="voice-provider-card ${escapeHtml3(provider.status)}">`,
|
|
6578
|
+
'<div class="voice-provider-card-header">',
|
|
6579
|
+
`<strong>${escapeHtml3(provider.provider)}</strong>`,
|
|
6580
|
+
`<span>${escapeHtml3(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
|
|
6581
|
+
"</div>",
|
|
6582
|
+
"<dl>",
|
|
6583
|
+
`<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
|
|
6584
|
+
`<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
|
|
6585
|
+
`<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
|
|
6586
|
+
`<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
|
|
6587
|
+
"</dl>",
|
|
6588
|
+
suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
|
|
6589
|
+
provider.lastError ? `<p>${escapeHtml3(provider.lastError)}</p>` : "",
|
|
6590
|
+
"</article>"
|
|
6591
|
+
].join("");
|
|
6592
|
+
}),
|
|
6593
|
+
"</div>"
|
|
6594
|
+
].join("");
|
|
6595
|
+
var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
|
|
6596
|
+
var createVoiceProviderHealthHTMLHandler = (options) => async () => {
|
|
6597
|
+
const providers = await summarizeVoiceProviderHealth(options);
|
|
6598
|
+
const render = options.render ?? renderVoiceProviderHealthHTML;
|
|
6599
|
+
const body = await render(providers);
|
|
6600
|
+
return new Response(body, {
|
|
6601
|
+
headers: {
|
|
6602
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6603
|
+
...options.headers
|
|
6604
|
+
}
|
|
6605
|
+
});
|
|
6606
|
+
};
|
|
6607
|
+
var createVoiceProviderHealthRoutes = (options) => {
|
|
6608
|
+
const path = options.path ?? "/api/provider-status";
|
|
6609
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
6610
|
+
const routes = new Elysia2({
|
|
6611
|
+
name: options.name ?? "absolutejs-voice-provider-health"
|
|
6612
|
+
}).get(path, createVoiceProviderHealthJSONHandler(options));
|
|
6613
|
+
if (htmlPath) {
|
|
6614
|
+
routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
|
|
6615
|
+
}
|
|
6616
|
+
return routes;
|
|
6617
|
+
};
|
|
6618
|
+
|
|
6619
|
+
// src/assistantHealth.ts
|
|
6620
|
+
var escapeHtml4 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6621
|
+
var renderCountMap = (values) => {
|
|
6622
|
+
const entries = Object.entries(values).sort((left, right) => right[1] - left[1]);
|
|
6623
|
+
if (entries.length === 0) {
|
|
6624
|
+
return '<p class="voice-assistant-health-empty">No data yet.</p>';
|
|
6625
|
+
}
|
|
6626
|
+
return [
|
|
6627
|
+
'<div class="voice-assistant-health-metrics">',
|
|
6628
|
+
...entries.map(([label, value]) => `<div><span>${escapeHtml4(label)}</span><strong>${String(value)}</strong></div>`),
|
|
6629
|
+
"</div>"
|
|
6630
|
+
].join("");
|
|
6631
|
+
};
|
|
6632
|
+
var getString2 = (value) => typeof value === "string" ? value : undefined;
|
|
6633
|
+
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) => {
|
|
6634
|
+
const failure = {
|
|
6635
|
+
at: event.at,
|
|
6636
|
+
assistantId: getString2(event.payload.assistantId),
|
|
6637
|
+
error: getString2(event.payload.error),
|
|
6638
|
+
provider: getString2(event.payload.provider),
|
|
6639
|
+
rateLimited: event.payload.rateLimited === true ? true : undefined,
|
|
6640
|
+
sessionId: event.sessionId,
|
|
6641
|
+
status: getString2(event.payload.providerStatus),
|
|
6642
|
+
turnId: event.turnId,
|
|
6643
|
+
type: event.type
|
|
6644
|
+
};
|
|
6645
|
+
const href = replayHref === false ? undefined : typeof replayHref === "function" ? replayHref(failure) : `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
|
|
6646
|
+
return {
|
|
6647
|
+
...failure,
|
|
6648
|
+
replayHref: href
|
|
6649
|
+
};
|
|
6650
|
+
});
|
|
6651
|
+
var summarizeVoiceAssistantHealth = async (options) => {
|
|
6652
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
6653
|
+
return {
|
|
6654
|
+
assistantRuns: await summarizeVoiceAssistantRuns({ events }),
|
|
6655
|
+
providerHealth: await summarizeVoiceProviderHealth({
|
|
6656
|
+
events,
|
|
6657
|
+
providers: options.providers
|
|
6658
|
+
}),
|
|
6659
|
+
recentFailures: getRecentFailures(events, options.maxFailures ?? 8, options.replayHref)
|
|
6660
|
+
};
|
|
6661
|
+
};
|
|
6662
|
+
var renderVoiceAssistantHealthHTML = (summary) => {
|
|
6663
|
+
const assistant = summary.assistantRuns.assistants[0];
|
|
6664
|
+
const failures = summary.recentFailures;
|
|
6665
|
+
return [
|
|
6666
|
+
'<div class="voice-assistant-health">',
|
|
6667
|
+
'<section class="voice-assistant-health-grid">',
|
|
6668
|
+
`<article><span>Runs</span><strong>${String(assistant?.runCount ?? 0)}</strong></article>`,
|
|
6669
|
+
`<article><span>Sessions</span><strong>${String(assistant?.sessions ?? 0)}</strong></article>`,
|
|
6670
|
+
`<article><span>Guardrails</span><strong>${String(assistant?.guardrailCount ?? 0)}</strong></article>`,
|
|
6671
|
+
`<article><span>Avg latency</span><strong>${String(assistant?.averageElapsedMs ?? 0)}ms</strong></article>`,
|
|
6672
|
+
"</section>",
|
|
6673
|
+
"<section>",
|
|
6674
|
+
"<h3>Provider Health</h3>",
|
|
6675
|
+
renderVoiceProviderHealthHTML(summary.providerHealth),
|
|
6676
|
+
"</section>",
|
|
6677
|
+
'<section class="voice-assistant-health-columns">',
|
|
6678
|
+
`<article><h3>Outcomes</h3>${renderCountMap(assistant?.outcomes ?? {})}</article>`,
|
|
6679
|
+
`<article><h3>Variants</h3>${renderCountMap(assistant?.variants ?? {})}</article>`,
|
|
6680
|
+
`<article><h3>Tools</h3>${renderCountMap(assistant?.toolCalls ?? {})}</article>`,
|
|
6681
|
+
`<article><h3>Artifact Plans</h3>${renderCountMap(assistant?.artifactPlans ?? {})}</article>`,
|
|
6682
|
+
"</section>",
|
|
6683
|
+
"<section>",
|
|
6684
|
+
"<h3>Recent Failures</h3>",
|
|
6685
|
+
failures.length === 0 ? '<p class="voice-assistant-health-empty">No failures yet.</p>' : [
|
|
6686
|
+
'<div class="voice-assistant-health-failures">',
|
|
6687
|
+
...failures.map((failure) => [
|
|
6688
|
+
"<article>",
|
|
6689
|
+
`<strong>${escapeHtml4(failure.provider ?? failure.assistantId ?? failure.type)}</strong>`,
|
|
6690
|
+
`<span>${escapeHtml4(failure.status ?? (failure.rateLimited ? "rate-limited" : "error"))}</span>`,
|
|
6691
|
+
failure.error ? `<p>${escapeHtml4(failure.error)}</p>` : "",
|
|
6692
|
+
`<small>${escapeHtml4(failure.sessionId)}${failure.turnId ? ` / ${escapeHtml4(failure.turnId)}` : ""}</small>`,
|
|
6693
|
+
failure.replayHref ? `<p><a href="${escapeHtml4(failure.replayHref)}">Open replay</a></p>` : "",
|
|
6694
|
+
"</article>"
|
|
6695
|
+
].join("")),
|
|
6696
|
+
"</div>"
|
|
6697
|
+
].join(""),
|
|
6698
|
+
"</section>",
|
|
6699
|
+
"</div>"
|
|
6700
|
+
].join("");
|
|
6701
|
+
};
|
|
6702
|
+
var createVoiceAssistantHealthJSONHandler = (options) => async () => summarizeVoiceAssistantHealth(options);
|
|
6703
|
+
var createVoiceAssistantHealthHTMLHandler = (options) => async () => {
|
|
6704
|
+
const summary = await summarizeVoiceAssistantHealth(options);
|
|
6705
|
+
const render = options.render ?? renderVoiceAssistantHealthHTML;
|
|
6706
|
+
const body = await render(summary);
|
|
6707
|
+
return new Response(body, {
|
|
6708
|
+
headers: {
|
|
6709
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6710
|
+
...options.headers
|
|
6711
|
+
}
|
|
6712
|
+
});
|
|
6713
|
+
};
|
|
6714
|
+
var createVoiceAssistantHealthRoutes = (options) => {
|
|
6715
|
+
const path = options.path ?? "/api/assistant-health";
|
|
6716
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
6717
|
+
const routes = new Elysia3({
|
|
6718
|
+
name: options.name ?? "absolutejs-voice-assistant-health"
|
|
6719
|
+
}).get(path, createVoiceAssistantHealthJSONHandler(options));
|
|
6720
|
+
if (htmlPath) {
|
|
6721
|
+
routes.get(htmlPath, createVoiceAssistantHealthHTMLHandler(options));
|
|
6722
|
+
}
|
|
6723
|
+
return routes;
|
|
6724
|
+
};
|
|
6725
|
+
// src/sessionReplay.ts
|
|
6726
|
+
import { Elysia as Elysia4 } from "elysia";
|
|
6727
|
+
|
|
6728
|
+
// src/trace.ts
|
|
6729
|
+
var createVoiceTraceEventId = (event) => [
|
|
6730
|
+
event.sessionId,
|
|
6731
|
+
event.turnId ?? "session",
|
|
6732
|
+
event.type,
|
|
6733
|
+
String(event.at ?? Date.now()),
|
|
6734
|
+
crypto.randomUUID()
|
|
6735
|
+
].map(encodeURIComponent).join(":");
|
|
6736
|
+
var createVoiceTraceEvent = (event) => ({
|
|
6737
|
+
...event,
|
|
6738
|
+
at: event.at,
|
|
6739
|
+
id: event.id ?? createVoiceTraceEventId({
|
|
6740
|
+
at: event.at,
|
|
6741
|
+
sessionId: event.sessionId,
|
|
6742
|
+
turnId: event.turnId,
|
|
6743
|
+
type: event.type
|
|
6744
|
+
})
|
|
6745
|
+
});
|
|
6746
|
+
var createVoiceTraceSinkDeliveryId = (events) => {
|
|
6747
|
+
const firstEvent = events[0];
|
|
6748
|
+
return [
|
|
6749
|
+
firstEvent?.sessionId ?? "trace",
|
|
6750
|
+
firstEvent?.traceId ?? "sink",
|
|
6751
|
+
String(firstEvent?.at ?? Date.now()),
|
|
6752
|
+
crypto.randomUUID()
|
|
6753
|
+
].map(encodeURIComponent).join(":");
|
|
6754
|
+
};
|
|
6755
|
+
var createVoiceTraceSinkDeliveryRecord = (input) => {
|
|
5964
6756
|
const createdAt = input.createdAt ?? Date.now();
|
|
5965
6757
|
return {
|
|
5966
6758
|
createdAt,
|
|
@@ -6035,7 +6827,7 @@ var sleep3 = async (delayMs) => {
|
|
|
6035
6827
|
}
|
|
6036
6828
|
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
6037
6829
|
};
|
|
6038
|
-
var
|
|
6830
|
+
var toHex4 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
6039
6831
|
var signVoiceTraceSinkBody = async (input) => {
|
|
6040
6832
|
const encoder = new TextEncoder;
|
|
6041
6833
|
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
@@ -6044,7 +6836,7 @@ var signVoiceTraceSinkBody = async (input) => {
|
|
|
6044
6836
|
}, false, ["sign"]);
|
|
6045
6837
|
const payload = encoder.encode(`${input.timestamp}.${input.body}`);
|
|
6046
6838
|
const signature = await crypto.subtle.sign("HMAC", key, payload);
|
|
6047
|
-
return `sha256=${
|
|
6839
|
+
return `sha256=${toHex4(new Uint8Array(signature))}`;
|
|
6048
6840
|
};
|
|
6049
6841
|
var createVoiceTraceSinkDeliveryError = (input) => {
|
|
6050
6842
|
if (input.response) {
|
|
@@ -6265,7 +7057,7 @@ var exportVoiceTrace = async (input) => {
|
|
|
6265
7057
|
};
|
|
6266
7058
|
};
|
|
6267
7059
|
var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
6268
|
-
var
|
|
7060
|
+
var escapeHtml5 = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6269
7061
|
var formatTraceValue = (value) => {
|
|
6270
7062
|
if (value === undefined || value === null) {
|
|
6271
7063
|
return "";
|
|
@@ -6543,10 +7335,10 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6543
7335
|
const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
|
|
6544
7336
|
return [
|
|
6545
7337
|
"<tr>",
|
|
6546
|
-
`<td>${
|
|
6547
|
-
`<td>${
|
|
6548
|
-
`<td>${
|
|
6549
|
-
`<td><code>${
|
|
7338
|
+
`<td>${escapeHtml5(String(offset))}</td>`,
|
|
7339
|
+
`<td>${escapeHtml5(event.type)}</td>`,
|
|
7340
|
+
`<td>${escapeHtml5(event.turnId ?? "")}</td>`,
|
|
7341
|
+
`<td><code>${escapeHtml5(JSON.stringify(event.payload))}</code></td>`,
|
|
6550
7342
|
"</tr>"
|
|
6551
7343
|
].join("");
|
|
6552
7344
|
}).join(`
|
|
@@ -6557,7 +7349,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6557
7349
|
"<head>",
|
|
6558
7350
|
'<meta charset="utf-8" />',
|
|
6559
7351
|
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
6560
|
-
`<title>${
|
|
7352
|
+
`<title>${escapeHtml5(options.title ?? "Voice Trace")}</title>`,
|
|
6561
7353
|
"<style>",
|
|
6562
7354
|
"body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
|
|
6563
7355
|
"main{max-width:1100px;margin:auto}",
|
|
@@ -6571,7 +7363,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6571
7363
|
"</style>",
|
|
6572
7364
|
"</head>",
|
|
6573
7365
|
"<body><main>",
|
|
6574
|
-
`<h1>${
|
|
7366
|
+
`<h1>${escapeHtml5(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
|
|
6575
7367
|
`<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
|
|
6576
7368
|
'<section class="summary">',
|
|
6577
7369
|
`<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
|
|
@@ -6585,7 +7377,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
6585
7377
|
eventRows,
|
|
6586
7378
|
"</tbody></table>",
|
|
6587
7379
|
"<h2>Markdown Export</h2>",
|
|
6588
|
-
`<pre>${
|
|
7380
|
+
`<pre>${escapeHtml5(markdown)}</pre>`,
|
|
6589
7381
|
"</main></body></html>"
|
|
6590
7382
|
].join(`
|
|
6591
7383
|
`);
|
|
@@ -6597,7 +7389,250 @@ var buildVoiceTraceReplay = (events, options = {}) => ({
|
|
|
6597
7389
|
summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
|
|
6598
7390
|
});
|
|
6599
7391
|
|
|
7392
|
+
// src/sessionReplay.ts
|
|
7393
|
+
var getString3 = (value) => typeof value === "string" ? value : undefined;
|
|
7394
|
+
var escapeHtml6 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
7395
|
+
var increment2 = (record, key) => {
|
|
7396
|
+
record[key] = (record[key] ?? 0) + 1;
|
|
7397
|
+
};
|
|
7398
|
+
var buildReplayTurns = (events) => {
|
|
7399
|
+
const turns = new Map;
|
|
7400
|
+
const getTurn = (turnId) => {
|
|
7401
|
+
const existing = turns.get(turnId);
|
|
7402
|
+
if (existing) {
|
|
7403
|
+
return existing;
|
|
7404
|
+
}
|
|
7405
|
+
const turn = {
|
|
7406
|
+
assistantReplies: [],
|
|
7407
|
+
errors: [],
|
|
7408
|
+
id: turnId,
|
|
7409
|
+
modelCalls: [],
|
|
7410
|
+
tools: [],
|
|
7411
|
+
transcripts: []
|
|
7412
|
+
};
|
|
7413
|
+
turns.set(turnId, turn);
|
|
7414
|
+
return turn;
|
|
7415
|
+
};
|
|
7416
|
+
for (const event of events) {
|
|
7417
|
+
const turnId = event.turnId ?? "session";
|
|
7418
|
+
const turn = getTurn(turnId);
|
|
7419
|
+
switch (event.type) {
|
|
7420
|
+
case "turn.transcript":
|
|
7421
|
+
turn.transcripts.push({
|
|
7422
|
+
isFinal: event.payload.isFinal === true,
|
|
7423
|
+
text: getString3(event.payload.text)
|
|
7424
|
+
});
|
|
7425
|
+
break;
|
|
7426
|
+
case "turn.committed":
|
|
7427
|
+
turn.committedText = getString3(event.payload.text);
|
|
7428
|
+
break;
|
|
7429
|
+
case "turn.assistant": {
|
|
7430
|
+
const text = getString3(event.payload.text);
|
|
7431
|
+
if (text) {
|
|
7432
|
+
turn.assistantReplies.push(text);
|
|
7433
|
+
}
|
|
7434
|
+
break;
|
|
7435
|
+
}
|
|
7436
|
+
case "agent.model":
|
|
7437
|
+
case "assistant.run":
|
|
7438
|
+
turn.modelCalls.push(event.payload);
|
|
7439
|
+
break;
|
|
7440
|
+
case "agent.tool":
|
|
7441
|
+
turn.tools.push(event.payload);
|
|
7442
|
+
break;
|
|
7443
|
+
case "session.error":
|
|
7444
|
+
turn.errors.push(event.payload);
|
|
7445
|
+
break;
|
|
7446
|
+
}
|
|
7447
|
+
}
|
|
7448
|
+
return [...turns.values()];
|
|
7449
|
+
};
|
|
7450
|
+
var summarizeVoiceSessionReplay = async (options) => {
|
|
7451
|
+
const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
|
|
7452
|
+
const events = filterVoiceTraceEvents(sourceEvents, {
|
|
7453
|
+
sessionId: options.sessionId
|
|
7454
|
+
});
|
|
7455
|
+
const replay = buildVoiceTraceReplay(events, {
|
|
7456
|
+
evaluation: options.evaluation,
|
|
7457
|
+
redact: options.redact,
|
|
7458
|
+
title: options.title ?? `Voice Session ${options.sessionId}`
|
|
7459
|
+
});
|
|
7460
|
+
const startedAt = replay.summary.startedAt;
|
|
7461
|
+
return {
|
|
7462
|
+
evaluation: replay.evaluation,
|
|
7463
|
+
events,
|
|
7464
|
+
html: replay.html,
|
|
7465
|
+
markdown: replay.markdown,
|
|
7466
|
+
sessionId: options.sessionId,
|
|
7467
|
+
summary: replay.summary,
|
|
7468
|
+
timeline: events.map((event) => ({
|
|
7469
|
+
at: event.at,
|
|
7470
|
+
offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
|
|
7471
|
+
payload: event.payload,
|
|
7472
|
+
turnId: event.turnId,
|
|
7473
|
+
type: event.type
|
|
7474
|
+
})),
|
|
7475
|
+
turns: buildReplayTurns(events)
|
|
7476
|
+
};
|
|
7477
|
+
};
|
|
7478
|
+
var summarizeVoiceSessions = async (options = {}) => {
|
|
7479
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
7480
|
+
const grouped = new Map;
|
|
7481
|
+
for (const event of events) {
|
|
7482
|
+
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
7483
|
+
}
|
|
7484
|
+
const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
|
|
7485
|
+
const sorted = filterVoiceTraceEvents(sessionEvents);
|
|
7486
|
+
const summary = buildVoiceTraceReplay(sorted, {
|
|
7487
|
+
evaluation: {
|
|
7488
|
+
requireAssistantReply: false,
|
|
7489
|
+
requireCompletedCall: false,
|
|
7490
|
+
requireTranscript: false,
|
|
7491
|
+
requireTurn: false
|
|
7492
|
+
}
|
|
7493
|
+
}).summary;
|
|
7494
|
+
const providerErrors = {};
|
|
7495
|
+
const providers = new Set;
|
|
7496
|
+
let latestOutcome;
|
|
7497
|
+
let errorCount = 0;
|
|
7498
|
+
for (const event of sorted) {
|
|
7499
|
+
const provider = getString3(event.payload.provider);
|
|
7500
|
+
if (provider) {
|
|
7501
|
+
providers.add(provider);
|
|
7502
|
+
}
|
|
7503
|
+
if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
|
|
7504
|
+
errorCount += 1;
|
|
7505
|
+
increment2(providerErrors, provider ?? "unknown");
|
|
7506
|
+
}
|
|
7507
|
+
const outcome = getString3(event.payload.outcome);
|
|
7508
|
+
if (outcome) {
|
|
7509
|
+
latestOutcome = outcome;
|
|
7510
|
+
}
|
|
7511
|
+
}
|
|
7512
|
+
const item = {
|
|
7513
|
+
endedAt: summary.endedAt,
|
|
7514
|
+
errorCount,
|
|
7515
|
+
eventCount: summary.eventCount,
|
|
7516
|
+
latestOutcome,
|
|
7517
|
+
providerErrors,
|
|
7518
|
+
providers: [...providers].sort(),
|
|
7519
|
+
sessionId,
|
|
7520
|
+
startedAt: summary.startedAt,
|
|
7521
|
+
status: errorCount > 0 ? "failed" : "healthy",
|
|
7522
|
+
transcriptCount: summary.transcriptCount,
|
|
7523
|
+
turnCount: summary.turnCount
|
|
7524
|
+
};
|
|
7525
|
+
const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
|
|
7526
|
+
return {
|
|
7527
|
+
...item,
|
|
7528
|
+
replayHref
|
|
7529
|
+
};
|
|
7530
|
+
});
|
|
7531
|
+
const search = options.q?.trim().toLowerCase();
|
|
7532
|
+
return sessions.filter((session) => {
|
|
7533
|
+
if (options.status && options.status !== "all" && session.status !== options.status) {
|
|
7534
|
+
return false;
|
|
7535
|
+
}
|
|
7536
|
+
if (options.provider && !session.providers.includes(options.provider)) {
|
|
7537
|
+
return false;
|
|
7538
|
+
}
|
|
7539
|
+
if (!search) {
|
|
7540
|
+
return true;
|
|
7541
|
+
}
|
|
7542
|
+
return [
|
|
7543
|
+
session.sessionId,
|
|
7544
|
+
session.latestOutcome,
|
|
7545
|
+
session.status,
|
|
7546
|
+
...session.providers
|
|
7547
|
+
].some((value) => value?.toLowerCase().includes(search));
|
|
7548
|
+
}).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
|
|
7549
|
+
};
|
|
7550
|
+
var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
|
|
7551
|
+
'<div class="voice-sessions-list">',
|
|
7552
|
+
...sessions.map((session) => [
|
|
7553
|
+
`<article class="voice-session-card ${escapeHtml6(session.status)}">`,
|
|
7554
|
+
'<div class="voice-session-card-header">',
|
|
7555
|
+
`<strong>${escapeHtml6(session.sessionId)}</strong>`,
|
|
7556
|
+
`<span>${escapeHtml6(session.status)}</span>`,
|
|
7557
|
+
"</div>",
|
|
7558
|
+
"<dl>",
|
|
7559
|
+
`<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
|
|
7560
|
+
`<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
|
|
7561
|
+
`<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
|
|
7562
|
+
`<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
|
|
7563
|
+
"</dl>",
|
|
7564
|
+
session.latestOutcome ? `<p>Outcome: ${escapeHtml6(session.latestOutcome)}</p>` : "",
|
|
7565
|
+
session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml6).join(", ")}</p>` : "",
|
|
7566
|
+
session.replayHref ? `<p><a href="${escapeHtml6(session.replayHref)}">Open replay</a></p>` : "",
|
|
7567
|
+
"</article>"
|
|
7568
|
+
].join("")),
|
|
7569
|
+
"</div>"
|
|
7570
|
+
].join("");
|
|
7571
|
+
var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
|
|
7572
|
+
...options,
|
|
7573
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
7574
|
+
provider: query?.provider ?? options.provider,
|
|
7575
|
+
q: query?.q ?? options.q,
|
|
7576
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
7577
|
+
});
|
|
7578
|
+
var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
|
|
7579
|
+
const sessions = await summarizeVoiceSessions({
|
|
7580
|
+
...options,
|
|
7581
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
7582
|
+
provider: query?.provider ?? options.provider,
|
|
7583
|
+
q: query?.q ?? options.q,
|
|
7584
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
7585
|
+
});
|
|
7586
|
+
const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
|
|
7587
|
+
return new Response(body, {
|
|
7588
|
+
headers: {
|
|
7589
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7590
|
+
...options.headers
|
|
7591
|
+
}
|
|
7592
|
+
});
|
|
7593
|
+
};
|
|
7594
|
+
var createVoiceSessionListRoutes = (options = {}) => {
|
|
7595
|
+
const path = options.path ?? "/api/voice-sessions";
|
|
7596
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
7597
|
+
const routes = new Elysia4({
|
|
7598
|
+
name: options.name ?? "absolutejs-voice-session-list"
|
|
7599
|
+
}).get(path, createVoiceSessionsJSONHandler(options));
|
|
7600
|
+
if (htmlPath) {
|
|
7601
|
+
routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
|
|
7602
|
+
}
|
|
7603
|
+
return routes;
|
|
7604
|
+
};
|
|
7605
|
+
var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
|
|
7606
|
+
...options,
|
|
7607
|
+
sessionId: params.sessionId ?? ""
|
|
7608
|
+
});
|
|
7609
|
+
var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
|
|
7610
|
+
const replay = await summarizeVoiceSessionReplay({
|
|
7611
|
+
...options,
|
|
7612
|
+
sessionId: params.sessionId ?? ""
|
|
7613
|
+
});
|
|
7614
|
+
const body = await (options.render?.(replay) ?? replay.html);
|
|
7615
|
+
return new Response(body, {
|
|
7616
|
+
headers: {
|
|
7617
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7618
|
+
...options.headers
|
|
7619
|
+
}
|
|
7620
|
+
});
|
|
7621
|
+
};
|
|
7622
|
+
var createVoiceSessionReplayRoutes = (options) => {
|
|
7623
|
+
const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
|
|
7624
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
7625
|
+
const routes = new Elysia4({
|
|
7626
|
+
name: options.name ?? "absolutejs-voice-session-replay"
|
|
7627
|
+
}).get(path, createVoiceSessionReplayJSONHandler(options));
|
|
7628
|
+
if (htmlPath) {
|
|
7629
|
+
routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
|
|
7630
|
+
}
|
|
7631
|
+
return routes;
|
|
7632
|
+
};
|
|
6600
7633
|
// src/fileStore.ts
|
|
7634
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
7635
|
+
import { join } from "path";
|
|
6601
7636
|
var listJsonFiles = async (directory) => {
|
|
6602
7637
|
try {
|
|
6603
7638
|
const entries = await readdir(directory, {
|
|
@@ -6613,6 +7648,7 @@ var listJsonFiles = async (directory) => {
|
|
|
6613
7648
|
};
|
|
6614
7649
|
var encodeStoreId = (id) => `${encodeURIComponent(id)}.json`;
|
|
6615
7650
|
var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
|
|
7651
|
+
var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
|
|
6616
7652
|
var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
6617
7653
|
var writeJsonFile = async (path, value, options) => {
|
|
6618
7654
|
await mkdir(options.directory, {
|
|
@@ -6773,106 +7809,915 @@ var createVoiceFileExternalObjectMapStore = (options) => {
|
|
|
6773
7809
|
};
|
|
6774
7810
|
return { find, get, list, remove, set };
|
|
6775
7811
|
};
|
|
6776
|
-
var createVoiceFileTraceEventStore = (options) => {
|
|
6777
|
-
const append = async (event) => {
|
|
6778
|
-
const stored = createVoiceTraceEvent(event);
|
|
6779
|
-
await writeJsonFile(resolveFilePath(options.directory, stored.id), stored, options);
|
|
6780
|
-
return stored;
|
|
6781
|
-
};
|
|
6782
|
-
const get = async (id) => {
|
|
6783
|
-
const path = resolveFilePath(options.directory, id);
|
|
6784
|
-
try {
|
|
6785
|
-
return await readJsonFile(path);
|
|
6786
|
-
} catch (error) {
|
|
6787
|
-
if (error.code === "ENOENT") {
|
|
6788
|
-
return;
|
|
7812
|
+
var createVoiceFileTraceEventStore = (options) => {
|
|
7813
|
+
const append = async (event) => {
|
|
7814
|
+
const stored = createVoiceTraceEvent(event);
|
|
7815
|
+
await writeJsonFile(resolveFilePath(options.directory, stored.id), stored, options);
|
|
7816
|
+
return stored;
|
|
7817
|
+
};
|
|
7818
|
+
const get = async (id) => {
|
|
7819
|
+
const path = resolveFilePath(options.directory, id);
|
|
7820
|
+
try {
|
|
7821
|
+
return await readJsonFile(path);
|
|
7822
|
+
} catch (error) {
|
|
7823
|
+
if (error.code === "ENOENT") {
|
|
7824
|
+
return;
|
|
7825
|
+
}
|
|
7826
|
+
throw error;
|
|
7827
|
+
}
|
|
7828
|
+
};
|
|
7829
|
+
const list = async (filter = {}) => {
|
|
7830
|
+
const files = await listJsonFiles(options.directory);
|
|
7831
|
+
const events = await Promise.all(files.map((file) => readJsonFile(file)));
|
|
7832
|
+
return filterVoiceTraceEvents(events, filter);
|
|
7833
|
+
};
|
|
7834
|
+
const remove = async (id) => {
|
|
7835
|
+
await rm(resolveFilePath(options.directory, id), {
|
|
7836
|
+
force: true
|
|
7837
|
+
});
|
|
7838
|
+
};
|
|
7839
|
+
return { append, get, list, remove };
|
|
7840
|
+
};
|
|
7841
|
+
var createVoiceFileTraceSinkDeliveryStore = (options) => {
|
|
7842
|
+
const get = async (id) => {
|
|
7843
|
+
const path = resolveFilePath(options.directory, id);
|
|
7844
|
+
try {
|
|
7845
|
+
return await readJsonFile(path);
|
|
7846
|
+
} catch (error) {
|
|
7847
|
+
if (error.code === "ENOENT") {
|
|
7848
|
+
return;
|
|
7849
|
+
}
|
|
7850
|
+
throw error;
|
|
7851
|
+
}
|
|
7852
|
+
};
|
|
7853
|
+
const list = async () => {
|
|
7854
|
+
const files = await listJsonFiles(options.directory);
|
|
7855
|
+
const deliveries = await Promise.all(files.map((file) => readJsonFile(file)));
|
|
7856
|
+
return deliveries.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id));
|
|
7857
|
+
};
|
|
7858
|
+
const set = async (id, delivery) => {
|
|
7859
|
+
await writeJsonFile(resolveFilePath(options.directory, id), {
|
|
7860
|
+
...delivery,
|
|
7861
|
+
id
|
|
7862
|
+
}, options);
|
|
7863
|
+
};
|
|
7864
|
+
const remove = async (id) => {
|
|
7865
|
+
await rm(resolveFilePath(options.directory, id), {
|
|
7866
|
+
force: true
|
|
7867
|
+
});
|
|
7868
|
+
};
|
|
7869
|
+
return { get, list, remove, set };
|
|
7870
|
+
};
|
|
7871
|
+
var createVoiceFileAssistantMemoryStore = (options) => {
|
|
7872
|
+
const get = async (input) => {
|
|
7873
|
+
const path = resolveFilePath(options.directory, createMemoryStoreId(input));
|
|
7874
|
+
try {
|
|
7875
|
+
return await readJsonFile(path);
|
|
7876
|
+
} catch (error) {
|
|
7877
|
+
if (error.code === "ENOENT") {
|
|
7878
|
+
return;
|
|
7879
|
+
}
|
|
7880
|
+
throw error;
|
|
7881
|
+
}
|
|
7882
|
+
};
|
|
7883
|
+
const list = async (input) => {
|
|
7884
|
+
const files = await listJsonFiles(options.directory);
|
|
7885
|
+
const records = await Promise.all(files.map((file) => readJsonFile(file)));
|
|
7886
|
+
return records.filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt);
|
|
7887
|
+
};
|
|
7888
|
+
const set = async (input) => {
|
|
7889
|
+
const existing = await get(input);
|
|
7890
|
+
const record = createVoiceAssistantMemoryRecord({
|
|
7891
|
+
...input,
|
|
7892
|
+
createdAt: input.createdAt ?? existing?.createdAt,
|
|
7893
|
+
updatedAt: input.updatedAt
|
|
7894
|
+
});
|
|
7895
|
+
await writeJsonFile(resolveFilePath(options.directory, createMemoryStoreId(record)), record, options);
|
|
7896
|
+
return record;
|
|
7897
|
+
};
|
|
7898
|
+
const remove = async (input) => {
|
|
7899
|
+
await rm(resolveFilePath(options.directory, createMemoryStoreId(input)), {
|
|
7900
|
+
force: true
|
|
7901
|
+
});
|
|
7902
|
+
};
|
|
7903
|
+
return { delete: remove, get, list, set };
|
|
7904
|
+
};
|
|
7905
|
+
var createVoiceFileRuntimeStorage = (options) => ({
|
|
7906
|
+
events: createVoiceFileIntegrationEventStore({
|
|
7907
|
+
...options,
|
|
7908
|
+
directory: join(options.directory, "events")
|
|
7909
|
+
}),
|
|
7910
|
+
externalObjects: createVoiceFileExternalObjectMapStore({
|
|
7911
|
+
...options,
|
|
7912
|
+
directory: join(options.directory, "external-objects")
|
|
7913
|
+
}),
|
|
7914
|
+
memories: createVoiceFileAssistantMemoryStore({
|
|
7915
|
+
...options,
|
|
7916
|
+
directory: join(options.directory, "memories")
|
|
7917
|
+
}),
|
|
7918
|
+
reviews: createVoiceFileReviewStore({
|
|
7919
|
+
...options,
|
|
7920
|
+
directory: join(options.directory, "reviews")
|
|
7921
|
+
}),
|
|
7922
|
+
session: createVoiceFileSessionStore({
|
|
7923
|
+
...options,
|
|
7924
|
+
directory: join(options.directory, "sessions")
|
|
7925
|
+
}),
|
|
7926
|
+
tasks: createVoiceFileTaskStore({
|
|
7927
|
+
...options,
|
|
7928
|
+
directory: join(options.directory, "tasks")
|
|
7929
|
+
}),
|
|
7930
|
+
traceDeliveries: createVoiceFileTraceSinkDeliveryStore({
|
|
7931
|
+
...options,
|
|
7932
|
+
directory: join(options.directory, "trace-deliveries")
|
|
7933
|
+
}),
|
|
7934
|
+
traces: createVoiceFileTraceEventStore({
|
|
7935
|
+
...options,
|
|
7936
|
+
directory: join(options.directory, "traces")
|
|
7937
|
+
})
|
|
7938
|
+
});
|
|
7939
|
+
var createStoredVoiceCallReviewArtifact = (id, artifact) => withVoiceCallReviewId(id, artifact);
|
|
7940
|
+
var createStoredVoiceOpsTask = (id, task) => withVoiceOpsTaskId(id, task);
|
|
7941
|
+
var createStoredVoiceIntegrationEvent = (id, event) => withVoiceIntegrationEventId(id, event);
|
|
7942
|
+
var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectMap({
|
|
7943
|
+
at: mapping.at,
|
|
7944
|
+
externalId: mapping.externalId,
|
|
7945
|
+
provider: mapping.provider,
|
|
7946
|
+
sinkId: mapping.sinkId,
|
|
7947
|
+
sourceId: mapping.sourceId,
|
|
7948
|
+
sourceType: mapping.sourceType
|
|
7949
|
+
});
|
|
7950
|
+
// src/modelAdapters.ts
|
|
7951
|
+
var OUTPUT_SCHEMA = {
|
|
7952
|
+
additionalProperties: false,
|
|
7953
|
+
properties: {
|
|
7954
|
+
assistantText: {
|
|
7955
|
+
type: "string"
|
|
7956
|
+
},
|
|
7957
|
+
complete: {
|
|
7958
|
+
type: "boolean"
|
|
7959
|
+
},
|
|
7960
|
+
escalate: {
|
|
7961
|
+
additionalProperties: false,
|
|
7962
|
+
properties: {
|
|
7963
|
+
metadata: {
|
|
7964
|
+
additionalProperties: true,
|
|
7965
|
+
type: "object"
|
|
7966
|
+
},
|
|
7967
|
+
reason: {
|
|
7968
|
+
type: "string"
|
|
7969
|
+
}
|
|
7970
|
+
},
|
|
7971
|
+
required: ["reason"],
|
|
7972
|
+
type: "object"
|
|
7973
|
+
},
|
|
7974
|
+
noAnswer: {
|
|
7975
|
+
additionalProperties: false,
|
|
7976
|
+
properties: {
|
|
7977
|
+
metadata: {
|
|
7978
|
+
additionalProperties: true,
|
|
7979
|
+
type: "object"
|
|
7980
|
+
}
|
|
7981
|
+
},
|
|
7982
|
+
type: "object"
|
|
7983
|
+
},
|
|
7984
|
+
result: {
|
|
7985
|
+
additionalProperties: true,
|
|
7986
|
+
type: "object"
|
|
7987
|
+
},
|
|
7988
|
+
transfer: {
|
|
7989
|
+
additionalProperties: false,
|
|
7990
|
+
properties: {
|
|
7991
|
+
metadata: {
|
|
7992
|
+
additionalProperties: true,
|
|
7993
|
+
type: "object"
|
|
7994
|
+
},
|
|
7995
|
+
reason: {
|
|
7996
|
+
type: "string"
|
|
7997
|
+
},
|
|
7998
|
+
target: {
|
|
7999
|
+
type: "string"
|
|
8000
|
+
}
|
|
8001
|
+
},
|
|
8002
|
+
required: ["target"],
|
|
8003
|
+
type: "object"
|
|
8004
|
+
},
|
|
8005
|
+
voicemail: {
|
|
8006
|
+
additionalProperties: false,
|
|
8007
|
+
properties: {
|
|
8008
|
+
metadata: {
|
|
8009
|
+
additionalProperties: true,
|
|
8010
|
+
type: "object"
|
|
8011
|
+
}
|
|
8012
|
+
},
|
|
8013
|
+
type: "object"
|
|
8014
|
+
}
|
|
8015
|
+
},
|
|
8016
|
+
type: "object"
|
|
8017
|
+
};
|
|
8018
|
+
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.";
|
|
8019
|
+
var stripJSONCodeFence = (value) => {
|
|
8020
|
+
const trimmed = value.trim();
|
|
8021
|
+
const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
8022
|
+
return match?.[1]?.trim() ?? value;
|
|
8023
|
+
};
|
|
8024
|
+
var parseJSON = (value) => {
|
|
8025
|
+
try {
|
|
8026
|
+
const parsed = JSON.parse(stripJSONCodeFence(value));
|
|
8027
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
8028
|
+
} catch {
|
|
8029
|
+
return {
|
|
8030
|
+
assistantText: value
|
|
8031
|
+
};
|
|
8032
|
+
}
|
|
8033
|
+
};
|
|
8034
|
+
var parseJSONValue = (value) => {
|
|
8035
|
+
try {
|
|
8036
|
+
return JSON.parse(value);
|
|
8037
|
+
} catch {
|
|
8038
|
+
return value;
|
|
8039
|
+
}
|
|
8040
|
+
};
|
|
8041
|
+
var getMessageToolCalls = (message) => {
|
|
8042
|
+
const toolCalls = message.metadata?.toolCalls;
|
|
8043
|
+
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
8044
|
+
};
|
|
8045
|
+
var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
|
|
8046
|
+
var sleep4 = (ms) => new Promise((resolve2) => {
|
|
8047
|
+
setTimeout(resolve2, ms);
|
|
8048
|
+
});
|
|
8049
|
+
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
8050
|
+
var defaultIsRateLimitError = (error) => /(\b429\b|rate limit|quota|too many requests)/i.test(errorMessage(error));
|
|
8051
|
+
var normalizeRouteOutput = (output) => {
|
|
8052
|
+
const result = {};
|
|
8053
|
+
if (typeof output.assistantText === "string") {
|
|
8054
|
+
result.assistantText = output.assistantText;
|
|
8055
|
+
}
|
|
8056
|
+
if (typeof output.complete === "boolean") {
|
|
8057
|
+
result.complete = output.complete;
|
|
8058
|
+
}
|
|
8059
|
+
if (output.result !== undefined) {
|
|
8060
|
+
result.result = output.result;
|
|
8061
|
+
}
|
|
8062
|
+
if (output.transfer && typeof output.transfer === "object") {
|
|
8063
|
+
const transfer = output.transfer;
|
|
8064
|
+
if (typeof transfer.target === "string") {
|
|
8065
|
+
result.transfer = {
|
|
8066
|
+
metadata: transfer.metadata && typeof transfer.metadata === "object" ? transfer.metadata : undefined,
|
|
8067
|
+
reason: typeof transfer.reason === "string" ? transfer.reason : undefined,
|
|
8068
|
+
target: transfer.target
|
|
8069
|
+
};
|
|
8070
|
+
}
|
|
8071
|
+
}
|
|
8072
|
+
if (output.escalate && typeof output.escalate === "object") {
|
|
8073
|
+
const escalate = output.escalate;
|
|
8074
|
+
if (typeof escalate.reason === "string") {
|
|
8075
|
+
result.escalate = {
|
|
8076
|
+
metadata: escalate.metadata && typeof escalate.metadata === "object" ? escalate.metadata : undefined,
|
|
8077
|
+
reason: escalate.reason
|
|
8078
|
+
};
|
|
8079
|
+
}
|
|
8080
|
+
}
|
|
8081
|
+
if (output.voicemail && typeof output.voicemail === "object") {
|
|
8082
|
+
const voicemail = output.voicemail;
|
|
8083
|
+
result.voicemail = {
|
|
8084
|
+
metadata: voicemail.metadata && typeof voicemail.metadata === "object" ? voicemail.metadata : undefined
|
|
8085
|
+
};
|
|
8086
|
+
}
|
|
8087
|
+
if (output.noAnswer && typeof output.noAnswer === "object") {
|
|
8088
|
+
const noAnswer = output.noAnswer;
|
|
8089
|
+
result.noAnswer = {
|
|
8090
|
+
metadata: noAnswer.metadata && typeof noAnswer.metadata === "object" ? noAnswer.metadata : undefined
|
|
8091
|
+
};
|
|
8092
|
+
}
|
|
8093
|
+
return result;
|
|
8094
|
+
};
|
|
8095
|
+
var createJSONVoiceAssistantModel = (options) => ({
|
|
8096
|
+
generate: async (input) => {
|
|
8097
|
+
const output = await options.generate(input);
|
|
8098
|
+
if ("assistantText" in output || "toolCalls" in output || "complete" in output || "transfer" in output || "escalate" in output) {
|
|
8099
|
+
return output;
|
|
8100
|
+
}
|
|
8101
|
+
return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
|
|
8102
|
+
}
|
|
8103
|
+
});
|
|
8104
|
+
var createVoiceProviderRouter = (options) => {
|
|
8105
|
+
const providerIds = Object.keys(options.providers);
|
|
8106
|
+
const firstProvider = providerIds[0];
|
|
8107
|
+
const policy = typeof options.policy === "string" ? {
|
|
8108
|
+
strategy: options.policy
|
|
8109
|
+
} : options.policy;
|
|
8110
|
+
const strategy = policy?.strategy ?? "prefer-selected";
|
|
8111
|
+
const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
|
|
8112
|
+
const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
|
|
8113
|
+
const healthState = new Map;
|
|
8114
|
+
const now = () => healthOptions?.now?.() ?? Date.now();
|
|
8115
|
+
const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
|
|
8116
|
+
const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
|
|
8117
|
+
const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
|
|
8118
|
+
const getHealth = (provider) => {
|
|
8119
|
+
const existing = healthState.get(provider);
|
|
8120
|
+
if (existing) {
|
|
8121
|
+
return existing;
|
|
8122
|
+
}
|
|
8123
|
+
const next = {
|
|
8124
|
+
consecutiveFailures: 0,
|
|
8125
|
+
provider,
|
|
8126
|
+
status: "healthy"
|
|
8127
|
+
};
|
|
8128
|
+
healthState.set(provider, next);
|
|
8129
|
+
return next;
|
|
8130
|
+
};
|
|
8131
|
+
const cloneHealth = (provider) => {
|
|
8132
|
+
if (!healthOptions) {
|
|
8133
|
+
return;
|
|
8134
|
+
}
|
|
8135
|
+
return {
|
|
8136
|
+
...getHealth(provider)
|
|
8137
|
+
};
|
|
8138
|
+
};
|
|
8139
|
+
const getSuppressionRemainingMs = (provider) => {
|
|
8140
|
+
if (!healthOptions) {
|
|
8141
|
+
return;
|
|
8142
|
+
}
|
|
8143
|
+
const suppressedUntil = getHealth(provider).suppressedUntil;
|
|
8144
|
+
return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
|
|
8145
|
+
};
|
|
8146
|
+
const isSuppressed = (provider) => {
|
|
8147
|
+
if (!healthOptions) {
|
|
8148
|
+
return false;
|
|
8149
|
+
}
|
|
8150
|
+
const health = getHealth(provider);
|
|
8151
|
+
return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
|
|
8152
|
+
};
|
|
8153
|
+
const recordProviderSuccess = (provider) => {
|
|
8154
|
+
if (!healthOptions) {
|
|
8155
|
+
return;
|
|
8156
|
+
}
|
|
8157
|
+
const health = getHealth(provider);
|
|
8158
|
+
health.consecutiveFailures = 0;
|
|
8159
|
+
health.status = "healthy";
|
|
8160
|
+
health.suppressedUntil = undefined;
|
|
8161
|
+
return cloneHealth(provider);
|
|
8162
|
+
};
|
|
8163
|
+
const recordProviderError = (provider, isProviderError, rateLimited) => {
|
|
8164
|
+
if (!healthOptions || !isProviderError) {
|
|
8165
|
+
return cloneHealth(provider);
|
|
8166
|
+
}
|
|
8167
|
+
const currentTime = now();
|
|
8168
|
+
const health = getHealth(provider);
|
|
8169
|
+
health.consecutiveFailures += 1;
|
|
8170
|
+
health.lastFailureAt = currentTime;
|
|
8171
|
+
if (rateLimited) {
|
|
8172
|
+
health.lastRateLimitedAt = currentTime;
|
|
8173
|
+
}
|
|
8174
|
+
if (rateLimited || health.consecutiveFailures >= failureThreshold) {
|
|
8175
|
+
health.status = "suppressed";
|
|
8176
|
+
health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
|
|
8177
|
+
}
|
|
8178
|
+
return cloneHealth(provider);
|
|
8179
|
+
};
|
|
8180
|
+
const resolveAllowedProviders = async (input) => {
|
|
8181
|
+
const allowProviders = policy?.allowProviders ?? options.allowProviders;
|
|
8182
|
+
const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
|
|
8183
|
+
return new Set(allowed ?? providerIds);
|
|
8184
|
+
};
|
|
8185
|
+
const sortProviders = (providers) => {
|
|
8186
|
+
if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
|
|
8187
|
+
return providers;
|
|
8188
|
+
}
|
|
8189
|
+
return [...providers].sort((left, right) => {
|
|
8190
|
+
const leftProfile = options.providerProfiles?.[left];
|
|
8191
|
+
const rightProfile = options.providerProfiles?.[right];
|
|
8192
|
+
const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
8193
|
+
const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
8194
|
+
return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
|
|
8195
|
+
});
|
|
8196
|
+
};
|
|
8197
|
+
const resolveOrder = async (input) => {
|
|
8198
|
+
const selectedProvider = await options.selectProvider?.(input);
|
|
8199
|
+
const allowedProviders = await resolveAllowedProviders(input);
|
|
8200
|
+
const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
|
|
8201
|
+
const rankedProviders = sortProviders([
|
|
8202
|
+
...fallbackOrder ?? providerIds
|
|
8203
|
+
]).filter((provider) => allowedProviders.has(provider));
|
|
8204
|
+
const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
|
|
8205
|
+
const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
|
|
8206
|
+
const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
|
|
8207
|
+
const seen = new Set;
|
|
8208
|
+
const order = [];
|
|
8209
|
+
const candidates = strategy === "ordered" ? candidateRankedProviders : [
|
|
8210
|
+
preferred,
|
|
8211
|
+
...candidateRankedProviders,
|
|
8212
|
+
...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
|
|
8213
|
+
];
|
|
8214
|
+
for (const provider of candidates) {
|
|
8215
|
+
if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
|
|
8216
|
+
continue;
|
|
8217
|
+
}
|
|
8218
|
+
seen.add(provider);
|
|
8219
|
+
order.push(provider);
|
|
8220
|
+
}
|
|
8221
|
+
return {
|
|
8222
|
+
order,
|
|
8223
|
+
selectedProvider: preferred
|
|
8224
|
+
};
|
|
8225
|
+
};
|
|
8226
|
+
const emit = async (event, input) => {
|
|
8227
|
+
await options.onProviderEvent?.(event, input);
|
|
8228
|
+
};
|
|
8229
|
+
return {
|
|
8230
|
+
generate: async (input) => {
|
|
8231
|
+
const { order, selectedProvider } = await resolveOrder(input);
|
|
8232
|
+
if (!selectedProvider || order.length === 0) {
|
|
8233
|
+
throw new Error("Voice provider router has no available providers.");
|
|
8234
|
+
}
|
|
8235
|
+
let lastError;
|
|
8236
|
+
for (const [index, provider] of order.entries()) {
|
|
8237
|
+
const model = options.providers[provider];
|
|
8238
|
+
if (!model) {
|
|
8239
|
+
continue;
|
|
8240
|
+
}
|
|
8241
|
+
const startedAt = Date.now();
|
|
8242
|
+
try {
|
|
8243
|
+
const output = await model.generate(input);
|
|
8244
|
+
const providerHealth = recordProviderSuccess(provider);
|
|
8245
|
+
await emit({
|
|
8246
|
+
at: Date.now(),
|
|
8247
|
+
elapsedMs: Date.now() - startedAt,
|
|
8248
|
+
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
8249
|
+
provider,
|
|
8250
|
+
providerHealth,
|
|
8251
|
+
recovered: provider !== selectedProvider,
|
|
8252
|
+
selectedProvider,
|
|
8253
|
+
status: provider === selectedProvider ? "success" : "fallback"
|
|
8254
|
+
}, input);
|
|
8255
|
+
return output;
|
|
8256
|
+
} catch (error) {
|
|
8257
|
+
lastError = error;
|
|
8258
|
+
const hasNextProvider = index < order.length - 1;
|
|
8259
|
+
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
8260
|
+
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
8261
|
+
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
8262
|
+
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
8263
|
+
const nextProvider = hasNextProvider ? order[index + 1] : undefined;
|
|
8264
|
+
await emit({
|
|
8265
|
+
at: Date.now(),
|
|
8266
|
+
elapsedMs: Date.now() - startedAt,
|
|
8267
|
+
error: errorMessage(error),
|
|
8268
|
+
fallbackProvider: shouldFallback ? nextProvider : undefined,
|
|
8269
|
+
provider,
|
|
8270
|
+
providerHealth,
|
|
8271
|
+
rateLimited,
|
|
8272
|
+
selectedProvider,
|
|
8273
|
+
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
8274
|
+
suppressedUntil: providerHealth?.suppressedUntil,
|
|
8275
|
+
status: "error"
|
|
8276
|
+
}, input);
|
|
8277
|
+
if (!hasNextProvider || !shouldFallback) {
|
|
8278
|
+
throw error;
|
|
8279
|
+
}
|
|
8280
|
+
}
|
|
8281
|
+
}
|
|
8282
|
+
throw lastError ?? new Error("Voice provider router did not run a provider.");
|
|
8283
|
+
}
|
|
8284
|
+
};
|
|
8285
|
+
};
|
|
8286
|
+
var messageToOpenAIInput = (message) => {
|
|
8287
|
+
if (message.role === "tool") {
|
|
8288
|
+
return [
|
|
8289
|
+
{
|
|
8290
|
+
call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
|
|
8291
|
+
output: message.content,
|
|
8292
|
+
type: "function_call_output"
|
|
8293
|
+
}
|
|
8294
|
+
];
|
|
8295
|
+
}
|
|
8296
|
+
const toolCalls = getMessageToolCalls(message);
|
|
8297
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
8298
|
+
return toolCalls.map((toolCall) => ({
|
|
8299
|
+
arguments: JSON.stringify(toolCall.args),
|
|
8300
|
+
call_id: toolCall.id ?? crypto.randomUUID(),
|
|
8301
|
+
name: toolCall.name,
|
|
8302
|
+
type: "function_call"
|
|
8303
|
+
}));
|
|
8304
|
+
}
|
|
8305
|
+
return [
|
|
8306
|
+
{
|
|
8307
|
+
content: message.content,
|
|
8308
|
+
role: message.role === "system" ? "developer" : message.role
|
|
8309
|
+
}
|
|
8310
|
+
];
|
|
8311
|
+
};
|
|
8312
|
+
var messagesToOpenAIInput = (messages) => messages.flatMap(messageToOpenAIInput);
|
|
8313
|
+
var messageToAnthropicMessage = (message) => {
|
|
8314
|
+
if (message.role === "system") {
|
|
8315
|
+
return;
|
|
8316
|
+
}
|
|
8317
|
+
if (message.role === "tool") {
|
|
8318
|
+
if (!message.toolCallId) {
|
|
8319
|
+
return {
|
|
8320
|
+
content: `Tool result from ${message.name ?? "tool"}: ${message.content}`,
|
|
8321
|
+
role: "user"
|
|
8322
|
+
};
|
|
8323
|
+
}
|
|
8324
|
+
return {
|
|
8325
|
+
content: [
|
|
8326
|
+
{
|
|
8327
|
+
content: message.content,
|
|
8328
|
+
tool_use_id: message.toolCallId,
|
|
8329
|
+
type: "tool_result"
|
|
8330
|
+
}
|
|
8331
|
+
],
|
|
8332
|
+
role: "user"
|
|
8333
|
+
};
|
|
8334
|
+
}
|
|
8335
|
+
const toolCalls = getMessageToolCalls(message);
|
|
8336
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
8337
|
+
return {
|
|
8338
|
+
content: [
|
|
8339
|
+
...message.content ? [
|
|
8340
|
+
{
|
|
8341
|
+
text: message.content,
|
|
8342
|
+
type: "text"
|
|
8343
|
+
}
|
|
8344
|
+
] : [],
|
|
8345
|
+
...toolCalls.map((toolCall) => ({
|
|
8346
|
+
id: toolCall.id ?? crypto.randomUUID(),
|
|
8347
|
+
input: toolCall.args,
|
|
8348
|
+
name: toolCall.name,
|
|
8349
|
+
type: "tool_use"
|
|
8350
|
+
}))
|
|
8351
|
+
],
|
|
8352
|
+
role: "assistant"
|
|
8353
|
+
};
|
|
8354
|
+
}
|
|
8355
|
+
return {
|
|
8356
|
+
content: message.content,
|
|
8357
|
+
role: message.role
|
|
8358
|
+
};
|
|
8359
|
+
};
|
|
8360
|
+
var toGeminiSchema = (schema) => {
|
|
8361
|
+
const next = {};
|
|
8362
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
8363
|
+
if (key === "additionalProperties") {
|
|
8364
|
+
continue;
|
|
8365
|
+
}
|
|
8366
|
+
if (key === "type" && typeof value === "string") {
|
|
8367
|
+
next[key] = value.toUpperCase();
|
|
8368
|
+
continue;
|
|
8369
|
+
}
|
|
8370
|
+
if (Array.isArray(value)) {
|
|
8371
|
+
next[key] = value.map((item) => item && typeof item === "object" ? toGeminiSchema(item) : item);
|
|
8372
|
+
continue;
|
|
8373
|
+
}
|
|
8374
|
+
if (value && typeof value === "object") {
|
|
8375
|
+
next[key] = toGeminiSchema(value);
|
|
8376
|
+
continue;
|
|
8377
|
+
}
|
|
8378
|
+
next[key] = value;
|
|
8379
|
+
}
|
|
8380
|
+
return next;
|
|
8381
|
+
};
|
|
8382
|
+
var messageToGeminiContent = (message) => {
|
|
8383
|
+
if (message.role === "system") {
|
|
8384
|
+
return;
|
|
8385
|
+
}
|
|
8386
|
+
if (message.role === "tool") {
|
|
8387
|
+
return {
|
|
8388
|
+
parts: [
|
|
8389
|
+
{
|
|
8390
|
+
functionResponse: {
|
|
8391
|
+
id: message.toolCallId,
|
|
8392
|
+
name: message.name ?? "tool",
|
|
8393
|
+
response: {
|
|
8394
|
+
result: parseJSONValue(message.content)
|
|
8395
|
+
}
|
|
8396
|
+
}
|
|
8397
|
+
}
|
|
8398
|
+
],
|
|
8399
|
+
role: "user"
|
|
8400
|
+
};
|
|
8401
|
+
}
|
|
8402
|
+
const toolCalls = getMessageToolCalls(message);
|
|
8403
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
8404
|
+
return {
|
|
8405
|
+
parts: [
|
|
8406
|
+
...message.content ? [
|
|
8407
|
+
{
|
|
8408
|
+
text: message.content
|
|
8409
|
+
}
|
|
8410
|
+
] : [],
|
|
8411
|
+
...toolCalls.map((toolCall) => ({
|
|
8412
|
+
functionCall: {
|
|
8413
|
+
args: toolCall.args,
|
|
8414
|
+
id: toolCall.id,
|
|
8415
|
+
name: toolCall.name
|
|
8416
|
+
}
|
|
8417
|
+
}))
|
|
8418
|
+
],
|
|
8419
|
+
role: "model"
|
|
8420
|
+
};
|
|
8421
|
+
}
|
|
8422
|
+
return {
|
|
8423
|
+
parts: [
|
|
8424
|
+
{
|
|
8425
|
+
text: message.content
|
|
8426
|
+
}
|
|
8427
|
+
],
|
|
8428
|
+
role: message.role === "assistant" ? "model" : "user"
|
|
8429
|
+
};
|
|
8430
|
+
};
|
|
8431
|
+
var extractText = (response) => {
|
|
8432
|
+
if (typeof response.output_text === "string") {
|
|
8433
|
+
return response.output_text;
|
|
8434
|
+
}
|
|
8435
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
8436
|
+
for (const item of output) {
|
|
8437
|
+
if (!item || typeof item !== "object") {
|
|
8438
|
+
continue;
|
|
8439
|
+
}
|
|
8440
|
+
const record = item;
|
|
8441
|
+
const content = Array.isArray(record.content) ? record.content : [];
|
|
8442
|
+
for (const contentItem of content) {
|
|
8443
|
+
if (!contentItem || typeof contentItem !== "object") {
|
|
8444
|
+
continue;
|
|
8445
|
+
}
|
|
8446
|
+
const contentRecord = contentItem;
|
|
8447
|
+
if (typeof contentRecord.text === "string") {
|
|
8448
|
+
return contentRecord.text;
|
|
8449
|
+
}
|
|
8450
|
+
}
|
|
8451
|
+
}
|
|
8452
|
+
return "";
|
|
8453
|
+
};
|
|
8454
|
+
var extractToolCalls = (response) => {
|
|
8455
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
8456
|
+
const toolCalls = [];
|
|
8457
|
+
for (const item of output) {
|
|
8458
|
+
if (!item || typeof item !== "object") {
|
|
8459
|
+
continue;
|
|
8460
|
+
}
|
|
8461
|
+
const record = item;
|
|
8462
|
+
if (record.type !== "function_call" || typeof record.name !== "string") {
|
|
8463
|
+
continue;
|
|
8464
|
+
}
|
|
8465
|
+
const args = typeof record.arguments === "string" ? parseJSON(record.arguments) : {};
|
|
8466
|
+
toolCalls.push({
|
|
8467
|
+
args,
|
|
8468
|
+
id: typeof record.call_id === "string" ? record.call_id : typeof record.id === "string" ? record.id : undefined,
|
|
8469
|
+
name: record.name
|
|
8470
|
+
});
|
|
8471
|
+
}
|
|
8472
|
+
return toolCalls;
|
|
8473
|
+
};
|
|
8474
|
+
var createOpenAIVoiceAssistantModel = (options) => {
|
|
8475
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8476
|
+
const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
|
|
8477
|
+
const model = options.model ?? "gpt-4.1-mini";
|
|
8478
|
+
return {
|
|
8479
|
+
generate: async (input) => {
|
|
8480
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
|
|
8481
|
+
body: JSON.stringify({
|
|
8482
|
+
input: messagesToOpenAIInput(input.messages),
|
|
8483
|
+
instructions: [
|
|
8484
|
+
input.system,
|
|
8485
|
+
"Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools."
|
|
8486
|
+
].filter(Boolean).join(`
|
|
8487
|
+
|
|
8488
|
+
`),
|
|
8489
|
+
max_output_tokens: options.maxOutputTokens,
|
|
8490
|
+
model,
|
|
8491
|
+
temperature: options.temperature,
|
|
8492
|
+
text: {
|
|
8493
|
+
format: {
|
|
8494
|
+
name: "voice_route_result",
|
|
8495
|
+
schema: OUTPUT_SCHEMA,
|
|
8496
|
+
strict: false,
|
|
8497
|
+
type: "json_schema"
|
|
8498
|
+
}
|
|
8499
|
+
},
|
|
8500
|
+
tool_choice: input.tools.length ? "auto" : "none",
|
|
8501
|
+
tools: input.tools.map((tool) => ({
|
|
8502
|
+
description: tool.description,
|
|
8503
|
+
name: tool.name,
|
|
8504
|
+
parameters: tool.parameters ?? {
|
|
8505
|
+
additionalProperties: true,
|
|
8506
|
+
type: "object"
|
|
8507
|
+
},
|
|
8508
|
+
strict: false,
|
|
8509
|
+
type: "function"
|
|
8510
|
+
}))
|
|
8511
|
+
}),
|
|
8512
|
+
headers: {
|
|
8513
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
8514
|
+
"content-type": "application/json"
|
|
8515
|
+
},
|
|
8516
|
+
method: "POST"
|
|
8517
|
+
});
|
|
8518
|
+
if (!response.ok) {
|
|
8519
|
+
throw createHTTPError("OpenAI", response);
|
|
6789
8520
|
}
|
|
6790
|
-
|
|
8521
|
+
const body = await response.json();
|
|
8522
|
+
if (body.usage && typeof body.usage === "object") {
|
|
8523
|
+
await options.onUsage?.(body.usage);
|
|
8524
|
+
}
|
|
8525
|
+
const toolCalls = extractToolCalls(body);
|
|
8526
|
+
if (toolCalls.length) {
|
|
8527
|
+
return {
|
|
8528
|
+
toolCalls
|
|
8529
|
+
};
|
|
8530
|
+
}
|
|
8531
|
+
return normalizeRouteOutput(parseJSON(extractText(body)));
|
|
6791
8532
|
}
|
|
6792
8533
|
};
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
8534
|
+
};
|
|
8535
|
+
var extractAnthropicText = (response) => {
|
|
8536
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
8537
|
+
return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
|
|
8538
|
+
`);
|
|
8539
|
+
};
|
|
8540
|
+
var extractAnthropicToolCalls = (response) => {
|
|
8541
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
8542
|
+
const toolCalls = [];
|
|
8543
|
+
for (const item of content) {
|
|
8544
|
+
if (!item || typeof item !== "object") {
|
|
8545
|
+
continue;
|
|
8546
|
+
}
|
|
8547
|
+
const record = item;
|
|
8548
|
+
if (record.type !== "tool_use" || typeof record.name !== "string") {
|
|
8549
|
+
continue;
|
|
8550
|
+
}
|
|
8551
|
+
toolCalls.push({
|
|
8552
|
+
args: record.input && typeof record.input === "object" ? record.input : {},
|
|
8553
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
8554
|
+
name: record.name
|
|
6801
8555
|
});
|
|
6802
|
-
}
|
|
6803
|
-
return
|
|
8556
|
+
}
|
|
8557
|
+
return toolCalls;
|
|
6804
8558
|
};
|
|
6805
|
-
var
|
|
6806
|
-
const
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
8559
|
+
var createAnthropicVoiceAssistantModel = (options) => {
|
|
8560
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8561
|
+
const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
|
|
8562
|
+
const model = options.model ?? "claude-sonnet-4-5";
|
|
8563
|
+
return {
|
|
8564
|
+
generate: async (input) => {
|
|
8565
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
|
|
8566
|
+
body: JSON.stringify({
|
|
8567
|
+
max_tokens: options.maxOutputTokens ?? 1024,
|
|
8568
|
+
messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
|
|
8569
|
+
model,
|
|
8570
|
+
system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
8571
|
+
|
|
8572
|
+
`),
|
|
8573
|
+
temperature: options.temperature,
|
|
8574
|
+
tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
|
|
8575
|
+
tools: input.tools.map((tool) => ({
|
|
8576
|
+
description: tool.description,
|
|
8577
|
+
input_schema: tool.parameters ?? {
|
|
8578
|
+
additionalProperties: true,
|
|
8579
|
+
type: "object"
|
|
8580
|
+
},
|
|
8581
|
+
name: tool.name
|
|
8582
|
+
}))
|
|
8583
|
+
}),
|
|
8584
|
+
headers: {
|
|
8585
|
+
"anthropic-version": options.version ?? "2023-06-01",
|
|
8586
|
+
"content-type": "application/json",
|
|
8587
|
+
"x-api-key": options.apiKey
|
|
8588
|
+
},
|
|
8589
|
+
method: "POST"
|
|
8590
|
+
});
|
|
8591
|
+
if (!response.ok) {
|
|
8592
|
+
throw createHTTPError("Anthropic", response);
|
|
6813
8593
|
}
|
|
6814
|
-
|
|
8594
|
+
const body = await response.json();
|
|
8595
|
+
if (body.usage && typeof body.usage === "object") {
|
|
8596
|
+
await options.onUsage?.(body.usage);
|
|
8597
|
+
}
|
|
8598
|
+
const toolCalls = extractAnthropicToolCalls(body);
|
|
8599
|
+
if (toolCalls.length) {
|
|
8600
|
+
return {
|
|
8601
|
+
assistantText: extractAnthropicText(body) || undefined,
|
|
8602
|
+
toolCalls
|
|
8603
|
+
};
|
|
8604
|
+
}
|
|
8605
|
+
return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
|
|
6815
8606
|
}
|
|
6816
8607
|
};
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
}
|
|
6828
|
-
const
|
|
6829
|
-
|
|
6830
|
-
|
|
8608
|
+
};
|
|
8609
|
+
var extractGeminiCandidateParts = (response) => {
|
|
8610
|
+
const candidates = Array.isArray(response.candidates) ? response.candidates : [];
|
|
8611
|
+
const first = candidates[0];
|
|
8612
|
+
if (!first || typeof first !== "object") {
|
|
8613
|
+
return [];
|
|
8614
|
+
}
|
|
8615
|
+
const content = first.content;
|
|
8616
|
+
if (!content || typeof content !== "object") {
|
|
8617
|
+
return [];
|
|
8618
|
+
}
|
|
8619
|
+
const parts = content.parts;
|
|
8620
|
+
return Array.isArray(parts) ? parts : [];
|
|
8621
|
+
};
|
|
8622
|
+
var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
8623
|
+
`);
|
|
8624
|
+
var extractGeminiToolCalls = (response) => {
|
|
8625
|
+
const toolCalls = [];
|
|
8626
|
+
for (const part of extractGeminiCandidateParts(response)) {
|
|
8627
|
+
if (!part || typeof part !== "object") {
|
|
8628
|
+
continue;
|
|
8629
|
+
}
|
|
8630
|
+
const functionCall = part.functionCall;
|
|
8631
|
+
if (!functionCall || typeof functionCall !== "object") {
|
|
8632
|
+
continue;
|
|
8633
|
+
}
|
|
8634
|
+
const record = functionCall;
|
|
8635
|
+
if (typeof record.name !== "string") {
|
|
8636
|
+
continue;
|
|
8637
|
+
}
|
|
8638
|
+
toolCalls.push({
|
|
8639
|
+
args: record.args && typeof record.args === "object" ? record.args : {},
|
|
8640
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
8641
|
+
name: record.name
|
|
6831
8642
|
});
|
|
8643
|
+
}
|
|
8644
|
+
return toolCalls;
|
|
8645
|
+
};
|
|
8646
|
+
var createGeminiVoiceAssistantModel = (options) => {
|
|
8647
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8648
|
+
const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
8649
|
+
const model = options.model ?? "gemini-2.5-flash";
|
|
8650
|
+
const maxRetries = Math.max(0, options.maxRetries ?? 2);
|
|
8651
|
+
return {
|
|
8652
|
+
generate: async (input) => {
|
|
8653
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
|
|
8654
|
+
let response;
|
|
8655
|
+
for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
|
|
8656
|
+
response = await fetchImpl(endpoint, {
|
|
8657
|
+
body: JSON.stringify({
|
|
8658
|
+
contents: input.messages.map(messageToGeminiContent).filter(Boolean),
|
|
8659
|
+
generationConfig: {
|
|
8660
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
8661
|
+
...input.tools.length ? {} : {
|
|
8662
|
+
responseMimeType: "application/json",
|
|
8663
|
+
responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
|
|
8664
|
+
},
|
|
8665
|
+
temperature: options.temperature
|
|
8666
|
+
},
|
|
8667
|
+
systemInstruction: {
|
|
8668
|
+
parts: [
|
|
8669
|
+
{
|
|
8670
|
+
text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
8671
|
+
|
|
8672
|
+
`)
|
|
8673
|
+
}
|
|
8674
|
+
]
|
|
8675
|
+
},
|
|
8676
|
+
tools: input.tools.length ? [
|
|
8677
|
+
{
|
|
8678
|
+
functionDeclarations: input.tools.map((tool) => ({
|
|
8679
|
+
description: tool.description,
|
|
8680
|
+
name: tool.name,
|
|
8681
|
+
parameters: toGeminiSchema(tool.parameters ?? {
|
|
8682
|
+
additionalProperties: true,
|
|
8683
|
+
type: "object"
|
|
8684
|
+
})
|
|
8685
|
+
}))
|
|
8686
|
+
}
|
|
8687
|
+
] : undefined
|
|
8688
|
+
}),
|
|
8689
|
+
headers: {
|
|
8690
|
+
"content-type": "application/json"
|
|
8691
|
+
},
|
|
8692
|
+
method: "POST"
|
|
8693
|
+
});
|
|
8694
|
+
if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
|
|
8695
|
+
break;
|
|
8696
|
+
}
|
|
8697
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
8698
|
+
await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
|
|
8699
|
+
}
|
|
8700
|
+
if (!response) {
|
|
8701
|
+
throw new Error("Gemini voice assistant model failed: no response");
|
|
8702
|
+
}
|
|
8703
|
+
if (!response.ok) {
|
|
8704
|
+
throw createHTTPError("Gemini", response);
|
|
8705
|
+
}
|
|
8706
|
+
const body = await response.json();
|
|
8707
|
+
if (body.usageMetadata && typeof body.usageMetadata === "object") {
|
|
8708
|
+
await options.onUsage?.(body.usageMetadata);
|
|
8709
|
+
}
|
|
8710
|
+
const toolCalls = extractGeminiToolCalls(body);
|
|
8711
|
+
if (toolCalls.length) {
|
|
8712
|
+
return {
|
|
8713
|
+
assistantText: extractGeminiText(body) || undefined,
|
|
8714
|
+
toolCalls
|
|
8715
|
+
};
|
|
8716
|
+
}
|
|
8717
|
+
return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
|
|
8718
|
+
}
|
|
6832
8719
|
};
|
|
6833
|
-
return { get, list, remove, set };
|
|
6834
8720
|
};
|
|
6835
|
-
var createVoiceFileRuntimeStorage = (options) => ({
|
|
6836
|
-
events: createVoiceFileIntegrationEventStore({
|
|
6837
|
-
...options,
|
|
6838
|
-
directory: join(options.directory, "events")
|
|
6839
|
-
}),
|
|
6840
|
-
externalObjects: createVoiceFileExternalObjectMapStore({
|
|
6841
|
-
...options,
|
|
6842
|
-
directory: join(options.directory, "external-objects")
|
|
6843
|
-
}),
|
|
6844
|
-
reviews: createVoiceFileReviewStore({
|
|
6845
|
-
...options,
|
|
6846
|
-
directory: join(options.directory, "reviews")
|
|
6847
|
-
}),
|
|
6848
|
-
session: createVoiceFileSessionStore({
|
|
6849
|
-
...options,
|
|
6850
|
-
directory: join(options.directory, "sessions")
|
|
6851
|
-
}),
|
|
6852
|
-
tasks: createVoiceFileTaskStore({
|
|
6853
|
-
...options,
|
|
6854
|
-
directory: join(options.directory, "tasks")
|
|
6855
|
-
}),
|
|
6856
|
-
traceDeliveries: createVoiceFileTraceSinkDeliveryStore({
|
|
6857
|
-
...options,
|
|
6858
|
-
directory: join(options.directory, "trace-deliveries")
|
|
6859
|
-
}),
|
|
6860
|
-
traces: createVoiceFileTraceEventStore({
|
|
6861
|
-
...options,
|
|
6862
|
-
directory: join(options.directory, "traces")
|
|
6863
|
-
})
|
|
6864
|
-
});
|
|
6865
|
-
var createStoredVoiceCallReviewArtifact = (id, artifact) => withVoiceCallReviewId(id, artifact);
|
|
6866
|
-
var createStoredVoiceOpsTask = (id, task) => withVoiceOpsTaskId(id, task);
|
|
6867
|
-
var createStoredVoiceIntegrationEvent = (id, event) => withVoiceIntegrationEventId(id, event);
|
|
6868
|
-
var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectMap({
|
|
6869
|
-
at: mapping.at,
|
|
6870
|
-
externalId: mapping.externalId,
|
|
6871
|
-
provider: mapping.provider,
|
|
6872
|
-
sinkId: mapping.sinkId,
|
|
6873
|
-
sourceId: mapping.sourceId,
|
|
6874
|
-
sourceType: mapping.sourceType
|
|
6875
|
-
});
|
|
6876
8721
|
// src/sqliteStore.ts
|
|
6877
8722
|
import { Database } from "bun:sqlite";
|
|
6878
8723
|
var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
|
|
@@ -7354,6 +9199,361 @@ var createVoiceMemoryStore = () => {
|
|
|
7354
9199
|
};
|
|
7355
9200
|
return { get, getOrCreate, list, remove, set };
|
|
7356
9201
|
};
|
|
9202
|
+
// src/opsWebhook.ts
|
|
9203
|
+
import { Elysia as Elysia5 } from "elysia";
|
|
9204
|
+
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
9205
|
+
var signVoiceOpsWebhookBody = async (input) => {
|
|
9206
|
+
const encoder = new TextEncoder;
|
|
9207
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
9208
|
+
hash: "SHA-256",
|
|
9209
|
+
name: "HMAC"
|
|
9210
|
+
}, false, ["sign"]);
|
|
9211
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
|
|
9212
|
+
return `sha256=${toHex5(new Uint8Array(signature))}`;
|
|
9213
|
+
};
|
|
9214
|
+
var timingSafeEqual = (left, right) => {
|
|
9215
|
+
const encoder = new TextEncoder;
|
|
9216
|
+
const leftBytes = encoder.encode(left);
|
|
9217
|
+
const rightBytes = encoder.encode(right);
|
|
9218
|
+
if (leftBytes.length !== rightBytes.length) {
|
|
9219
|
+
return false;
|
|
9220
|
+
}
|
|
9221
|
+
let diff = 0;
|
|
9222
|
+
for (let index = 0;index < leftBytes.length; index += 1) {
|
|
9223
|
+
diff |= leftBytes[index] ^ rightBytes[index];
|
|
9224
|
+
}
|
|
9225
|
+
return diff === 0;
|
|
9226
|
+
};
|
|
9227
|
+
var resolveWebhookLink = async (resolver, event) => {
|
|
9228
|
+
if (typeof resolver === "function") {
|
|
9229
|
+
return resolver({
|
|
9230
|
+
event
|
|
9231
|
+
});
|
|
9232
|
+
}
|
|
9233
|
+
return resolver;
|
|
9234
|
+
};
|
|
9235
|
+
var joinBaseUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
9236
|
+
var asString = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9237
|
+
var buildVoiceOpsWebhookEntity = (event) => ({
|
|
9238
|
+
disposition: asString(event.payload.disposition),
|
|
9239
|
+
outcome: asString(event.payload.outcome),
|
|
9240
|
+
priority: asString(event.payload.priority),
|
|
9241
|
+
queue: asString(event.payload.queue),
|
|
9242
|
+
reviewId: asString(event.payload.reviewId),
|
|
9243
|
+
scenarioId: asString(event.payload.scenarioId),
|
|
9244
|
+
sessionId: asString(event.payload.sessionId),
|
|
9245
|
+
status: asString(event.payload.status),
|
|
9246
|
+
target: asString(event.payload.target),
|
|
9247
|
+
taskId: asString(event.payload.taskId)
|
|
9248
|
+
});
|
|
9249
|
+
var createVoiceOpsWebhookEnvelope = async (input) => {
|
|
9250
|
+
const entity = buildVoiceOpsWebhookEntity(input.event);
|
|
9251
|
+
const replayHref = await resolveWebhookLink(input.replayHref, input.event) ?? (input.baseUrl && entity.sessionId ? joinBaseUrl(input.baseUrl, `/api/voice-sessions/${encodeURIComponent(entity.sessionId)}/replay`) : undefined);
|
|
9252
|
+
const links = {
|
|
9253
|
+
event: await resolveWebhookLink(input.eventHref, input.event),
|
|
9254
|
+
replay: replayHref,
|
|
9255
|
+
review: await resolveWebhookLink(input.reviewHref, input.event),
|
|
9256
|
+
task: await resolveWebhookLink(input.taskHref, input.event)
|
|
9257
|
+
};
|
|
9258
|
+
return {
|
|
9259
|
+
entity,
|
|
9260
|
+
event: {
|
|
9261
|
+
createdAt: input.event.createdAt,
|
|
9262
|
+
id: input.event.id,
|
|
9263
|
+
payload: input.event.payload,
|
|
9264
|
+
type: input.event.type
|
|
9265
|
+
},
|
|
9266
|
+
links: links.event || links.replay || links.review || links.task ? links : undefined,
|
|
9267
|
+
schemaVersion: 1,
|
|
9268
|
+
source: "absolutejs-voice"
|
|
9269
|
+
};
|
|
9270
|
+
};
|
|
9271
|
+
var createVoiceOpsWebhookSink = (options) => createVoiceIntegrationHTTPSink({
|
|
9272
|
+
...options,
|
|
9273
|
+
body: ({ event }) => createVoiceOpsWebhookEnvelope({
|
|
9274
|
+
baseUrl: options.baseUrl,
|
|
9275
|
+
event,
|
|
9276
|
+
eventHref: options.eventHref,
|
|
9277
|
+
replayHref: options.replayHref,
|
|
9278
|
+
reviewHref: options.reviewHref,
|
|
9279
|
+
taskHref: options.taskHref
|
|
9280
|
+
}),
|
|
9281
|
+
kind: options.kind ?? "ops-webhook"
|
|
9282
|
+
});
|
|
9283
|
+
var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
9284
|
+
if (!input.secret) {
|
|
9285
|
+
return {
|
|
9286
|
+
ok: false,
|
|
9287
|
+
reason: "missing-secret"
|
|
9288
|
+
};
|
|
9289
|
+
}
|
|
9290
|
+
if (!input.signature) {
|
|
9291
|
+
return {
|
|
9292
|
+
ok: false,
|
|
9293
|
+
reason: "missing-signature"
|
|
9294
|
+
};
|
|
9295
|
+
}
|
|
9296
|
+
if (!input.signature.startsWith("sha256=")) {
|
|
9297
|
+
return {
|
|
9298
|
+
ok: false,
|
|
9299
|
+
reason: "unsupported-algorithm"
|
|
9300
|
+
};
|
|
9301
|
+
}
|
|
9302
|
+
if (!input.timestamp) {
|
|
9303
|
+
return {
|
|
9304
|
+
ok: false,
|
|
9305
|
+
reason: "missing-timestamp"
|
|
9306
|
+
};
|
|
9307
|
+
}
|
|
9308
|
+
const timestampMs = Number(input.timestamp);
|
|
9309
|
+
const toleranceMs = Math.max(0, input.toleranceMs ?? 5 * 60 * 1000);
|
|
9310
|
+
if (!Number.isFinite(timestampMs) || toleranceMs > 0 && Math.abs((input.now ?? Date.now()) - timestampMs) > toleranceMs) {
|
|
9311
|
+
return {
|
|
9312
|
+
ok: false,
|
|
9313
|
+
reason: "stale-timestamp"
|
|
9314
|
+
};
|
|
9315
|
+
}
|
|
9316
|
+
const expected = await signVoiceOpsWebhookBody({
|
|
9317
|
+
body: input.body,
|
|
9318
|
+
secret: input.secret,
|
|
9319
|
+
timestamp: input.timestamp
|
|
9320
|
+
});
|
|
9321
|
+
if (!timingSafeEqual(expected, input.signature)) {
|
|
9322
|
+
return {
|
|
9323
|
+
ok: false,
|
|
9324
|
+
reason: "invalid-signature"
|
|
9325
|
+
};
|
|
9326
|
+
}
|
|
9327
|
+
return {
|
|
9328
|
+
ok: true
|
|
9329
|
+
};
|
|
9330
|
+
};
|
|
9331
|
+
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
9332
|
+
const path = options.path ?? "/api/voice-ops/webhook";
|
|
9333
|
+
return new Elysia5().post(path, async ({ body, request, set }) => {
|
|
9334
|
+
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
9335
|
+
if (options.signingSecret) {
|
|
9336
|
+
const verification = await verifyVoiceOpsWebhookSignature({
|
|
9337
|
+
body: bodyText,
|
|
9338
|
+
secret: options.signingSecret,
|
|
9339
|
+
signature: request.headers.get("x-absolutejs-signature"),
|
|
9340
|
+
timestamp: request.headers.get("x-absolutejs-timestamp"),
|
|
9341
|
+
toleranceMs: options.toleranceMs
|
|
9342
|
+
});
|
|
9343
|
+
if (!verification.ok) {
|
|
9344
|
+
set.status = 401;
|
|
9345
|
+
return {
|
|
9346
|
+
ok: false,
|
|
9347
|
+
reason: verification.reason
|
|
9348
|
+
};
|
|
9349
|
+
}
|
|
9350
|
+
}
|
|
9351
|
+
const envelope = JSON.parse(bodyText);
|
|
9352
|
+
await options.onEnvelope?.({
|
|
9353
|
+
envelope,
|
|
9354
|
+
request
|
|
9355
|
+
});
|
|
9356
|
+
return {
|
|
9357
|
+
eventId: envelope.event?.id,
|
|
9358
|
+
ok: true,
|
|
9359
|
+
type: envelope.event?.type
|
|
9360
|
+
};
|
|
9361
|
+
}, {
|
|
9362
|
+
parse: "text"
|
|
9363
|
+
});
|
|
9364
|
+
};
|
|
9365
|
+
// src/handoffHealth.ts
|
|
9366
|
+
import { Elysia as Elysia6 } from "elysia";
|
|
9367
|
+
var escapeHtml7 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9368
|
+
var getString4 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9369
|
+
var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
|
|
9370
|
+
var increment3 = (record, key) => {
|
|
9371
|
+
record[key] = (record[key] ?? 0) + 1;
|
|
9372
|
+
};
|
|
9373
|
+
var normalizeDelivery = (adapterId, value) => {
|
|
9374
|
+
const record = value && typeof value === "object" ? value : {};
|
|
9375
|
+
return {
|
|
9376
|
+
adapterId: getString4(record.adapterId) ?? adapterId,
|
|
9377
|
+
adapterKind: getString4(record.adapterKind),
|
|
9378
|
+
deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
|
|
9379
|
+
deliveredTo: getString4(record.deliveredTo),
|
|
9380
|
+
error: getString4(record.error),
|
|
9381
|
+
status: isStatus(record.status) ? record.status : "failed"
|
|
9382
|
+
};
|
|
9383
|
+
};
|
|
9384
|
+
var normalizeDeliveries = (payload) => {
|
|
9385
|
+
const deliveries = payload.deliveries;
|
|
9386
|
+
if (!deliveries || typeof deliveries !== "object") {
|
|
9387
|
+
return [];
|
|
9388
|
+
}
|
|
9389
|
+
return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
|
|
9390
|
+
};
|
|
9391
|
+
var resolveReplayHref = (event, replayHref) => {
|
|
9392
|
+
if (replayHref === false) {
|
|
9393
|
+
return;
|
|
9394
|
+
}
|
|
9395
|
+
if (typeof replayHref === "function") {
|
|
9396
|
+
return replayHref(event);
|
|
9397
|
+
}
|
|
9398
|
+
return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
|
|
9399
|
+
};
|
|
9400
|
+
var summarizeVoiceHandoffHealth = async (options = {}) => {
|
|
9401
|
+
const sourceEvents = options.events ?? await options.store?.list() ?? [];
|
|
9402
|
+
const search = options.q?.trim().toLowerCase();
|
|
9403
|
+
const byAction = {};
|
|
9404
|
+
const byAdapter = {};
|
|
9405
|
+
const byStatus = {
|
|
9406
|
+
delivered: 0,
|
|
9407
|
+
failed: 0,
|
|
9408
|
+
skipped: 0
|
|
9409
|
+
};
|
|
9410
|
+
const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
|
|
9411
|
+
const status = isStatus(event.payload.status) ? event.payload.status : "failed";
|
|
9412
|
+
const deliveries = normalizeDeliveries(event.payload);
|
|
9413
|
+
const item = {
|
|
9414
|
+
action: getString4(event.payload.action),
|
|
9415
|
+
at: event.at,
|
|
9416
|
+
deliveries,
|
|
9417
|
+
reason: getString4(event.payload.reason),
|
|
9418
|
+
sessionId: event.sessionId,
|
|
9419
|
+
status,
|
|
9420
|
+
target: getString4(event.payload.target)
|
|
9421
|
+
};
|
|
9422
|
+
return {
|
|
9423
|
+
...item,
|
|
9424
|
+
replayHref: resolveReplayHref(item, options.replayHref)
|
|
9425
|
+
};
|
|
9426
|
+
}).filter((event) => {
|
|
9427
|
+
if (options.status && options.status !== "all" && event.status !== options.status) {
|
|
9428
|
+
return false;
|
|
9429
|
+
}
|
|
9430
|
+
if (!search) {
|
|
9431
|
+
return true;
|
|
9432
|
+
}
|
|
9433
|
+
return [
|
|
9434
|
+
event.action,
|
|
9435
|
+
event.reason,
|
|
9436
|
+
event.sessionId,
|
|
9437
|
+
event.status,
|
|
9438
|
+
event.target,
|
|
9439
|
+
...event.deliveries.flatMap((delivery) => [
|
|
9440
|
+
delivery.adapterId,
|
|
9441
|
+
delivery.adapterKind,
|
|
9442
|
+
delivery.deliveredTo,
|
|
9443
|
+
delivery.error,
|
|
9444
|
+
delivery.status
|
|
9445
|
+
])
|
|
9446
|
+
].some((value) => value?.toLowerCase().includes(search));
|
|
9447
|
+
}).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
|
|
9448
|
+
for (const event of events) {
|
|
9449
|
+
byStatus[event.status] += 1;
|
|
9450
|
+
if (event.action) {
|
|
9451
|
+
increment3(byAction, event.action);
|
|
9452
|
+
}
|
|
9453
|
+
for (const delivery of event.deliveries) {
|
|
9454
|
+
byAdapter[delivery.adapterId] ??= {
|
|
9455
|
+
delivered: 0,
|
|
9456
|
+
failed: 0,
|
|
9457
|
+
skipped: 0
|
|
9458
|
+
};
|
|
9459
|
+
byAdapter[delivery.adapterId][delivery.status] += 1;
|
|
9460
|
+
}
|
|
9461
|
+
}
|
|
9462
|
+
return {
|
|
9463
|
+
byAction,
|
|
9464
|
+
byAdapter,
|
|
9465
|
+
byStatus,
|
|
9466
|
+
events,
|
|
9467
|
+
failed: byStatus.failed,
|
|
9468
|
+
total: events.length
|
|
9469
|
+
};
|
|
9470
|
+
};
|
|
9471
|
+
var renderMetricGrid = (summary) => [
|
|
9472
|
+
'<section class="voice-handoff-health-grid">',
|
|
9473
|
+
`<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
|
|
9474
|
+
`<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
|
|
9475
|
+
`<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
|
|
9476
|
+
`<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
|
|
9477
|
+
"</section>"
|
|
9478
|
+
].join("");
|
|
9479
|
+
var renderActionSummary = (summary) => {
|
|
9480
|
+
const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
|
|
9481
|
+
const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
|
|
9482
|
+
return [
|
|
9483
|
+
'<section class="voice-handoff-health-columns">',
|
|
9484
|
+
"<article><h3>Actions</h3>",
|
|
9485
|
+
actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml7(action)}: ${String(count)}</li>`).join("")}</ul>`,
|
|
9486
|
+
"</article>",
|
|
9487
|
+
"<article><h3>Adapters</h3>",
|
|
9488
|
+
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>`,
|
|
9489
|
+
"</article>",
|
|
9490
|
+
"</section>"
|
|
9491
|
+
].join("");
|
|
9492
|
+
};
|
|
9493
|
+
var renderVoiceHandoffHealthHTML = (summary) => [
|
|
9494
|
+
'<div class="voice-handoff-health">',
|
|
9495
|
+
renderMetricGrid(summary),
|
|
9496
|
+
renderActionSummary(summary),
|
|
9497
|
+
"<section>",
|
|
9498
|
+
"<h3>Recent Handoffs</h3>",
|
|
9499
|
+
summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
|
|
9500
|
+
'<div class="voice-handoff-health-events">',
|
|
9501
|
+
...summary.events.map((event) => [
|
|
9502
|
+
`<article class="${escapeHtml7(event.status)}">`,
|
|
9503
|
+
'<div class="voice-handoff-health-event-header">',
|
|
9504
|
+
`<strong>${escapeHtml7(event.action ?? "handoff")}</strong>`,
|
|
9505
|
+
`<span>${escapeHtml7(event.status)}</span>`,
|
|
9506
|
+
"</div>",
|
|
9507
|
+
`<p><small>${escapeHtml7(event.sessionId)}</small></p>`,
|
|
9508
|
+
event.target ? `<p>Target: ${escapeHtml7(event.target)}</p>` : "",
|
|
9509
|
+
event.reason ? `<p>Reason: ${escapeHtml7(event.reason)}</p>` : "",
|
|
9510
|
+
event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
|
|
9511
|
+
"<li>",
|
|
9512
|
+
`${escapeHtml7(delivery.adapterId)}: ${escapeHtml7(delivery.status)}`,
|
|
9513
|
+
delivery.deliveredTo ? ` to ${escapeHtml7(delivery.deliveredTo)}` : "",
|
|
9514
|
+
delivery.error ? ` (${escapeHtml7(delivery.error)})` : "",
|
|
9515
|
+
"</li>"
|
|
9516
|
+
].join("")).join("")}</ul>` : "",
|
|
9517
|
+
event.replayHref ? `<p><a href="${escapeHtml7(event.replayHref)}">Open replay</a></p>` : "",
|
|
9518
|
+
"</article>"
|
|
9519
|
+
].join("")),
|
|
9520
|
+
"</div>"
|
|
9521
|
+
].join(""),
|
|
9522
|
+
"</section>",
|
|
9523
|
+
"</div>"
|
|
9524
|
+
].join("");
|
|
9525
|
+
var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
|
|
9526
|
+
...options,
|
|
9527
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
9528
|
+
q: query?.q ?? options.q,
|
|
9529
|
+
status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
|
|
9530
|
+
});
|
|
9531
|
+
var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
|
|
9532
|
+
const summary = await summarizeVoiceHandoffHealth({
|
|
9533
|
+
...options,
|
|
9534
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
9535
|
+
q: query?.q ?? options.q,
|
|
9536
|
+
status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
|
|
9537
|
+
});
|
|
9538
|
+
const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
|
|
9539
|
+
return new Response(body, {
|
|
9540
|
+
headers: {
|
|
9541
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9542
|
+
...options.headers
|
|
9543
|
+
}
|
|
9544
|
+
});
|
|
9545
|
+
};
|
|
9546
|
+
var createVoiceHandoffHealthRoutes = (options = {}) => {
|
|
9547
|
+
const path = options.path ?? "/api/voice-handoffs";
|
|
9548
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
9549
|
+
const routes = new Elysia6({
|
|
9550
|
+
name: options.name ?? "absolutejs-voice-handoff-health"
|
|
9551
|
+
}).get(path, createVoiceHandoffHealthJSONHandler(options));
|
|
9552
|
+
if (htmlPath) {
|
|
9553
|
+
routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
|
|
9554
|
+
}
|
|
9555
|
+
return routes;
|
|
9556
|
+
};
|
|
7357
9557
|
// src/queue.ts
|
|
7358
9558
|
var releaseLeaseScript = `
|
|
7359
9559
|
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
|
@@ -8070,10 +10270,10 @@ var createVoiceOpsTaskProcessorWorker = (options) => ({
|
|
|
8070
10270
|
result.completed += 1;
|
|
8071
10271
|
} catch (error) {
|
|
8072
10272
|
await options.onError?.(error, task);
|
|
8073
|
-
const
|
|
10273
|
+
const errorMessage2 = error instanceof Error ? error.message : String(error);
|
|
8074
10274
|
const failedTask = failVoiceOpsTask(task, {
|
|
8075
10275
|
actor: task.claimedBy ?? "ops-worker",
|
|
8076
|
-
error:
|
|
10276
|
+
error: errorMessage2
|
|
8077
10277
|
});
|
|
8078
10278
|
if (shouldDeadLetterTask(failedTask, options.maxFailures)) {
|
|
8079
10279
|
const deadLetterTask = deadLetterVoiceOpsTask(failedTask, {
|
|
@@ -8886,7 +11086,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
|
|
|
8886
11086
|
import { Buffer as Buffer2 } from "buffer";
|
|
8887
11087
|
var TWILIO_MULAW_SAMPLE_RATE = 8000;
|
|
8888
11088
|
var VOICE_PCM_SAMPLE_RATE = 16000;
|
|
8889
|
-
var
|
|
11089
|
+
var escapeXml2 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
8890
11090
|
var normalizeOnTurn2 = (handler) => {
|
|
8891
11091
|
if (handler.length > 1) {
|
|
8892
11092
|
const directHandler = handler;
|
|
@@ -9082,8 +11282,8 @@ var createTwilioSocketAdapter = (socket, getState) => ({
|
|
|
9082
11282
|
}
|
|
9083
11283
|
});
|
|
9084
11284
|
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="${
|
|
11285
|
+
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
|
|
11286
|
+
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
11287
|
};
|
|
9088
11288
|
var createTwilioMediaStreamBridge = (socket, options) => {
|
|
9089
11289
|
const runtimePreset = resolveVoiceRuntimePreset(options.preset);
|
|
@@ -9319,15 +11519,21 @@ export {
|
|
|
9319
11519
|
withVoiceOpsTaskId,
|
|
9320
11520
|
withVoiceIntegrationEventId,
|
|
9321
11521
|
voice,
|
|
11522
|
+
verifyVoiceOpsWebhookSignature,
|
|
9322
11523
|
transcodeTwilioInboundPayloadToPCM16,
|
|
9323
11524
|
transcodePCMToTwilioOutboundPayload,
|
|
9324
11525
|
summarizeVoiceTraceSinkDeliveries,
|
|
9325
11526
|
summarizeVoiceTrace,
|
|
11527
|
+
summarizeVoiceSessions,
|
|
11528
|
+
summarizeVoiceSessionReplay,
|
|
11529
|
+
summarizeVoiceProviderHealth,
|
|
9326
11530
|
summarizeVoiceOpsTasks,
|
|
9327
11531
|
summarizeVoiceOpsTaskQueue,
|
|
9328
11532
|
summarizeVoiceOpsTaskAnalytics,
|
|
9329
11533
|
summarizeVoiceIntegrationEvents,
|
|
11534
|
+
summarizeVoiceHandoffHealth,
|
|
9330
11535
|
summarizeVoiceAssistantRuns,
|
|
11536
|
+
summarizeVoiceAssistantHealth,
|
|
9331
11537
|
startVoiceOpsTask,
|
|
9332
11538
|
shapeTelephonyAssistantText,
|
|
9333
11539
|
selectVoiceTraceEventsForPrune,
|
|
@@ -9339,14 +11545,19 @@ export {
|
|
|
9339
11545
|
resolveVoiceOpsTaskAssignment,
|
|
9340
11546
|
resolveVoiceOpsTaskAgeBucket,
|
|
9341
11547
|
resolveVoiceOpsPreset,
|
|
11548
|
+
resolveVoiceAssistantMemoryNamespace,
|
|
9342
11549
|
resolveTurnDetectionConfig,
|
|
9343
11550
|
resolveAudioConditioningConfig,
|
|
9344
11551
|
requeueVoiceOpsTask,
|
|
9345
11552
|
reopenVoiceOpsTask,
|
|
9346
11553
|
renderVoiceTraceMarkdown,
|
|
9347
11554
|
renderVoiceTraceHTML,
|
|
11555
|
+
renderVoiceSessionsHTML,
|
|
11556
|
+
renderVoiceProviderHealthHTML,
|
|
11557
|
+
renderVoiceHandoffHealthHTML,
|
|
9348
11558
|
renderVoiceCallReviewMarkdown,
|
|
9349
11559
|
renderVoiceCallReviewHTML,
|
|
11560
|
+
renderVoiceAssistantHealthHTML,
|
|
9350
11561
|
redactVoiceTraceText,
|
|
9351
11562
|
redactVoiceTraceEvents,
|
|
9352
11563
|
redactVoiceTraceEvent,
|
|
@@ -9366,13 +11577,16 @@ export {
|
|
|
9366
11577
|
deliverVoiceTraceEventsToSinks,
|
|
9367
11578
|
deliverVoiceIntegrationEventToSinks,
|
|
9368
11579
|
deliverVoiceIntegrationEvent,
|
|
11580
|
+
deliverVoiceHandoff,
|
|
9369
11581
|
decodeTwilioMulawBase64,
|
|
9370
11582
|
deadLetterVoiceOpsTask,
|
|
9371
11583
|
createVoiceZendeskTicketUpdateSink,
|
|
9372
11584
|
createVoiceZendeskTicketSyncSinks,
|
|
9373
11585
|
createVoiceZendeskTicketSink,
|
|
11586
|
+
createVoiceWebhookHandoffAdapter,
|
|
9374
11587
|
createVoiceWebhookDeliveryWorkerLoop,
|
|
9375
11588
|
createVoiceWebhookDeliveryWorker,
|
|
11589
|
+
createVoiceTwilioRedirectHandoffAdapter,
|
|
9376
11590
|
createVoiceTraceSinkStore,
|
|
9377
11591
|
createVoiceTraceSinkDeliveryWorkerLoop,
|
|
9378
11592
|
createVoiceTraceSinkDeliveryWorker,
|
|
@@ -9384,7 +11598,13 @@ export {
|
|
|
9384
11598
|
createVoiceTaskUpdatedEvent,
|
|
9385
11599
|
createVoiceTaskSLABreachedEvent,
|
|
9386
11600
|
createVoiceTaskCreatedEvent,
|
|
11601
|
+
createVoiceSessionsJSONHandler,
|
|
11602
|
+
createVoiceSessionsHTMLHandler,
|
|
11603
|
+
createVoiceSessionReplayRoutes,
|
|
11604
|
+
createVoiceSessionReplayJSONHandler,
|
|
11605
|
+
createVoiceSessionReplayHTMLHandler,
|
|
9387
11606
|
createVoiceSessionRecord,
|
|
11607
|
+
createVoiceSessionListRoutes,
|
|
9388
11608
|
createVoiceSession,
|
|
9389
11609
|
createVoiceSTTRoutingCorrectionHandler,
|
|
9390
11610
|
createVoiceSQLiteTraceSinkDeliveryStore,
|
|
@@ -9399,6 +11619,10 @@ export {
|
|
|
9399
11619
|
createVoiceReviewSavedEvent,
|
|
9400
11620
|
createVoiceRedisTaskLeaseCoordinator,
|
|
9401
11621
|
createVoiceRedisIdempotencyStore,
|
|
11622
|
+
createVoiceProviderRouter,
|
|
11623
|
+
createVoiceProviderHealthRoutes,
|
|
11624
|
+
createVoiceProviderHealthJSONHandler,
|
|
11625
|
+
createVoiceProviderHealthHTMLHandler,
|
|
9402
11626
|
createVoicePostgresTraceSinkDeliveryStore,
|
|
9403
11627
|
createVoicePostgresTraceEventStore,
|
|
9404
11628
|
createVoicePostgresTaskStore,
|
|
@@ -9407,6 +11631,9 @@ export {
|
|
|
9407
11631
|
createVoicePostgresReviewStore,
|
|
9408
11632
|
createVoicePostgresIntegrationEventStore,
|
|
9409
11633
|
createVoicePostgresExternalObjectMapStore,
|
|
11634
|
+
createVoiceOpsWebhookSink,
|
|
11635
|
+
createVoiceOpsWebhookReceiverRoutes,
|
|
11636
|
+
createVoiceOpsWebhookEnvelope,
|
|
9410
11637
|
createVoiceOpsTaskWorker,
|
|
9411
11638
|
createVoiceOpsTaskProcessorWorkerLoop,
|
|
9412
11639
|
createVoiceOpsTaskProcessorWorker,
|
|
@@ -9414,6 +11641,7 @@ export {
|
|
|
9414
11641
|
createVoiceMemoryTraceSinkDeliveryStore,
|
|
9415
11642
|
createVoiceMemoryTraceEventStore,
|
|
9416
11643
|
createVoiceMemoryStore,
|
|
11644
|
+
createVoiceMemoryAssistantMemoryStore,
|
|
9417
11645
|
createVoiceLinearIssueUpdateSink,
|
|
9418
11646
|
createVoiceLinearIssueSyncSinks,
|
|
9419
11647
|
createVoiceLinearIssueSink,
|
|
@@ -9425,6 +11653,9 @@ export {
|
|
|
9425
11653
|
createVoiceHubSpotTaskSyncSinks,
|
|
9426
11654
|
createVoiceHubSpotTaskSink,
|
|
9427
11655
|
createVoiceHelpdeskTicketSink,
|
|
11656
|
+
createVoiceHandoffHealthRoutes,
|
|
11657
|
+
createVoiceHandoffHealthJSONHandler,
|
|
11658
|
+
createVoiceHandoffHealthHTMLHandler,
|
|
9428
11659
|
createVoiceFileTraceSinkDeliveryStore,
|
|
9429
11660
|
createVoiceFileTraceEventStore,
|
|
9430
11661
|
createVoiceFileTaskStore,
|
|
@@ -9433,6 +11664,7 @@ export {
|
|
|
9433
11664
|
createVoiceFileReviewStore,
|
|
9434
11665
|
createVoiceFileIntegrationEventStore,
|
|
9435
11666
|
createVoiceFileExternalObjectMapStore,
|
|
11667
|
+
createVoiceFileAssistantMemoryStore,
|
|
9436
11668
|
createVoiceExternalObjectMapId,
|
|
9437
11669
|
createVoiceExternalObjectMap,
|
|
9438
11670
|
createVoiceExperiment,
|
|
@@ -9441,6 +11673,11 @@ export {
|
|
|
9441
11673
|
createVoiceCallReviewFromLiveTelephonyReport,
|
|
9442
11674
|
createVoiceCallCompletedEvent,
|
|
9443
11675
|
createVoiceCRMActivitySink,
|
|
11676
|
+
createVoiceAssistantMemoryRecord,
|
|
11677
|
+
createVoiceAssistantMemoryHandle,
|
|
11678
|
+
createVoiceAssistantHealthRoutes,
|
|
11679
|
+
createVoiceAssistantHealthJSONHandler,
|
|
11680
|
+
createVoiceAssistantHealthHTMLHandler,
|
|
9444
11681
|
createVoiceAssistant,
|
|
9445
11682
|
createVoiceAgentTool,
|
|
9446
11683
|
createVoiceAgentSquad,
|
|
@@ -9453,9 +11690,13 @@ export {
|
|
|
9453
11690
|
createStoredVoiceCallReviewArtifact,
|
|
9454
11691
|
createRiskyTurnCorrectionHandler,
|
|
9455
11692
|
createPhraseHintCorrectionHandler,
|
|
11693
|
+
createOpenAIVoiceAssistantModel,
|
|
11694
|
+
createJSONVoiceAssistantModel,
|
|
9456
11695
|
createId,
|
|
11696
|
+
createGeminiVoiceAssistantModel,
|
|
9457
11697
|
createDomainPhraseHints,
|
|
9458
11698
|
createDomainLexicon,
|
|
11699
|
+
createAnthropicVoiceAssistantModel,
|
|
9459
11700
|
conditionAudioChunk,
|
|
9460
11701
|
completeVoiceOpsTask,
|
|
9461
11702
|
claimVoiceOpsTask,
|