@absolutejs/voice 0.0.22-beta.5 → 0.0.22-beta.51
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 +3 -0
- package/dist/angular/index.js +463 -43
- package/dist/angular/voice-app-kit-status.service.d.ts +12 -0
- package/dist/angular/voice-provider-status.service.d.ts +12 -0
- package/dist/angular/voice-stream.service.d.ts +2 -0
- package/dist/angular/voice-workflow-status.service.d.ts +12 -0
- package/dist/appKit.d.ts +92 -0
- package/dist/assistantHealth.d.ts +81 -0
- package/dist/client/actions.d.ts +22 -0
- package/dist/client/appKitStatus.d.ts +19 -0
- package/dist/client/connection.d.ts +3 -0
- package/dist/client/htmxBootstrap.js +44 -2
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +285 -2
- package/dist/client/providerStatus.d.ts +19 -0
- package/dist/client/workflowStatus.d.ts +19 -0
- package/dist/diagnosticsRoutes.d.ts +44 -0
- package/dist/evalRoutes.d.ts +213 -0
- package/dist/handoff.d.ts +54 -0
- package/dist/handoffHealth.d.ts +94 -0
- package/dist/index.d.ts +32 -4
- package/dist/index.js +4425 -166
- package/dist/modelAdapters.d.ts +81 -0
- package/dist/opsConsoleRoutes.d.ts +77 -0
- package/dist/opsWebhook.d.ts +126 -0
- package/dist/providerAdapters.d.ts +37 -0
- package/dist/providerHealth.d.ts +79 -0
- package/dist/qualityRoutes.d.ts +76 -0
- package/dist/queue.d.ts +52 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +355 -11
- package/dist/react/useVoiceAppKitStatus.d.ts +8 -0
- 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/react/useVoiceWorkflowStatus.d.ts +8 -0
- package/dist/resilienceRoutes.d.ts +106 -0
- package/dist/sessionReplay.d.ts +175 -0
- package/dist/svelte/createVoiceAppKitStatus.d.ts +8 -0
- package/dist/svelte/createVoiceProviderStatus.d.ts +8 -0
- package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
- package/dist/svelte/index.d.ts +3 -0
- package/dist/svelte/index.js +292 -3
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1468 -7
- package/dist/testing/ioProviderSimulator.d.ts +41 -0
- package/dist/testing/providerSimulator.d.ts +44 -0
- package/dist/trace.d.ts +1 -1
- package/dist/types.d.ts +84 -2
- package/dist/vue/index.d.ts +3 -0
- package/dist/vue/index.js +412 -25
- package/dist/vue/useVoiceAppKitStatus.d.ts +9 -0
- package/dist/vue/useVoiceProviderStatus.d.ts +9 -0
- package/dist/vue/useVoiceStream.d.ts +2 -0
- package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
- package/dist/workflowContract.d.ts +91 -0
- package/package.json +1 -1
package/dist/testing/index.js
CHANGED
|
@@ -2105,6 +2105,12 @@ var serverMessageToAction = (message) => {
|
|
|
2105
2105
|
sessionId: message.sessionId,
|
|
2106
2106
|
type: "complete"
|
|
2107
2107
|
};
|
|
2108
|
+
case "call_lifecycle":
|
|
2109
|
+
return {
|
|
2110
|
+
event: message.event,
|
|
2111
|
+
sessionId: message.sessionId,
|
|
2112
|
+
type: "call_lifecycle"
|
|
2113
|
+
};
|
|
2108
2114
|
case "error":
|
|
2109
2115
|
return {
|
|
2110
2116
|
message: normalizeErrorMessage(message.message),
|
|
@@ -2148,7 +2154,7 @@ var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
|
|
|
2148
2154
|
var noop = () => {};
|
|
2149
2155
|
var noopUnsubscribe = () => noop;
|
|
2150
2156
|
var NOOP_CONNECTION = {
|
|
2151
|
-
|
|
2157
|
+
callControl: noop,
|
|
2152
2158
|
close: noop,
|
|
2153
2159
|
endTurn: noop,
|
|
2154
2160
|
getReadyState: () => WS_CLOSED,
|
|
@@ -2156,6 +2162,7 @@ var NOOP_CONNECTION = {
|
|
|
2156
2162
|
getSessionId: () => "",
|
|
2157
2163
|
send: noop,
|
|
2158
2164
|
sendAudio: noop,
|
|
2165
|
+
start: () => {},
|
|
2159
2166
|
subscribe: noopUnsubscribe
|
|
2160
2167
|
};
|
|
2161
2168
|
var createSessionId = () => crypto.randomUUID();
|
|
@@ -2177,6 +2184,7 @@ var isVoiceServerMessage = (value) => {
|
|
|
2177
2184
|
switch (value.type) {
|
|
2178
2185
|
case "audio":
|
|
2179
2186
|
case "assistant":
|
|
2187
|
+
case "call_lifecycle":
|
|
2180
2188
|
case "complete":
|
|
2181
2189
|
case "error":
|
|
2182
2190
|
case "final":
|
|
@@ -2317,6 +2325,12 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
2317
2325
|
const endTurn = () => {
|
|
2318
2326
|
send({ type: "end_turn" });
|
|
2319
2327
|
};
|
|
2328
|
+
const callControl = (message) => {
|
|
2329
|
+
send({
|
|
2330
|
+
...message,
|
|
2331
|
+
type: "call_control"
|
|
2332
|
+
});
|
|
2333
|
+
};
|
|
2320
2334
|
const close = () => {
|
|
2321
2335
|
clearTimers();
|
|
2322
2336
|
if (state.ws) {
|
|
@@ -2334,7 +2348,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
2334
2348
|
};
|
|
2335
2349
|
connect();
|
|
2336
2350
|
return {
|
|
2337
|
-
|
|
2351
|
+
callControl,
|
|
2338
2352
|
close,
|
|
2339
2353
|
endTurn,
|
|
2340
2354
|
getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
|
|
@@ -2342,6 +2356,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
2342
2356
|
getSessionId: () => state.sessionId,
|
|
2343
2357
|
send,
|
|
2344
2358
|
sendAudio,
|
|
2359
|
+
start,
|
|
2345
2360
|
subscribe
|
|
2346
2361
|
};
|
|
2347
2362
|
};
|
|
@@ -2350,6 +2365,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
2350
2365
|
var createInitialState2 = () => ({
|
|
2351
2366
|
assistantAudio: [],
|
|
2352
2367
|
assistantTexts: [],
|
|
2368
|
+
call: null,
|
|
2353
2369
|
error: null,
|
|
2354
2370
|
isConnected: false,
|
|
2355
2371
|
scenarioId: null,
|
|
@@ -2393,6 +2409,20 @@ var createVoiceStreamStore = () => {
|
|
|
2393
2409
|
status: "completed"
|
|
2394
2410
|
};
|
|
2395
2411
|
break;
|
|
2412
|
+
case "call_lifecycle":
|
|
2413
|
+
state = {
|
|
2414
|
+
...state,
|
|
2415
|
+
call: {
|
|
2416
|
+
...state.call,
|
|
2417
|
+
disposition: action.event.type === "end" ? action.event.disposition : state.call?.disposition,
|
|
2418
|
+
endedAt: action.event.type === "end" ? action.event.at : state.call?.endedAt,
|
|
2419
|
+
events: [...state.call?.events ?? [], action.event],
|
|
2420
|
+
lastEventAt: action.event.at,
|
|
2421
|
+
startedAt: state.call?.startedAt ?? action.event.at
|
|
2422
|
+
},
|
|
2423
|
+
sessionId: action.sessionId
|
|
2424
|
+
};
|
|
2425
|
+
break;
|
|
2396
2426
|
case "connected":
|
|
2397
2427
|
state = {
|
|
2398
2428
|
...state,
|
|
@@ -2479,6 +2509,9 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
2479
2509
|
}
|
|
2480
2510
|
});
|
|
2481
2511
|
return {
|
|
2512
|
+
callControl(message) {
|
|
2513
|
+
connection.callControl(message);
|
|
2514
|
+
},
|
|
2482
2515
|
close() {
|
|
2483
2516
|
unsubscribeConnection();
|
|
2484
2517
|
connection.close();
|
|
@@ -2522,6 +2555,9 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
2522
2555
|
get assistantAudio() {
|
|
2523
2556
|
return store.getSnapshot().assistantAudio;
|
|
2524
2557
|
},
|
|
2558
|
+
get call() {
|
|
2559
|
+
return store.getSnapshot().call;
|
|
2560
|
+
},
|
|
2525
2561
|
sendAudio(audio) {
|
|
2526
2562
|
connection.sendAudio(audio);
|
|
2527
2563
|
},
|
|
@@ -2854,6 +2890,7 @@ var resolveVoiceRuntimePreset = (name = "default") => {
|
|
|
2854
2890
|
var createInitialState3 = (stream) => ({
|
|
2855
2891
|
assistantAudio: [...stream.assistantAudio],
|
|
2856
2892
|
assistantTexts: [...stream.assistantTexts],
|
|
2893
|
+
call: stream.call,
|
|
2857
2894
|
error: stream.error,
|
|
2858
2895
|
isConnected: stream.isConnected,
|
|
2859
2896
|
isRecording: false,
|
|
@@ -2883,6 +2920,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
2883
2920
|
...state,
|
|
2884
2921
|
assistantAudio: [...stream.assistantAudio],
|
|
2885
2922
|
assistantTexts: [...stream.assistantTexts],
|
|
2923
|
+
call: stream.call,
|
|
2886
2924
|
error: stream.error,
|
|
2887
2925
|
isConnected: stream.isConnected,
|
|
2888
2926
|
partial: stream.partial,
|
|
@@ -2960,6 +2998,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
2960
2998
|
bindHTMX(bindingOptions) {
|
|
2961
2999
|
return bindVoiceHTMX(stream, bindingOptions);
|
|
2962
3000
|
},
|
|
3001
|
+
callControl: (message) => stream.callControl(message),
|
|
2963
3002
|
close,
|
|
2964
3003
|
endTurn: () => stream.endTurn(),
|
|
2965
3004
|
get error() {
|
|
@@ -3012,6 +3051,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
3012
3051
|
},
|
|
3013
3052
|
get assistantAudio() {
|
|
3014
3053
|
return state.assistantAudio;
|
|
3054
|
+
},
|
|
3055
|
+
get call() {
|
|
3056
|
+
return state.call;
|
|
3015
3057
|
}
|
|
3016
3058
|
};
|
|
3017
3059
|
};
|
|
@@ -3468,6 +3510,935 @@ var loadVoiceTestFixtures = async (fixtureDirectory) => {
|
|
|
3468
3510
|
}
|
|
3469
3511
|
return fixtures;
|
|
3470
3512
|
};
|
|
3513
|
+
// src/testing/ioProviderSimulator.ts
|
|
3514
|
+
var defaultFailureMessage = (input) => `Simulated ${input.provider} ${input.kind.toUpperCase()} ${input.operation} failure.`;
|
|
3515
|
+
var resolveRecoveryElapsedMs = (value, provider) => {
|
|
3516
|
+
if (typeof value === "number") {
|
|
3517
|
+
return value;
|
|
3518
|
+
}
|
|
3519
|
+
return value?.[provider] ?? 25;
|
|
3520
|
+
};
|
|
3521
|
+
var createHealth = (input) => ({
|
|
3522
|
+
consecutiveFailures: input.status === "healthy" ? 0 : 1,
|
|
3523
|
+
lastFailureAt: input.status === "healthy" ? undefined : input.now,
|
|
3524
|
+
provider: input.provider,
|
|
3525
|
+
status: input.status,
|
|
3526
|
+
suppressedUntil: input.suppressedUntil
|
|
3527
|
+
});
|
|
3528
|
+
var resolveFallback = async (options, provider) => {
|
|
3529
|
+
const configured = typeof options.fallback === "function" ? await options.fallback(provider) : options.fallback;
|
|
3530
|
+
return (configured ?? options.providers).find((candidate) => candidate !== provider);
|
|
3531
|
+
};
|
|
3532
|
+
var createVoiceIOProviderFailureSimulator = (options) => {
|
|
3533
|
+
if (options.providers.length === 0) {
|
|
3534
|
+
throw new Error("At least one provider is required.");
|
|
3535
|
+
}
|
|
3536
|
+
const now = options.now ?? Date.now;
|
|
3537
|
+
const operation = options.operation ?? "open";
|
|
3538
|
+
const cooldownMs = Math.max(0, options.cooldownMs ?? 30000);
|
|
3539
|
+
const emit = async (event, input) => {
|
|
3540
|
+
await options.onProviderEvent?.(event, input);
|
|
3541
|
+
};
|
|
3542
|
+
const run = async (provider, mode) => {
|
|
3543
|
+
if (!options.providers.includes(provider)) {
|
|
3544
|
+
throw new Error(`${provider} is not configured for simulation.`);
|
|
3545
|
+
}
|
|
3546
|
+
const startedAt = now();
|
|
3547
|
+
const sessionId = options.sessionId?.({ mode, now: startedAt, provider }) ?? `${options.kind}-provider-sim-${startedAt}`;
|
|
3548
|
+
if (mode === "recovery") {
|
|
3549
|
+
await emit({
|
|
3550
|
+
at: startedAt,
|
|
3551
|
+
attempt: 0,
|
|
3552
|
+
elapsedMs: resolveRecoveryElapsedMs(options.recoveryElapsedMs, provider),
|
|
3553
|
+
kind: options.kind,
|
|
3554
|
+
latencyBudgetMs: options.latencyBudgets?.[provider],
|
|
3555
|
+
operation,
|
|
3556
|
+
provider,
|
|
3557
|
+
providerHealth: createHealth({
|
|
3558
|
+
now: startedAt,
|
|
3559
|
+
provider,
|
|
3560
|
+
status: "healthy"
|
|
3561
|
+
}),
|
|
3562
|
+
selectedProvider: provider,
|
|
3563
|
+
status: "success"
|
|
3564
|
+
}, { mode, provider, sessionId });
|
|
3565
|
+
return {
|
|
3566
|
+
mode,
|
|
3567
|
+
provider,
|
|
3568
|
+
sessionId,
|
|
3569
|
+
status: "simulated"
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
const fallbackProvider = await resolveFallback(options, provider);
|
|
3573
|
+
const suppressedUntil = startedAt + cooldownMs;
|
|
3574
|
+
await emit({
|
|
3575
|
+
at: startedAt,
|
|
3576
|
+
attempt: 0,
|
|
3577
|
+
elapsedMs: options.failureElapsedMs ?? 10,
|
|
3578
|
+
error: (options.failureMessage ?? defaultFailureMessage)({
|
|
3579
|
+
kind: options.kind,
|
|
3580
|
+
operation,
|
|
3581
|
+
provider
|
|
3582
|
+
}),
|
|
3583
|
+
fallbackProvider,
|
|
3584
|
+
kind: options.kind,
|
|
3585
|
+
latencyBudgetMs: options.latencyBudgets?.[provider],
|
|
3586
|
+
operation,
|
|
3587
|
+
provider,
|
|
3588
|
+
providerHealth: createHealth({
|
|
3589
|
+
now: startedAt,
|
|
3590
|
+
provider,
|
|
3591
|
+
status: "suppressed",
|
|
3592
|
+
suppressedUntil
|
|
3593
|
+
}),
|
|
3594
|
+
selectedProvider: provider,
|
|
3595
|
+
status: "error",
|
|
3596
|
+
suppressedUntil
|
|
3597
|
+
}, { mode, provider, sessionId });
|
|
3598
|
+
if (fallbackProvider) {
|
|
3599
|
+
await emit({
|
|
3600
|
+
at: startedAt + 1,
|
|
3601
|
+
attempt: 1,
|
|
3602
|
+
elapsedMs: resolveRecoveryElapsedMs(options.recoveryElapsedMs, fallbackProvider),
|
|
3603
|
+
fallbackProvider,
|
|
3604
|
+
kind: options.kind,
|
|
3605
|
+
latencyBudgetMs: options.latencyBudgets?.[fallbackProvider],
|
|
3606
|
+
operation,
|
|
3607
|
+
provider: fallbackProvider,
|
|
3608
|
+
providerHealth: createHealth({
|
|
3609
|
+
now: startedAt + 1,
|
|
3610
|
+
provider: fallbackProvider,
|
|
3611
|
+
status: "healthy"
|
|
3612
|
+
}),
|
|
3613
|
+
selectedProvider: provider,
|
|
3614
|
+
status: "fallback"
|
|
3615
|
+
}, { mode, provider, sessionId });
|
|
3616
|
+
}
|
|
3617
|
+
return {
|
|
3618
|
+
fallbackProvider,
|
|
3619
|
+
mode,
|
|
3620
|
+
provider,
|
|
3621
|
+
sessionId,
|
|
3622
|
+
status: "simulated",
|
|
3623
|
+
suppressedUntil
|
|
3624
|
+
};
|
|
3625
|
+
};
|
|
3626
|
+
return {
|
|
3627
|
+
run
|
|
3628
|
+
};
|
|
3629
|
+
};
|
|
3630
|
+
// src/modelAdapters.ts
|
|
3631
|
+
var OUTPUT_SCHEMA = {
|
|
3632
|
+
additionalProperties: false,
|
|
3633
|
+
properties: {
|
|
3634
|
+
assistantText: {
|
|
3635
|
+
type: "string"
|
|
3636
|
+
},
|
|
3637
|
+
complete: {
|
|
3638
|
+
type: "boolean"
|
|
3639
|
+
},
|
|
3640
|
+
escalate: {
|
|
3641
|
+
additionalProperties: false,
|
|
3642
|
+
properties: {
|
|
3643
|
+
metadata: {
|
|
3644
|
+
additionalProperties: true,
|
|
3645
|
+
type: "object"
|
|
3646
|
+
},
|
|
3647
|
+
reason: {
|
|
3648
|
+
type: "string"
|
|
3649
|
+
}
|
|
3650
|
+
},
|
|
3651
|
+
required: ["reason"],
|
|
3652
|
+
type: "object"
|
|
3653
|
+
},
|
|
3654
|
+
noAnswer: {
|
|
3655
|
+
additionalProperties: false,
|
|
3656
|
+
properties: {
|
|
3657
|
+
metadata: {
|
|
3658
|
+
additionalProperties: true,
|
|
3659
|
+
type: "object"
|
|
3660
|
+
}
|
|
3661
|
+
},
|
|
3662
|
+
type: "object"
|
|
3663
|
+
},
|
|
3664
|
+
result: {
|
|
3665
|
+
additionalProperties: true,
|
|
3666
|
+
type: "object"
|
|
3667
|
+
},
|
|
3668
|
+
transfer: {
|
|
3669
|
+
additionalProperties: false,
|
|
3670
|
+
properties: {
|
|
3671
|
+
metadata: {
|
|
3672
|
+
additionalProperties: true,
|
|
3673
|
+
type: "object"
|
|
3674
|
+
},
|
|
3675
|
+
reason: {
|
|
3676
|
+
type: "string"
|
|
3677
|
+
},
|
|
3678
|
+
target: {
|
|
3679
|
+
type: "string"
|
|
3680
|
+
}
|
|
3681
|
+
},
|
|
3682
|
+
required: ["target"],
|
|
3683
|
+
type: "object"
|
|
3684
|
+
},
|
|
3685
|
+
voicemail: {
|
|
3686
|
+
additionalProperties: false,
|
|
3687
|
+
properties: {
|
|
3688
|
+
metadata: {
|
|
3689
|
+
additionalProperties: true,
|
|
3690
|
+
type: "object"
|
|
3691
|
+
}
|
|
3692
|
+
},
|
|
3693
|
+
type: "object"
|
|
3694
|
+
}
|
|
3695
|
+
},
|
|
3696
|
+
type: "object"
|
|
3697
|
+
};
|
|
3698
|
+
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.";
|
|
3699
|
+
var stripJSONCodeFence = (value) => {
|
|
3700
|
+
const trimmed = value.trim();
|
|
3701
|
+
const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
3702
|
+
return match?.[1]?.trim() ?? value;
|
|
3703
|
+
};
|
|
3704
|
+
var parseJSON = (value) => {
|
|
3705
|
+
try {
|
|
3706
|
+
const parsed = JSON.parse(stripJSONCodeFence(value));
|
|
3707
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3708
|
+
} catch {
|
|
3709
|
+
return {
|
|
3710
|
+
assistantText: value
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
};
|
|
3714
|
+
var parseJSONValue = (value) => {
|
|
3715
|
+
try {
|
|
3716
|
+
return JSON.parse(value);
|
|
3717
|
+
} catch {
|
|
3718
|
+
return value;
|
|
3719
|
+
}
|
|
3720
|
+
};
|
|
3721
|
+
|
|
3722
|
+
class VoiceProviderTimeoutError extends Error {
|
|
3723
|
+
provider;
|
|
3724
|
+
timeoutMs;
|
|
3725
|
+
constructor(provider, timeoutMs) {
|
|
3726
|
+
super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
|
|
3727
|
+
this.name = "VoiceProviderTimeoutError";
|
|
3728
|
+
this.provider = provider;
|
|
3729
|
+
this.timeoutMs = timeoutMs;
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
var getMessageToolCalls = (message) => {
|
|
3733
|
+
const toolCalls = message.metadata?.toolCalls;
|
|
3734
|
+
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
3735
|
+
};
|
|
3736
|
+
var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
|
|
3737
|
+
var sleep = (ms) => new Promise((resolve2) => {
|
|
3738
|
+
setTimeout(resolve2, ms);
|
|
3739
|
+
});
|
|
3740
|
+
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
3741
|
+
var defaultIsRateLimitError = (error) => /(\b429\b|rate limit|quota|too many requests)/i.test(errorMessage(error));
|
|
3742
|
+
var normalizeRouteOutput = (output) => {
|
|
3743
|
+
const result = {};
|
|
3744
|
+
if (typeof output.assistantText === "string") {
|
|
3745
|
+
result.assistantText = output.assistantText;
|
|
3746
|
+
}
|
|
3747
|
+
if (typeof output.complete === "boolean") {
|
|
3748
|
+
result.complete = output.complete;
|
|
3749
|
+
}
|
|
3750
|
+
if (output.result !== undefined) {
|
|
3751
|
+
result.result = output.result;
|
|
3752
|
+
}
|
|
3753
|
+
if (output.transfer && typeof output.transfer === "object") {
|
|
3754
|
+
const transfer = output.transfer;
|
|
3755
|
+
if (typeof transfer.target === "string") {
|
|
3756
|
+
result.transfer = {
|
|
3757
|
+
metadata: transfer.metadata && typeof transfer.metadata === "object" ? transfer.metadata : undefined,
|
|
3758
|
+
reason: typeof transfer.reason === "string" ? transfer.reason : undefined,
|
|
3759
|
+
target: transfer.target
|
|
3760
|
+
};
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
if (output.escalate && typeof output.escalate === "object") {
|
|
3764
|
+
const escalate = output.escalate;
|
|
3765
|
+
if (typeof escalate.reason === "string") {
|
|
3766
|
+
result.escalate = {
|
|
3767
|
+
metadata: escalate.metadata && typeof escalate.metadata === "object" ? escalate.metadata : undefined,
|
|
3768
|
+
reason: escalate.reason
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
if (output.voicemail && typeof output.voicemail === "object") {
|
|
3773
|
+
const voicemail = output.voicemail;
|
|
3774
|
+
result.voicemail = {
|
|
3775
|
+
metadata: voicemail.metadata && typeof voicemail.metadata === "object" ? voicemail.metadata : undefined
|
|
3776
|
+
};
|
|
3777
|
+
}
|
|
3778
|
+
if (output.noAnswer && typeof output.noAnswer === "object") {
|
|
3779
|
+
const noAnswer = output.noAnswer;
|
|
3780
|
+
result.noAnswer = {
|
|
3781
|
+
metadata: noAnswer.metadata && typeof noAnswer.metadata === "object" ? noAnswer.metadata : undefined
|
|
3782
|
+
};
|
|
3783
|
+
}
|
|
3784
|
+
return result;
|
|
3785
|
+
};
|
|
3786
|
+
var createJSONVoiceAssistantModel = (options) => ({
|
|
3787
|
+
generate: async (input) => {
|
|
3788
|
+
const output = await options.generate(input);
|
|
3789
|
+
if ("assistantText" in output || "toolCalls" in output || "complete" in output || "transfer" in output || "escalate" in output) {
|
|
3790
|
+
return output;
|
|
3791
|
+
}
|
|
3792
|
+
return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
|
|
3793
|
+
}
|
|
3794
|
+
});
|
|
3795
|
+
var createVoiceProviderRouter = (options) => {
|
|
3796
|
+
const providerIds = Object.keys(options.providers);
|
|
3797
|
+
const firstProvider = providerIds[0];
|
|
3798
|
+
const policy = typeof options.policy === "string" ? {
|
|
3799
|
+
strategy: options.policy
|
|
3800
|
+
} : options.policy;
|
|
3801
|
+
const strategy = policy?.strategy ?? "prefer-selected";
|
|
3802
|
+
const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
|
|
3803
|
+
const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
|
|
3804
|
+
const healthState = new Map;
|
|
3805
|
+
const now = () => healthOptions?.now?.() ?? Date.now();
|
|
3806
|
+
const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
|
|
3807
|
+
const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
|
|
3808
|
+
const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
|
|
3809
|
+
const getProviderTimeoutMs = (provider) => {
|
|
3810
|
+
const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
|
|
3811
|
+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
|
|
3812
|
+
};
|
|
3813
|
+
const getHealth = (provider) => {
|
|
3814
|
+
const existing = healthState.get(provider);
|
|
3815
|
+
if (existing) {
|
|
3816
|
+
return existing;
|
|
3817
|
+
}
|
|
3818
|
+
const next = {
|
|
3819
|
+
consecutiveFailures: 0,
|
|
3820
|
+
provider,
|
|
3821
|
+
status: "healthy"
|
|
3822
|
+
};
|
|
3823
|
+
healthState.set(provider, next);
|
|
3824
|
+
return next;
|
|
3825
|
+
};
|
|
3826
|
+
const cloneHealth = (provider) => {
|
|
3827
|
+
if (!healthOptions) {
|
|
3828
|
+
return;
|
|
3829
|
+
}
|
|
3830
|
+
return {
|
|
3831
|
+
...getHealth(provider)
|
|
3832
|
+
};
|
|
3833
|
+
};
|
|
3834
|
+
const getSuppressionRemainingMs = (provider) => {
|
|
3835
|
+
if (!healthOptions) {
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
const suppressedUntil = getHealth(provider).suppressedUntil;
|
|
3839
|
+
return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
|
|
3840
|
+
};
|
|
3841
|
+
const isSuppressed = (provider) => {
|
|
3842
|
+
if (!healthOptions) {
|
|
3843
|
+
return false;
|
|
3844
|
+
}
|
|
3845
|
+
const health = getHealth(provider);
|
|
3846
|
+
return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
|
|
3847
|
+
};
|
|
3848
|
+
const recordProviderSuccess = (provider) => {
|
|
3849
|
+
if (!healthOptions) {
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
const health = getHealth(provider);
|
|
3853
|
+
health.consecutiveFailures = 0;
|
|
3854
|
+
health.status = "healthy";
|
|
3855
|
+
health.suppressedUntil = undefined;
|
|
3856
|
+
return cloneHealth(provider);
|
|
3857
|
+
};
|
|
3858
|
+
const recordProviderError = (provider, isProviderError, rateLimited) => {
|
|
3859
|
+
if (!healthOptions || !isProviderError) {
|
|
3860
|
+
return cloneHealth(provider);
|
|
3861
|
+
}
|
|
3862
|
+
const currentTime = now();
|
|
3863
|
+
const health = getHealth(provider);
|
|
3864
|
+
health.consecutiveFailures += 1;
|
|
3865
|
+
health.lastFailureAt = currentTime;
|
|
3866
|
+
if (rateLimited) {
|
|
3867
|
+
health.lastRateLimitedAt = currentTime;
|
|
3868
|
+
}
|
|
3869
|
+
if (rateLimited || health.consecutiveFailures >= failureThreshold) {
|
|
3870
|
+
health.status = "suppressed";
|
|
3871
|
+
health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
|
|
3872
|
+
}
|
|
3873
|
+
return cloneHealth(provider);
|
|
3874
|
+
};
|
|
3875
|
+
const resolveAllowedProviders = async (input) => {
|
|
3876
|
+
const allowProviders = policy?.allowProviders ?? options.allowProviders;
|
|
3877
|
+
const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
|
|
3878
|
+
return new Set(allowed ?? providerIds);
|
|
3879
|
+
};
|
|
3880
|
+
const sortProviders = (providers) => {
|
|
3881
|
+
if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
|
|
3882
|
+
return providers;
|
|
3883
|
+
}
|
|
3884
|
+
return [...providers].sort((left, right) => {
|
|
3885
|
+
const leftProfile = options.providerProfiles?.[left];
|
|
3886
|
+
const rightProfile = options.providerProfiles?.[right];
|
|
3887
|
+
const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
3888
|
+
const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
3889
|
+
return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
|
|
3890
|
+
});
|
|
3891
|
+
};
|
|
3892
|
+
const resolveOrder = async (input) => {
|
|
3893
|
+
const selectedProvider = await options.selectProvider?.(input);
|
|
3894
|
+
const allowedProviders = await resolveAllowedProviders(input);
|
|
3895
|
+
const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
|
|
3896
|
+
const rankedProviders = sortProviders([
|
|
3897
|
+
...fallbackOrder ?? providerIds
|
|
3898
|
+
]).filter((provider) => allowedProviders.has(provider));
|
|
3899
|
+
const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
|
|
3900
|
+
const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
|
|
3901
|
+
const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
|
|
3902
|
+
const seen = new Set;
|
|
3903
|
+
const order = [];
|
|
3904
|
+
const candidates = strategy === "ordered" ? candidateRankedProviders : [
|
|
3905
|
+
preferred,
|
|
3906
|
+
...candidateRankedProviders,
|
|
3907
|
+
...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
|
|
3908
|
+
];
|
|
3909
|
+
for (const provider of candidates) {
|
|
3910
|
+
if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
|
|
3911
|
+
continue;
|
|
3912
|
+
}
|
|
3913
|
+
seen.add(provider);
|
|
3914
|
+
order.push(provider);
|
|
3915
|
+
}
|
|
3916
|
+
return {
|
|
3917
|
+
order,
|
|
3918
|
+
selectedProvider: preferred
|
|
3919
|
+
};
|
|
3920
|
+
};
|
|
3921
|
+
const emit = async (event, input) => {
|
|
3922
|
+
await options.onProviderEvent?.(event, input);
|
|
3923
|
+
};
|
|
3924
|
+
const runProvider = async (provider, model, input) => {
|
|
3925
|
+
const timeoutMs = getProviderTimeoutMs(provider);
|
|
3926
|
+
if (!timeoutMs) {
|
|
3927
|
+
return model.generate(input);
|
|
3928
|
+
}
|
|
3929
|
+
let timeout;
|
|
3930
|
+
try {
|
|
3931
|
+
return await Promise.race([
|
|
3932
|
+
model.generate(input),
|
|
3933
|
+
new Promise((_, reject) => {
|
|
3934
|
+
timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
|
|
3935
|
+
})
|
|
3936
|
+
]);
|
|
3937
|
+
} finally {
|
|
3938
|
+
if (timeout) {
|
|
3939
|
+
clearTimeout(timeout);
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
};
|
|
3943
|
+
return {
|
|
3944
|
+
generate: async (input) => {
|
|
3945
|
+
const { order, selectedProvider } = await resolveOrder(input);
|
|
3946
|
+
if (!selectedProvider || order.length === 0) {
|
|
3947
|
+
throw new Error("Voice provider router has no available providers.");
|
|
3948
|
+
}
|
|
3949
|
+
let lastError;
|
|
3950
|
+
for (const [index, provider] of order.entries()) {
|
|
3951
|
+
const model = options.providers[provider];
|
|
3952
|
+
if (!model) {
|
|
3953
|
+
continue;
|
|
3954
|
+
}
|
|
3955
|
+
const startedAt = Date.now();
|
|
3956
|
+
try {
|
|
3957
|
+
const output = await runProvider(provider, model, input);
|
|
3958
|
+
const providerHealth = recordProviderSuccess(provider);
|
|
3959
|
+
await emit({
|
|
3960
|
+
at: Date.now(),
|
|
3961
|
+
attempt: index + 1,
|
|
3962
|
+
elapsedMs: Date.now() - startedAt,
|
|
3963
|
+
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
3964
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
3965
|
+
provider,
|
|
3966
|
+
providerHealth,
|
|
3967
|
+
recovered: provider !== selectedProvider,
|
|
3968
|
+
selectedProvider,
|
|
3969
|
+
status: provider === selectedProvider ? "success" : "fallback"
|
|
3970
|
+
}, input);
|
|
3971
|
+
return output;
|
|
3972
|
+
} catch (error) {
|
|
3973
|
+
lastError = error;
|
|
3974
|
+
const hasNextProvider = index < order.length - 1;
|
|
3975
|
+
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
3976
|
+
const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
|
|
3977
|
+
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
3978
|
+
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
3979
|
+
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
3980
|
+
const nextProvider = hasNextProvider ? order[index + 1] : undefined;
|
|
3981
|
+
await emit({
|
|
3982
|
+
at: Date.now(),
|
|
3983
|
+
attempt: index + 1,
|
|
3984
|
+
elapsedMs: Date.now() - startedAt,
|
|
3985
|
+
error: errorMessage(error),
|
|
3986
|
+
fallbackProvider: shouldFallback ? nextProvider : undefined,
|
|
3987
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
3988
|
+
provider,
|
|
3989
|
+
providerHealth,
|
|
3990
|
+
rateLimited,
|
|
3991
|
+
selectedProvider,
|
|
3992
|
+
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
3993
|
+
suppressedUntil: providerHealth?.suppressedUntil,
|
|
3994
|
+
status: "error",
|
|
3995
|
+
timedOut
|
|
3996
|
+
}, input);
|
|
3997
|
+
if (!hasNextProvider || !shouldFallback) {
|
|
3998
|
+
throw error;
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
throw lastError ?? new Error("Voice provider router did not run a provider.");
|
|
4003
|
+
}
|
|
4004
|
+
};
|
|
4005
|
+
};
|
|
4006
|
+
var messageToOpenAIInput = (message) => {
|
|
4007
|
+
if (message.role === "tool") {
|
|
4008
|
+
return [
|
|
4009
|
+
{
|
|
4010
|
+
call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
|
|
4011
|
+
output: message.content,
|
|
4012
|
+
type: "function_call_output"
|
|
4013
|
+
}
|
|
4014
|
+
];
|
|
4015
|
+
}
|
|
4016
|
+
const toolCalls = getMessageToolCalls(message);
|
|
4017
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
4018
|
+
return toolCalls.map((toolCall) => ({
|
|
4019
|
+
arguments: JSON.stringify(toolCall.args),
|
|
4020
|
+
call_id: toolCall.id ?? crypto.randomUUID(),
|
|
4021
|
+
name: toolCall.name,
|
|
4022
|
+
type: "function_call"
|
|
4023
|
+
}));
|
|
4024
|
+
}
|
|
4025
|
+
return [
|
|
4026
|
+
{
|
|
4027
|
+
content: message.content,
|
|
4028
|
+
role: message.role === "system" ? "developer" : message.role
|
|
4029
|
+
}
|
|
4030
|
+
];
|
|
4031
|
+
};
|
|
4032
|
+
var messagesToOpenAIInput = (messages) => messages.flatMap(messageToOpenAIInput);
|
|
4033
|
+
var messageToAnthropicMessage = (message) => {
|
|
4034
|
+
if (message.role === "system") {
|
|
4035
|
+
return;
|
|
4036
|
+
}
|
|
4037
|
+
if (message.role === "tool") {
|
|
4038
|
+
if (!message.toolCallId) {
|
|
4039
|
+
return {
|
|
4040
|
+
content: `Tool result from ${message.name ?? "tool"}: ${message.content}`,
|
|
4041
|
+
role: "user"
|
|
4042
|
+
};
|
|
4043
|
+
}
|
|
4044
|
+
return {
|
|
4045
|
+
content: [
|
|
4046
|
+
{
|
|
4047
|
+
content: message.content,
|
|
4048
|
+
tool_use_id: message.toolCallId,
|
|
4049
|
+
type: "tool_result"
|
|
4050
|
+
}
|
|
4051
|
+
],
|
|
4052
|
+
role: "user"
|
|
4053
|
+
};
|
|
4054
|
+
}
|
|
4055
|
+
const toolCalls = getMessageToolCalls(message);
|
|
4056
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
4057
|
+
return {
|
|
4058
|
+
content: [
|
|
4059
|
+
...message.content ? [
|
|
4060
|
+
{
|
|
4061
|
+
text: message.content,
|
|
4062
|
+
type: "text"
|
|
4063
|
+
}
|
|
4064
|
+
] : [],
|
|
4065
|
+
...toolCalls.map((toolCall) => ({
|
|
4066
|
+
id: toolCall.id ?? crypto.randomUUID(),
|
|
4067
|
+
input: toolCall.args,
|
|
4068
|
+
name: toolCall.name,
|
|
4069
|
+
type: "tool_use"
|
|
4070
|
+
}))
|
|
4071
|
+
],
|
|
4072
|
+
role: "assistant"
|
|
4073
|
+
};
|
|
4074
|
+
}
|
|
4075
|
+
return {
|
|
4076
|
+
content: message.content,
|
|
4077
|
+
role: message.role
|
|
4078
|
+
};
|
|
4079
|
+
};
|
|
4080
|
+
var toGeminiSchema = (schema) => {
|
|
4081
|
+
const next = {};
|
|
4082
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
4083
|
+
if (key === "additionalProperties") {
|
|
4084
|
+
continue;
|
|
4085
|
+
}
|
|
4086
|
+
if (key === "type" && typeof value === "string") {
|
|
4087
|
+
next[key] = value.toUpperCase();
|
|
4088
|
+
continue;
|
|
4089
|
+
}
|
|
4090
|
+
if (Array.isArray(value)) {
|
|
4091
|
+
next[key] = value.map((item) => item && typeof item === "object" ? toGeminiSchema(item) : item);
|
|
4092
|
+
continue;
|
|
4093
|
+
}
|
|
4094
|
+
if (value && typeof value === "object") {
|
|
4095
|
+
next[key] = toGeminiSchema(value);
|
|
4096
|
+
continue;
|
|
4097
|
+
}
|
|
4098
|
+
next[key] = value;
|
|
4099
|
+
}
|
|
4100
|
+
return next;
|
|
4101
|
+
};
|
|
4102
|
+
var messageToGeminiContent = (message) => {
|
|
4103
|
+
if (message.role === "system") {
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
4106
|
+
if (message.role === "tool") {
|
|
4107
|
+
return {
|
|
4108
|
+
parts: [
|
|
4109
|
+
{
|
|
4110
|
+
functionResponse: {
|
|
4111
|
+
id: message.toolCallId,
|
|
4112
|
+
name: message.name ?? "tool",
|
|
4113
|
+
response: {
|
|
4114
|
+
result: parseJSONValue(message.content)
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
],
|
|
4119
|
+
role: "user"
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
const toolCalls = getMessageToolCalls(message);
|
|
4123
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
4124
|
+
return {
|
|
4125
|
+
parts: [
|
|
4126
|
+
...message.content ? [
|
|
4127
|
+
{
|
|
4128
|
+
text: message.content
|
|
4129
|
+
}
|
|
4130
|
+
] : [],
|
|
4131
|
+
...toolCalls.map((toolCall) => ({
|
|
4132
|
+
functionCall: {
|
|
4133
|
+
args: toolCall.args,
|
|
4134
|
+
id: toolCall.id,
|
|
4135
|
+
name: toolCall.name
|
|
4136
|
+
}
|
|
4137
|
+
}))
|
|
4138
|
+
],
|
|
4139
|
+
role: "model"
|
|
4140
|
+
};
|
|
4141
|
+
}
|
|
4142
|
+
return {
|
|
4143
|
+
parts: [
|
|
4144
|
+
{
|
|
4145
|
+
text: message.content
|
|
4146
|
+
}
|
|
4147
|
+
],
|
|
4148
|
+
role: message.role === "assistant" ? "model" : "user"
|
|
4149
|
+
};
|
|
4150
|
+
};
|
|
4151
|
+
var extractText = (response) => {
|
|
4152
|
+
if (typeof response.output_text === "string") {
|
|
4153
|
+
return response.output_text;
|
|
4154
|
+
}
|
|
4155
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
4156
|
+
for (const item of output) {
|
|
4157
|
+
if (!item || typeof item !== "object") {
|
|
4158
|
+
continue;
|
|
4159
|
+
}
|
|
4160
|
+
const record = item;
|
|
4161
|
+
const content = Array.isArray(record.content) ? record.content : [];
|
|
4162
|
+
for (const contentItem of content) {
|
|
4163
|
+
if (!contentItem || typeof contentItem !== "object") {
|
|
4164
|
+
continue;
|
|
4165
|
+
}
|
|
4166
|
+
const contentRecord = contentItem;
|
|
4167
|
+
if (typeof contentRecord.text === "string") {
|
|
4168
|
+
return contentRecord.text;
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
return "";
|
|
4173
|
+
};
|
|
4174
|
+
var extractToolCalls = (response) => {
|
|
4175
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
4176
|
+
const toolCalls = [];
|
|
4177
|
+
for (const item of output) {
|
|
4178
|
+
if (!item || typeof item !== "object") {
|
|
4179
|
+
continue;
|
|
4180
|
+
}
|
|
4181
|
+
const record = item;
|
|
4182
|
+
if (record.type !== "function_call" || typeof record.name !== "string") {
|
|
4183
|
+
continue;
|
|
4184
|
+
}
|
|
4185
|
+
const args = typeof record.arguments === "string" ? parseJSON(record.arguments) : {};
|
|
4186
|
+
toolCalls.push({
|
|
4187
|
+
args,
|
|
4188
|
+
id: typeof record.call_id === "string" ? record.call_id : typeof record.id === "string" ? record.id : undefined,
|
|
4189
|
+
name: record.name
|
|
4190
|
+
});
|
|
4191
|
+
}
|
|
4192
|
+
return toolCalls;
|
|
4193
|
+
};
|
|
4194
|
+
var createOpenAIVoiceAssistantModel = (options) => {
|
|
4195
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
4196
|
+
const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
|
|
4197
|
+
const model = options.model ?? "gpt-4.1-mini";
|
|
4198
|
+
return {
|
|
4199
|
+
generate: async (input) => {
|
|
4200
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
|
|
4201
|
+
body: JSON.stringify({
|
|
4202
|
+
input: messagesToOpenAIInput(input.messages),
|
|
4203
|
+
instructions: [
|
|
4204
|
+
input.system,
|
|
4205
|
+
"Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools."
|
|
4206
|
+
].filter(Boolean).join(`
|
|
4207
|
+
|
|
4208
|
+
`),
|
|
4209
|
+
max_output_tokens: options.maxOutputTokens,
|
|
4210
|
+
model,
|
|
4211
|
+
temperature: options.temperature,
|
|
4212
|
+
text: {
|
|
4213
|
+
format: {
|
|
4214
|
+
name: "voice_route_result",
|
|
4215
|
+
schema: OUTPUT_SCHEMA,
|
|
4216
|
+
strict: false,
|
|
4217
|
+
type: "json_schema"
|
|
4218
|
+
}
|
|
4219
|
+
},
|
|
4220
|
+
tool_choice: input.tools.length ? "auto" : "none",
|
|
4221
|
+
tools: input.tools.map((tool) => ({
|
|
4222
|
+
description: tool.description,
|
|
4223
|
+
name: tool.name,
|
|
4224
|
+
parameters: tool.parameters ?? {
|
|
4225
|
+
additionalProperties: true,
|
|
4226
|
+
type: "object"
|
|
4227
|
+
},
|
|
4228
|
+
strict: false,
|
|
4229
|
+
type: "function"
|
|
4230
|
+
}))
|
|
4231
|
+
}),
|
|
4232
|
+
headers: {
|
|
4233
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
4234
|
+
"content-type": "application/json"
|
|
4235
|
+
},
|
|
4236
|
+
method: "POST"
|
|
4237
|
+
});
|
|
4238
|
+
if (!response.ok) {
|
|
4239
|
+
throw createHTTPError("OpenAI", response);
|
|
4240
|
+
}
|
|
4241
|
+
const body = await response.json();
|
|
4242
|
+
if (body.usage && typeof body.usage === "object") {
|
|
4243
|
+
await options.onUsage?.(body.usage);
|
|
4244
|
+
}
|
|
4245
|
+
const toolCalls = extractToolCalls(body);
|
|
4246
|
+
if (toolCalls.length) {
|
|
4247
|
+
return {
|
|
4248
|
+
toolCalls
|
|
4249
|
+
};
|
|
4250
|
+
}
|
|
4251
|
+
return normalizeRouteOutput(parseJSON(extractText(body)));
|
|
4252
|
+
}
|
|
4253
|
+
};
|
|
4254
|
+
};
|
|
4255
|
+
var extractAnthropicText = (response) => {
|
|
4256
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
4257
|
+
return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
|
|
4258
|
+
`);
|
|
4259
|
+
};
|
|
4260
|
+
var extractAnthropicToolCalls = (response) => {
|
|
4261
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
4262
|
+
const toolCalls = [];
|
|
4263
|
+
for (const item of content) {
|
|
4264
|
+
if (!item || typeof item !== "object") {
|
|
4265
|
+
continue;
|
|
4266
|
+
}
|
|
4267
|
+
const record = item;
|
|
4268
|
+
if (record.type !== "tool_use" || typeof record.name !== "string") {
|
|
4269
|
+
continue;
|
|
4270
|
+
}
|
|
4271
|
+
toolCalls.push({
|
|
4272
|
+
args: record.input && typeof record.input === "object" ? record.input : {},
|
|
4273
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
4274
|
+
name: record.name
|
|
4275
|
+
});
|
|
4276
|
+
}
|
|
4277
|
+
return toolCalls;
|
|
4278
|
+
};
|
|
4279
|
+
var createAnthropicVoiceAssistantModel = (options) => {
|
|
4280
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
4281
|
+
const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
|
|
4282
|
+
const model = options.model ?? "claude-sonnet-4-5";
|
|
4283
|
+
return {
|
|
4284
|
+
generate: async (input) => {
|
|
4285
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
|
|
4286
|
+
body: JSON.stringify({
|
|
4287
|
+
max_tokens: options.maxOutputTokens ?? 1024,
|
|
4288
|
+
messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
|
|
4289
|
+
model,
|
|
4290
|
+
system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
4291
|
+
|
|
4292
|
+
`),
|
|
4293
|
+
temperature: options.temperature,
|
|
4294
|
+
tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
|
|
4295
|
+
tools: input.tools.map((tool) => ({
|
|
4296
|
+
description: tool.description,
|
|
4297
|
+
input_schema: tool.parameters ?? {
|
|
4298
|
+
additionalProperties: true,
|
|
4299
|
+
type: "object"
|
|
4300
|
+
},
|
|
4301
|
+
name: tool.name
|
|
4302
|
+
}))
|
|
4303
|
+
}),
|
|
4304
|
+
headers: {
|
|
4305
|
+
"anthropic-version": options.version ?? "2023-06-01",
|
|
4306
|
+
"content-type": "application/json",
|
|
4307
|
+
"x-api-key": options.apiKey
|
|
4308
|
+
},
|
|
4309
|
+
method: "POST"
|
|
4310
|
+
});
|
|
4311
|
+
if (!response.ok) {
|
|
4312
|
+
throw createHTTPError("Anthropic", response);
|
|
4313
|
+
}
|
|
4314
|
+
const body = await response.json();
|
|
4315
|
+
if (body.usage && typeof body.usage === "object") {
|
|
4316
|
+
await options.onUsage?.(body.usage);
|
|
4317
|
+
}
|
|
4318
|
+
const toolCalls = extractAnthropicToolCalls(body);
|
|
4319
|
+
if (toolCalls.length) {
|
|
4320
|
+
return {
|
|
4321
|
+
assistantText: extractAnthropicText(body) || undefined,
|
|
4322
|
+
toolCalls
|
|
4323
|
+
};
|
|
4324
|
+
}
|
|
4325
|
+
return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
|
|
4326
|
+
}
|
|
4327
|
+
};
|
|
4328
|
+
};
|
|
4329
|
+
var extractGeminiCandidateParts = (response) => {
|
|
4330
|
+
const candidates = Array.isArray(response.candidates) ? response.candidates : [];
|
|
4331
|
+
const first = candidates[0];
|
|
4332
|
+
if (!first || typeof first !== "object") {
|
|
4333
|
+
return [];
|
|
4334
|
+
}
|
|
4335
|
+
const content = first.content;
|
|
4336
|
+
if (!content || typeof content !== "object") {
|
|
4337
|
+
return [];
|
|
4338
|
+
}
|
|
4339
|
+
const parts = content.parts;
|
|
4340
|
+
return Array.isArray(parts) ? parts : [];
|
|
4341
|
+
};
|
|
4342
|
+
var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
4343
|
+
`);
|
|
4344
|
+
var extractGeminiToolCalls = (response) => {
|
|
4345
|
+
const toolCalls = [];
|
|
4346
|
+
for (const part of extractGeminiCandidateParts(response)) {
|
|
4347
|
+
if (!part || typeof part !== "object") {
|
|
4348
|
+
continue;
|
|
4349
|
+
}
|
|
4350
|
+
const functionCall = part.functionCall;
|
|
4351
|
+
if (!functionCall || typeof functionCall !== "object") {
|
|
4352
|
+
continue;
|
|
4353
|
+
}
|
|
4354
|
+
const record = functionCall;
|
|
4355
|
+
if (typeof record.name !== "string") {
|
|
4356
|
+
continue;
|
|
4357
|
+
}
|
|
4358
|
+
toolCalls.push({
|
|
4359
|
+
args: record.args && typeof record.args === "object" ? record.args : {},
|
|
4360
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
4361
|
+
name: record.name
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
return toolCalls;
|
|
4365
|
+
};
|
|
4366
|
+
var createGeminiVoiceAssistantModel = (options) => {
|
|
4367
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
4368
|
+
const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
4369
|
+
const model = options.model ?? "gemini-2.5-flash";
|
|
4370
|
+
const maxRetries = Math.max(0, options.maxRetries ?? 2);
|
|
4371
|
+
return {
|
|
4372
|
+
generate: async (input) => {
|
|
4373
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
|
|
4374
|
+
let response;
|
|
4375
|
+
for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
|
|
4376
|
+
response = await fetchImpl(endpoint, {
|
|
4377
|
+
body: JSON.stringify({
|
|
4378
|
+
contents: input.messages.map(messageToGeminiContent).filter(Boolean),
|
|
4379
|
+
generationConfig: {
|
|
4380
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
4381
|
+
...input.tools.length ? {} : {
|
|
4382
|
+
responseMimeType: "application/json",
|
|
4383
|
+
responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
|
|
4384
|
+
},
|
|
4385
|
+
temperature: options.temperature
|
|
4386
|
+
},
|
|
4387
|
+
systemInstruction: {
|
|
4388
|
+
parts: [
|
|
4389
|
+
{
|
|
4390
|
+
text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
4391
|
+
|
|
4392
|
+
`)
|
|
4393
|
+
}
|
|
4394
|
+
]
|
|
4395
|
+
},
|
|
4396
|
+
tools: input.tools.length ? [
|
|
4397
|
+
{
|
|
4398
|
+
functionDeclarations: input.tools.map((tool) => ({
|
|
4399
|
+
description: tool.description,
|
|
4400
|
+
name: tool.name,
|
|
4401
|
+
parameters: toGeminiSchema(tool.parameters ?? {
|
|
4402
|
+
additionalProperties: true,
|
|
4403
|
+
type: "object"
|
|
4404
|
+
})
|
|
4405
|
+
}))
|
|
4406
|
+
}
|
|
4407
|
+
] : undefined
|
|
4408
|
+
}),
|
|
4409
|
+
headers: {
|
|
4410
|
+
"content-type": "application/json"
|
|
4411
|
+
},
|
|
4412
|
+
method: "POST"
|
|
4413
|
+
});
|
|
4414
|
+
if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
|
|
4415
|
+
break;
|
|
4416
|
+
}
|
|
4417
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
4418
|
+
await sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
|
|
4419
|
+
}
|
|
4420
|
+
if (!response) {
|
|
4421
|
+
throw new Error("Gemini voice assistant model failed: no response");
|
|
4422
|
+
}
|
|
4423
|
+
if (!response.ok) {
|
|
4424
|
+
throw createHTTPError("Gemini", response);
|
|
4425
|
+
}
|
|
4426
|
+
const body = await response.json();
|
|
4427
|
+
if (body.usageMetadata && typeof body.usageMetadata === "object") {
|
|
4428
|
+
await options.onUsage?.(body.usageMetadata);
|
|
4429
|
+
}
|
|
4430
|
+
const toolCalls = extractGeminiToolCalls(body);
|
|
4431
|
+
if (toolCalls.length) {
|
|
4432
|
+
return {
|
|
4433
|
+
assistantText: extractGeminiText(body) || undefined,
|
|
4434
|
+
toolCalls
|
|
4435
|
+
};
|
|
4436
|
+
}
|
|
4437
|
+
return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
|
|
4438
|
+
}
|
|
4439
|
+
};
|
|
4440
|
+
};
|
|
4441
|
+
|
|
3471
4442
|
// src/store.ts
|
|
3472
4443
|
var createId = () => crypto.randomUUID();
|
|
3473
4444
|
var createVoiceSessionRecord = (id, scenarioId) => ({
|
|
@@ -3508,6 +4479,118 @@ var toVoiceSessionSummary = (session) => ({
|
|
|
3508
4479
|
turnCount: session.turns.length
|
|
3509
4480
|
});
|
|
3510
4481
|
|
|
4482
|
+
// src/testing/providerSimulator.ts
|
|
4483
|
+
var getContextQuery = (context) => context.query;
|
|
4484
|
+
var titleCaseProvider = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ");
|
|
4485
|
+
var resolveRequestedProvider = (context, providers) => {
|
|
4486
|
+
const provider = getContextQuery(context).provider;
|
|
4487
|
+
return providers.includes(provider) ? provider : providers[0];
|
|
4488
|
+
};
|
|
4489
|
+
var createVoiceProviderFailureSimulator = (options) => {
|
|
4490
|
+
if (options.providers.length === 0) {
|
|
4491
|
+
throw new Error("At least one provider is required.");
|
|
4492
|
+
}
|
|
4493
|
+
const providerModels = Object.fromEntries(options.providers.map((provider) => [
|
|
4494
|
+
provider,
|
|
4495
|
+
{
|
|
4496
|
+
generate: async (input) => {
|
|
4497
|
+
const query = getContextQuery(input.context);
|
|
4498
|
+
if (provider === query.simulateFailureProvider) {
|
|
4499
|
+
const label = options.providerLabel?.(provider) ?? titleCaseProvider(provider);
|
|
4500
|
+
throw new Error(`${label} voice assistant model failed: HTTP 429`);
|
|
4501
|
+
}
|
|
4502
|
+
if (options.response) {
|
|
4503
|
+
return options.response({
|
|
4504
|
+
...input,
|
|
4505
|
+
mode: query.recoverProvider === provider ? "recovery" : "failure",
|
|
4506
|
+
provider
|
|
4507
|
+
});
|
|
4508
|
+
}
|
|
4509
|
+
return {
|
|
4510
|
+
assistantText: `Simulated ${provider} provider recovered.`
|
|
4511
|
+
};
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
]));
|
|
4515
|
+
const router = createVoiceProviderRouter({
|
|
4516
|
+
allowProviders: async (input) => {
|
|
4517
|
+
const recoverProvider = getContextQuery(input.context).recoverProvider;
|
|
4518
|
+
if (recoverProvider) {
|
|
4519
|
+
return [recoverProvider];
|
|
4520
|
+
}
|
|
4521
|
+
if (typeof options.allowProviders === "function") {
|
|
4522
|
+
return options.allowProviders(input);
|
|
4523
|
+
}
|
|
4524
|
+
return options.allowProviders ?? options.providers;
|
|
4525
|
+
},
|
|
4526
|
+
fallback: async (input) => {
|
|
4527
|
+
const selectedProvider = resolveRequestedProvider(input.context, options.providers);
|
|
4528
|
+
if (typeof options.fallback === "function") {
|
|
4529
|
+
return options.fallback(selectedProvider, input);
|
|
4530
|
+
}
|
|
4531
|
+
return options.fallback ?? options.providers.filter((provider) => provider !== selectedProvider);
|
|
4532
|
+
},
|
|
4533
|
+
fallbackMode: "provider-error",
|
|
4534
|
+
isProviderError: options.isProviderError,
|
|
4535
|
+
isRateLimitError: options.isRateLimitError,
|
|
4536
|
+
onProviderEvent: options.onProviderEvent,
|
|
4537
|
+
policy: "prefer-selected",
|
|
4538
|
+
providerHealth: options.providerHealth ?? {
|
|
4539
|
+
cooldownMs: 30000,
|
|
4540
|
+
failureThreshold: 1,
|
|
4541
|
+
rateLimitCooldownMs: 120000
|
|
4542
|
+
},
|
|
4543
|
+
providers: providerModels,
|
|
4544
|
+
selectProvider: ({ context }) => resolveRequestedProvider(context, options.providers)
|
|
4545
|
+
});
|
|
4546
|
+
const run = async (provider, mode) => {
|
|
4547
|
+
const now = Date.now();
|
|
4548
|
+
const session = createVoiceSessionRecord(`provider-sim-${now}`, "provider-simulation");
|
|
4549
|
+
const turn = {
|
|
4550
|
+
committedAt: now,
|
|
4551
|
+
id: `provider-sim-turn-${now}`,
|
|
4552
|
+
text: mode === "failure" ? `Simulate ${provider} provider failure.` : `Simulate ${provider} provider recovery.`,
|
|
4553
|
+
transcripts: []
|
|
4554
|
+
};
|
|
4555
|
+
const context = {
|
|
4556
|
+
query: {
|
|
4557
|
+
provider,
|
|
4558
|
+
...mode === "recovery" ? { recoverProvider: provider } : {},
|
|
4559
|
+
...mode === "failure" ? { simulateFailureProvider: provider } : {}
|
|
4560
|
+
}
|
|
4561
|
+
};
|
|
4562
|
+
const result = await router.generate({
|
|
4563
|
+
agentId: "provider-simulator",
|
|
4564
|
+
context,
|
|
4565
|
+
messages: [
|
|
4566
|
+
{
|
|
4567
|
+
content: turn.text,
|
|
4568
|
+
role: "user"
|
|
4569
|
+
}
|
|
4570
|
+
],
|
|
4571
|
+
session,
|
|
4572
|
+
system: "Simulate provider routing without calling external APIs.",
|
|
4573
|
+
tools: [],
|
|
4574
|
+
turn
|
|
4575
|
+
});
|
|
4576
|
+
return {
|
|
4577
|
+
mode,
|
|
4578
|
+
provider,
|
|
4579
|
+
replayHref: options.replayHref === false ? undefined : typeof options.replayHref === "function" ? options.replayHref({
|
|
4580
|
+
provider,
|
|
4581
|
+
sessionId: session.id,
|
|
4582
|
+
turnId: turn.id
|
|
4583
|
+
}) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(session.id)}/replay/htmx`,
|
|
4584
|
+
result,
|
|
4585
|
+
sessionId: session.id,
|
|
4586
|
+
status: "simulated",
|
|
4587
|
+
turnId: turn.id
|
|
4588
|
+
};
|
|
4589
|
+
};
|
|
4590
|
+
return {
|
|
4591
|
+
run
|
|
4592
|
+
};
|
|
4593
|
+
};
|
|
3511
4594
|
// src/memoryStore.ts
|
|
3512
4595
|
var createVoiceMemoryStore = () => {
|
|
3513
4596
|
const sessions = new Map;
|
|
@@ -3533,6 +4616,289 @@ var createVoiceMemoryStore = () => {
|
|
|
3533
4616
|
// src/session.ts
|
|
3534
4617
|
import { Buffer } from "buffer";
|
|
3535
4618
|
|
|
4619
|
+
// src/handoff.ts
|
|
4620
|
+
var toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4621
|
+
var signHandoffBody = async (input) => {
|
|
4622
|
+
const encoder = new TextEncoder;
|
|
4623
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
4624
|
+
hash: "SHA-256",
|
|
4625
|
+
name: "HMAC"
|
|
4626
|
+
}, false, ["sign"]);
|
|
4627
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
|
|
4628
|
+
return `sha256=${toHex(new Uint8Array(signature))}`;
|
|
4629
|
+
};
|
|
4630
|
+
var toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
4631
|
+
var createSkippedDelivery = (adapter) => ({
|
|
4632
|
+
adapterId: adapter.id,
|
|
4633
|
+
adapterKind: adapter.kind,
|
|
4634
|
+
status: "skipped"
|
|
4635
|
+
});
|
|
4636
|
+
var aggregateHandoffStatus = (deliveries) => {
|
|
4637
|
+
const statuses = Object.values(deliveries).map((delivery) => delivery.status);
|
|
4638
|
+
if (statuses.some((status) => status === "failed")) {
|
|
4639
|
+
return "failed";
|
|
4640
|
+
}
|
|
4641
|
+
if (statuses.some((status) => status === "delivered")) {
|
|
4642
|
+
return "delivered";
|
|
4643
|
+
}
|
|
4644
|
+
return "skipped";
|
|
4645
|
+
};
|
|
4646
|
+
var createHandoffDeliveryId = (input) => [
|
|
4647
|
+
"voice-handoff",
|
|
4648
|
+
input.sessionId,
|
|
4649
|
+
input.action,
|
|
4650
|
+
Date.now(),
|
|
4651
|
+
crypto.randomUUID()
|
|
4652
|
+
].join(":");
|
|
4653
|
+
var resolveHandoffDeliveryError = (deliveries) => Object.values(deliveries).map((delivery) => delivery.error).find(Boolean);
|
|
4654
|
+
var defaultWebhookBody = (input) => ({
|
|
4655
|
+
action: input.action,
|
|
4656
|
+
metadata: input.metadata,
|
|
4657
|
+
reason: input.reason,
|
|
4658
|
+
result: input.result,
|
|
4659
|
+
session: {
|
|
4660
|
+
id: input.session.id,
|
|
4661
|
+
scenarioId: input.session.scenarioId,
|
|
4662
|
+
status: input.session.status
|
|
4663
|
+
},
|
|
4664
|
+
source: "absolutejs-voice",
|
|
4665
|
+
target: input.target
|
|
4666
|
+
});
|
|
4667
|
+
var deliverVoiceHandoff = async (input) => {
|
|
4668
|
+
if (!input.config || input.config.adapters.length === 0) {
|
|
4669
|
+
return;
|
|
4670
|
+
}
|
|
4671
|
+
const deliveries = {};
|
|
4672
|
+
for (const adapter of input.config.adapters) {
|
|
4673
|
+
if (adapter.actions && !adapter.actions.includes(input.handoff.action)) {
|
|
4674
|
+
deliveries[adapter.id] = createSkippedDelivery(adapter);
|
|
4675
|
+
continue;
|
|
4676
|
+
}
|
|
4677
|
+
try {
|
|
4678
|
+
const result = await adapter.handoff(input.handoff);
|
|
4679
|
+
deliveries[adapter.id] = {
|
|
4680
|
+
...result,
|
|
4681
|
+
adapterId: adapter.id,
|
|
4682
|
+
adapterKind: adapter.kind
|
|
4683
|
+
};
|
|
4684
|
+
} catch (error) {
|
|
4685
|
+
deliveries[adapter.id] = {
|
|
4686
|
+
adapterId: adapter.id,
|
|
4687
|
+
adapterKind: adapter.kind,
|
|
4688
|
+
error: toErrorMessage(error),
|
|
4689
|
+
status: "failed"
|
|
4690
|
+
};
|
|
4691
|
+
if (input.config.failMode === "throw") {
|
|
4692
|
+
throw error;
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
return {
|
|
4697
|
+
action: input.handoff.action,
|
|
4698
|
+
deliveries,
|
|
4699
|
+
status: aggregateHandoffStatus(deliveries)
|
|
4700
|
+
};
|
|
4701
|
+
};
|
|
4702
|
+
var createVoiceHandoffDeliveryRecord = (input) => {
|
|
4703
|
+
const now = Date.now();
|
|
4704
|
+
return {
|
|
4705
|
+
action: input.action,
|
|
4706
|
+
context: input.context,
|
|
4707
|
+
createdAt: now,
|
|
4708
|
+
deliveryAttempts: 0,
|
|
4709
|
+
deliveryStatus: "pending",
|
|
4710
|
+
id: input.id ?? createHandoffDeliveryId({
|
|
4711
|
+
action: input.action,
|
|
4712
|
+
sessionId: input.session.id
|
|
4713
|
+
}),
|
|
4714
|
+
metadata: input.metadata,
|
|
4715
|
+
reason: input.reason,
|
|
4716
|
+
result: input.result,
|
|
4717
|
+
session: input.session,
|
|
4718
|
+
sessionId: input.session.id,
|
|
4719
|
+
target: input.target,
|
|
4720
|
+
updatedAt: now
|
|
4721
|
+
};
|
|
4722
|
+
};
|
|
4723
|
+
var applyVoiceHandoffDeliveryResult = (delivery, result) => ({
|
|
4724
|
+
...delivery,
|
|
4725
|
+
deliveredAt: result.status === "delivered" || result.status === "skipped" ? Date.now() : delivery.deliveredAt,
|
|
4726
|
+
deliveries: result.deliveries,
|
|
4727
|
+
deliveryAttempts: (delivery.deliveryAttempts ?? 0) + 1,
|
|
4728
|
+
deliveryError: result.status === "failed" ? resolveHandoffDeliveryError(result.deliveries) : undefined,
|
|
4729
|
+
deliveryStatus: result.status,
|
|
4730
|
+
updatedAt: Date.now()
|
|
4731
|
+
});
|
|
4732
|
+
var deliverVoiceHandoffDelivery = async (options) => {
|
|
4733
|
+
const result = await deliverVoiceHandoff({
|
|
4734
|
+
config: {
|
|
4735
|
+
adapters: options.adapters,
|
|
4736
|
+
failMode: options.failMode
|
|
4737
|
+
},
|
|
4738
|
+
handoff: {
|
|
4739
|
+
action: options.delivery.action,
|
|
4740
|
+
api: options.api,
|
|
4741
|
+
context: options.delivery.context,
|
|
4742
|
+
metadata: options.delivery.metadata,
|
|
4743
|
+
reason: options.delivery.reason,
|
|
4744
|
+
result: options.delivery.result,
|
|
4745
|
+
session: options.delivery.session,
|
|
4746
|
+
target: options.delivery.target
|
|
4747
|
+
}
|
|
4748
|
+
});
|
|
4749
|
+
return result ? applyVoiceHandoffDeliveryResult(options.delivery, result) : {
|
|
4750
|
+
...options.delivery,
|
|
4751
|
+
deliveryAttempts: (options.delivery.deliveryAttempts ?? 0) + 1,
|
|
4752
|
+
deliveryStatus: "skipped",
|
|
4753
|
+
updatedAt: Date.now()
|
|
4754
|
+
};
|
|
4755
|
+
};
|
|
4756
|
+
var createVoiceMemoryHandoffDeliveryStore = () => {
|
|
4757
|
+
const deliveries = new Map;
|
|
4758
|
+
return {
|
|
4759
|
+
get: async (id) => deliveries.get(id),
|
|
4760
|
+
list: async () => [...deliveries.values()].sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)),
|
|
4761
|
+
remove: async (id) => {
|
|
4762
|
+
deliveries.delete(id);
|
|
4763
|
+
},
|
|
4764
|
+
set: async (id, delivery) => {
|
|
4765
|
+
deliveries.set(id, delivery);
|
|
4766
|
+
}
|
|
4767
|
+
};
|
|
4768
|
+
};
|
|
4769
|
+
var createVoiceWebhookHandoffAdapter = (options) => ({
|
|
4770
|
+
actions: options.actions,
|
|
4771
|
+
handoff: async (input) => {
|
|
4772
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
4773
|
+
if (typeof fetchImpl !== "function") {
|
|
4774
|
+
return {
|
|
4775
|
+
deliveredTo: options.url,
|
|
4776
|
+
error: "Handoff delivery failed: fetch is not available in this runtime.",
|
|
4777
|
+
status: "failed"
|
|
4778
|
+
};
|
|
4779
|
+
}
|
|
4780
|
+
const body = JSON.stringify(await options.body?.(input) ?? defaultWebhookBody(input));
|
|
4781
|
+
const headers = {
|
|
4782
|
+
"content-type": "application/json",
|
|
4783
|
+
...options.headers
|
|
4784
|
+
};
|
|
4785
|
+
if (options.signingSecret) {
|
|
4786
|
+
const timestamp = String(Date.now());
|
|
4787
|
+
headers["x-absolutejs-timestamp"] = timestamp;
|
|
4788
|
+
headers["x-absolutejs-signature"] = await signHandoffBody({
|
|
4789
|
+
body,
|
|
4790
|
+
secret: options.signingSecret,
|
|
4791
|
+
timestamp
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
4794
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
|
|
4795
|
+
const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
4796
|
+
try {
|
|
4797
|
+
const response = await fetchImpl(options.url, {
|
|
4798
|
+
body,
|
|
4799
|
+
headers,
|
|
4800
|
+
method: options.method ?? "POST",
|
|
4801
|
+
signal: controller?.signal
|
|
4802
|
+
});
|
|
4803
|
+
if (!response.ok) {
|
|
4804
|
+
return {
|
|
4805
|
+
deliveredTo: options.url,
|
|
4806
|
+
error: `Handoff delivery failed with response ${response.status}.`,
|
|
4807
|
+
status: "failed"
|
|
4808
|
+
};
|
|
4809
|
+
}
|
|
4810
|
+
return {
|
|
4811
|
+
deliveredAt: Date.now(),
|
|
4812
|
+
deliveredTo: options.url,
|
|
4813
|
+
status: "delivered"
|
|
4814
|
+
};
|
|
4815
|
+
} finally {
|
|
4816
|
+
if (timeout) {
|
|
4817
|
+
clearTimeout(timeout);
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
},
|
|
4821
|
+
id: options.id,
|
|
4822
|
+
kind: options.kind ?? "webhook"
|
|
4823
|
+
});
|
|
4824
|
+
var escapeXml = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
4825
|
+
var defaultTwilioTransferTwiML = (input) => {
|
|
4826
|
+
if (!input.target) {
|
|
4827
|
+
return "<Response><Hangup /></Response>";
|
|
4828
|
+
}
|
|
4829
|
+
return `<Response><Dial>${escapeXml(input.target)}</Dial></Response>`;
|
|
4830
|
+
};
|
|
4831
|
+
var resolveTwilioCallSid = async (resolver, input) => {
|
|
4832
|
+
if (typeof resolver === "function") {
|
|
4833
|
+
return resolver(input);
|
|
4834
|
+
}
|
|
4835
|
+
if (typeof resolver === "string" && resolver.length > 0) {
|
|
4836
|
+
return resolver;
|
|
4837
|
+
}
|
|
4838
|
+
const metadataSid = typeof input.metadata?.callSid === "string" ? input.metadata.callSid : undefined;
|
|
4839
|
+
const sessionMetadata = input.session.metadata && typeof input.session.metadata === "object" ? input.session.metadata : undefined;
|
|
4840
|
+
const sessionSid = typeof sessionMetadata?.callSid === "string" ? sessionMetadata.callSid : undefined;
|
|
4841
|
+
return metadataSid ?? sessionSid;
|
|
4842
|
+
};
|
|
4843
|
+
var createVoiceTwilioRedirectHandoffAdapter = (options) => ({
|
|
4844
|
+
actions: options.actions ?? ["transfer"],
|
|
4845
|
+
handoff: async (input) => {
|
|
4846
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
4847
|
+
const callSid = await resolveTwilioCallSid(options.callSid, input);
|
|
4848
|
+
if (!callSid) {
|
|
4849
|
+
return {
|
|
4850
|
+
error: "Twilio handoff requires a callSid.",
|
|
4851
|
+
status: "failed"
|
|
4852
|
+
};
|
|
4853
|
+
}
|
|
4854
|
+
if (typeof fetchImpl !== "function") {
|
|
4855
|
+
return {
|
|
4856
|
+
error: "Twilio handoff failed: fetch is not available in this runtime.",
|
|
4857
|
+
status: "failed"
|
|
4858
|
+
};
|
|
4859
|
+
}
|
|
4860
|
+
const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(options.accountSid)}/Calls/${encodeURIComponent(callSid)}.json`;
|
|
4861
|
+
const body = new URLSearchParams({
|
|
4862
|
+
Twiml: await (options.buildTwiML?.(input) ?? defaultTwilioTransferTwiML(input))
|
|
4863
|
+
});
|
|
4864
|
+
const auth = btoa(`${options.accountSid}:${options.authToken}`);
|
|
4865
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
|
|
4866
|
+
const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
4867
|
+
try {
|
|
4868
|
+
const response = await fetchImpl(url, {
|
|
4869
|
+
body,
|
|
4870
|
+
headers: {
|
|
4871
|
+
authorization: `Basic ${auth}`,
|
|
4872
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
4873
|
+
},
|
|
4874
|
+
method: "POST",
|
|
4875
|
+
signal: controller?.signal
|
|
4876
|
+
});
|
|
4877
|
+
if (!response.ok) {
|
|
4878
|
+
return {
|
|
4879
|
+
deliveredTo: url,
|
|
4880
|
+
error: `Twilio handoff failed with response ${response.status}.`,
|
|
4881
|
+
status: "failed"
|
|
4882
|
+
};
|
|
4883
|
+
}
|
|
4884
|
+
return {
|
|
4885
|
+
deliveredAt: Date.now(),
|
|
4886
|
+
deliveredTo: url,
|
|
4887
|
+
metadata: {
|
|
4888
|
+
callSid
|
|
4889
|
+
},
|
|
4890
|
+
status: "delivered"
|
|
4891
|
+
};
|
|
4892
|
+
} finally {
|
|
4893
|
+
if (timeout) {
|
|
4894
|
+
clearTimeout(timeout);
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
},
|
|
4898
|
+
id: options.id ?? "twilio-redirect",
|
|
4899
|
+
kind: "twilio-redirect"
|
|
4900
|
+
});
|
|
4901
|
+
|
|
3536
4902
|
// src/logger.ts
|
|
3537
4903
|
var noop2 = () => {};
|
|
3538
4904
|
var createNoopLogger = () => ({
|
|
@@ -3725,6 +5091,7 @@ var pushCallLifecycleEvent = (session, input) => {
|
|
|
3725
5091
|
}
|
|
3726
5092
|
return lifecycle;
|
|
3727
5093
|
};
|
|
5094
|
+
var getLatestCallLifecycleEvent = (session) => session.call?.events.at(-1);
|
|
3728
5095
|
var createVoiceSession = (options) => {
|
|
3729
5096
|
const logger = resolveLogger(options.logger);
|
|
3730
5097
|
const reconnect = {
|
|
@@ -3825,6 +5192,64 @@ var createVoiceSession = (options) => {
|
|
|
3825
5192
|
});
|
|
3826
5193
|
}
|
|
3827
5194
|
};
|
|
5195
|
+
const sendCallLifecycle = async (session) => {
|
|
5196
|
+
const event = getLatestCallLifecycleEvent(session);
|
|
5197
|
+
if (!event) {
|
|
5198
|
+
return;
|
|
5199
|
+
}
|
|
5200
|
+
await send({
|
|
5201
|
+
event,
|
|
5202
|
+
sessionId: options.id,
|
|
5203
|
+
type: "call_lifecycle"
|
|
5204
|
+
});
|
|
5205
|
+
};
|
|
5206
|
+
const runHandoff = async (input) => {
|
|
5207
|
+
const queuedDelivery = options.handoff?.deliveryQueue ? createVoiceHandoffDeliveryRecord({
|
|
5208
|
+
action: input.action,
|
|
5209
|
+
context: options.context,
|
|
5210
|
+
metadata: input.metadata,
|
|
5211
|
+
reason: input.reason,
|
|
5212
|
+
result: input.result,
|
|
5213
|
+
session: input.session,
|
|
5214
|
+
target: input.target
|
|
5215
|
+
}) : undefined;
|
|
5216
|
+
if (queuedDelivery) {
|
|
5217
|
+
await options.handoff?.deliveryQueue?.set(queuedDelivery.id, queuedDelivery);
|
|
5218
|
+
}
|
|
5219
|
+
if (options.handoff?.enqueueOnly) {
|
|
5220
|
+
return;
|
|
5221
|
+
}
|
|
5222
|
+
const result = await deliverVoiceHandoff({
|
|
5223
|
+
config: options.handoff,
|
|
5224
|
+
handoff: {
|
|
5225
|
+
action: input.action,
|
|
5226
|
+
api,
|
|
5227
|
+
context: options.context,
|
|
5228
|
+
metadata: input.metadata,
|
|
5229
|
+
reason: input.reason,
|
|
5230
|
+
result: input.result,
|
|
5231
|
+
session: input.session,
|
|
5232
|
+
target: input.target
|
|
5233
|
+
}
|
|
5234
|
+
});
|
|
5235
|
+
if (!result) {
|
|
5236
|
+
return;
|
|
5237
|
+
}
|
|
5238
|
+
if (queuedDelivery) {
|
|
5239
|
+
const updatedDelivery = applyVoiceHandoffDeliveryResult(queuedDelivery, result);
|
|
5240
|
+
await options.handoff?.deliveryQueue?.set(updatedDelivery.id, updatedDelivery);
|
|
5241
|
+
}
|
|
5242
|
+
await appendTrace({
|
|
5243
|
+
metadata: input.metadata,
|
|
5244
|
+
payload: {
|
|
5245
|
+
...result,
|
|
5246
|
+
reason: input.reason,
|
|
5247
|
+
target: input.target
|
|
5248
|
+
},
|
|
5249
|
+
session: input.session,
|
|
5250
|
+
type: "call.handoff"
|
|
5251
|
+
});
|
|
5252
|
+
};
|
|
3828
5253
|
const readSession = async () => options.store.getOrCreate(options.id);
|
|
3829
5254
|
const writeSession = async (mutate) => {
|
|
3830
5255
|
const session = await options.store.getOrCreate(options.id);
|
|
@@ -4015,6 +5440,7 @@ var createVoiceSession = (options) => {
|
|
|
4015
5440
|
await appendTrace({
|
|
4016
5441
|
payload: {
|
|
4017
5442
|
disposition,
|
|
5443
|
+
metadata: input.metadata,
|
|
4018
5444
|
reason: input.reason,
|
|
4019
5445
|
target: input.target,
|
|
4020
5446
|
type: "end"
|
|
@@ -4022,6 +5448,7 @@ var createVoiceSession = (options) => {
|
|
|
4022
5448
|
session,
|
|
4023
5449
|
type: "call.lifecycle"
|
|
4024
5450
|
});
|
|
5451
|
+
await sendCallLifecycle(session);
|
|
4025
5452
|
await send({
|
|
4026
5453
|
sessionId: options.id,
|
|
4027
5454
|
type: "complete"
|
|
@@ -4101,6 +5528,15 @@ var createVoiceSession = (options) => {
|
|
|
4101
5528
|
session,
|
|
4102
5529
|
type: "call.lifecycle"
|
|
4103
5530
|
});
|
|
5531
|
+
await sendCallLifecycle(session);
|
|
5532
|
+
await runHandoff({
|
|
5533
|
+
action: "transfer",
|
|
5534
|
+
metadata: input.metadata,
|
|
5535
|
+
reason: input.reason,
|
|
5536
|
+
result: input.result,
|
|
5537
|
+
session,
|
|
5538
|
+
target: input.target
|
|
5539
|
+
});
|
|
4104
5540
|
await completeInternal(input.result, {
|
|
4105
5541
|
disposition: "transferred",
|
|
4106
5542
|
invokeOnComplete: false,
|
|
@@ -4126,6 +5562,14 @@ var createVoiceSession = (options) => {
|
|
|
4126
5562
|
session,
|
|
4127
5563
|
type: "call.lifecycle"
|
|
4128
5564
|
});
|
|
5565
|
+
await sendCallLifecycle(session);
|
|
5566
|
+
await runHandoff({
|
|
5567
|
+
action: "escalate",
|
|
5568
|
+
metadata: input.metadata,
|
|
5569
|
+
reason: input.reason,
|
|
5570
|
+
result: input.result,
|
|
5571
|
+
session
|
|
5572
|
+
});
|
|
4129
5573
|
await completeInternal(input.result, {
|
|
4130
5574
|
disposition: "escalated",
|
|
4131
5575
|
invokeOnComplete: false,
|
|
@@ -4148,6 +5592,13 @@ var createVoiceSession = (options) => {
|
|
|
4148
5592
|
session,
|
|
4149
5593
|
type: "call.lifecycle"
|
|
4150
5594
|
});
|
|
5595
|
+
await sendCallLifecycle(session);
|
|
5596
|
+
await runHandoff({
|
|
5597
|
+
action: "no-answer",
|
|
5598
|
+
metadata: input?.metadata,
|
|
5599
|
+
result: input?.result,
|
|
5600
|
+
session
|
|
5601
|
+
});
|
|
4151
5602
|
await completeInternal(input?.result, {
|
|
4152
5603
|
disposition: "no-answer",
|
|
4153
5604
|
invokeOnComplete: false,
|
|
@@ -4169,6 +5620,13 @@ var createVoiceSession = (options) => {
|
|
|
4169
5620
|
session,
|
|
4170
5621
|
type: "call.lifecycle"
|
|
4171
5622
|
});
|
|
5623
|
+
await sendCallLifecycle(session);
|
|
5624
|
+
await runHandoff({
|
|
5625
|
+
action: "voicemail",
|
|
5626
|
+
metadata: input?.metadata,
|
|
5627
|
+
result: input?.result,
|
|
5628
|
+
session
|
|
5629
|
+
});
|
|
4172
5630
|
await completeInternal(input?.result, {
|
|
4173
5631
|
disposition: "voicemail",
|
|
4174
5632
|
invokeOnComplete: false,
|
|
@@ -4955,6 +6413,7 @@ var createVoiceSession = (options) => {
|
|
|
4955
6413
|
session,
|
|
4956
6414
|
type: "call.lifecycle"
|
|
4957
6415
|
});
|
|
6416
|
+
await sendCallLifecycle(session);
|
|
4958
6417
|
}
|
|
4959
6418
|
await send({
|
|
4960
6419
|
sessionId: options.id,
|
|
@@ -5545,7 +7004,7 @@ var createVoiceCallReviewFromLiveTelephonyReport = (report, options = {}) => {
|
|
|
5545
7004
|
}
|
|
5546
7005
|
};
|
|
5547
7006
|
};
|
|
5548
|
-
var
|
|
7007
|
+
var toErrorMessage2 = (error) => {
|
|
5549
7008
|
if (typeof error === "string" && error.trim().length > 0) {
|
|
5550
7009
|
return error;
|
|
5551
7010
|
}
|
|
@@ -5632,7 +7091,7 @@ var createVoiceCallReviewRecorder = (options = {}) => {
|
|
|
5632
7091
|
};
|
|
5633
7092
|
},
|
|
5634
7093
|
recordError: (error) => {
|
|
5635
|
-
const message =
|
|
7094
|
+
const message = toErrorMessage2(error);
|
|
5636
7095
|
errors.push(message);
|
|
5637
7096
|
push("turn", "error", {
|
|
5638
7097
|
reason: message
|
|
@@ -6341,7 +7800,7 @@ var runVoiceSessionBenchmarkSeries = async (input) => {
|
|
|
6341
7800
|
import { Buffer as Buffer2 } from "buffer";
|
|
6342
7801
|
var TWILIO_MULAW_SAMPLE_RATE = 8000;
|
|
6343
7802
|
var VOICE_PCM_SAMPLE_RATE = 16000;
|
|
6344
|
-
var
|
|
7803
|
+
var escapeXml2 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
6345
7804
|
var normalizeOnTurn = (handler) => {
|
|
6346
7805
|
if (handler.length > 1) {
|
|
6347
7806
|
const directHandler = handler;
|
|
@@ -6537,8 +7996,8 @@ var createTwilioSocketAdapter = (socket, getState) => ({
|
|
|
6537
7996
|
}
|
|
6538
7997
|
});
|
|
6539
7998
|
var createTwilioVoiceResponse = (options) => {
|
|
6540
|
-
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${
|
|
6541
|
-
return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${
|
|
7999
|
+
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
|
|
8000
|
+
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>`;
|
|
6542
8001
|
};
|
|
6543
8002
|
var createTwilioMediaStreamBridge = (socket, options) => {
|
|
6544
8003
|
const runtimePreset = resolveVoiceRuntimePreset(options.preset);
|
|
@@ -7208,6 +8667,8 @@ export {
|
|
|
7208
8667
|
getDefaultVoiceDuplexBenchmarkScenarios,
|
|
7209
8668
|
getDefaultTTSBenchmarkFixtures,
|
|
7210
8669
|
evaluateSTTBenchmarkAcceptance,
|
|
8670
|
+
createVoiceProviderFailureSimulator,
|
|
8671
|
+
createVoiceIOProviderFailureSimulator,
|
|
7211
8672
|
createVoiceCallReviewRecorder,
|
|
7212
8673
|
createVoiceCallReviewFromLiveTelephonyReport,
|
|
7213
8674
|
createTelephonyVoiceTestFixtures,
|