@absolutejs/voice 0.0.22-beta.31 → 0.0.22-beta.310
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/README.md +3250 -73
- package/dist/agent.d.ts +62 -0
- package/dist/agentSquadContract.d.ts +98 -0
- package/dist/angular/index.d.ts +16 -0
- package/dist/angular/index.js +3498 -1128
- package/dist/angular/voice-agent-squad-status.service.d.ts +12 -0
- package/dist/angular/voice-campaign-dialer-proof.service.d.ts +14 -0
- package/dist/angular/voice-controller.service.d.ts +1 -0
- package/dist/angular/voice-delivery-runtime.component.d.ts +17 -0
- package/dist/angular/voice-delivery-runtime.service.d.ts +16 -0
- package/dist/angular/voice-live-ops.service.d.ts +11 -0
- package/dist/angular/voice-ops-action-center.service.d.ts +13 -0
- package/dist/angular/voice-ops-status.component.d.ts +15 -0
- package/dist/angular/voice-ops-status.service.d.ts +12 -0
- package/dist/angular/voice-platform-coverage.service.d.ts +12 -0
- package/dist/angular/voice-proof-trends.service.d.ts +12 -0
- package/dist/angular/voice-provider-capabilities.service.d.ts +12 -0
- package/dist/angular/voice-provider-contracts.service.d.ts +12 -0
- package/dist/angular/voice-readiness-failures.service.d.ts +13 -0
- package/dist/angular/voice-routing-status.service.d.ts +11 -0
- package/dist/angular/voice-stream.service.d.ts +1 -0
- package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
- package/dist/angular/voice-turn-latency.service.d.ts +13 -0
- package/dist/angular/voice-turn-quality.service.d.ts +12 -0
- package/dist/angular/voice-workflow-status.service.d.ts +12 -0
- package/dist/audit.d.ts +128 -0
- package/dist/auditDeliveryRoutes.d.ts +85 -0
- package/dist/auditExport.d.ts +34 -0
- package/dist/auditRoutes.d.ts +66 -0
- package/dist/auditSinks.d.ts +151 -0
- package/dist/bargeInRoutes.d.ts +56 -0
- package/dist/campaign.d.ts +768 -0
- package/dist/campaignDialers.d.ts +111 -0
- package/dist/client/actions.d.ts +83 -0
- package/dist/client/agentSquadStatus.d.ts +37 -0
- package/dist/client/agentSquadStatusWidget.d.ts +24 -0
- package/dist/client/bargeInMonitor.d.ts +7 -0
- package/dist/client/campaignDialerProof.d.ts +23 -0
- package/dist/client/deliveryRuntime.d.ts +34 -0
- package/dist/client/deliveryRuntimeWidget.d.ts +37 -0
- package/dist/client/duplex.d.ts +1 -1
- package/dist/client/htmxBootstrap.js +703 -13
- package/dist/client/index.d.ts +70 -0
- package/dist/client/index.js +5257 -19
- package/dist/client/liveOps.d.ts +22 -0
- package/dist/client/liveOpsWidget.d.ts +23 -0
- package/dist/client/liveTurnLatency.d.ts +41 -0
- package/dist/client/opsActionCenter.d.ts +54 -0
- package/dist/client/opsActionCenterWidget.d.ts +29 -0
- package/dist/client/opsActionHistory.d.ts +19 -0
- package/dist/client/opsActionHistoryWidget.d.ts +11 -0
- package/dist/client/opsStatus.d.ts +19 -0
- package/dist/client/opsStatusWidget.d.ts +40 -0
- package/dist/client/platformCoverage.d.ts +19 -0
- package/dist/client/platformCoverageWidget.d.ts +37 -0
- package/dist/client/proofTrends.d.ts +19 -0
- package/dist/client/proofTrendsWidget.d.ts +37 -0
- package/dist/client/providerCapabilities.d.ts +19 -0
- package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
- package/dist/client/providerContracts.d.ts +19 -0
- package/dist/client/providerContractsWidget.d.ts +37 -0
- package/dist/client/providerSimulationControls.d.ts +33 -0
- package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
- package/dist/client/providerStatusWidget.d.ts +32 -0
- package/dist/client/readinessFailures.d.ts +19 -0
- package/dist/client/readinessFailuresWidget.d.ts +42 -0
- package/dist/client/routingStatus.d.ts +19 -0
- package/dist/client/routingStatusWidget.d.ts +28 -0
- package/dist/client/traceTimeline.d.ts +19 -0
- package/dist/client/traceTimelineWidget.d.ts +36 -0
- package/dist/client/turnLatency.d.ts +22 -0
- package/dist/client/turnLatencyWidget.d.ts +33 -0
- package/dist/client/turnQuality.d.ts +19 -0
- package/dist/client/turnQualityWidget.d.ts +32 -0
- package/dist/client/workflowStatus.d.ts +19 -0
- package/dist/competitiveCoverage.d.ts +141 -0
- package/dist/dataControl.d.ts +180 -0
- package/dist/deliveryRuntime.d.ts +158 -0
- package/dist/deliverySinkRoutes.d.ts +117 -0
- package/dist/demoReadyRoutes.d.ts +98 -0
- package/dist/diagnosticsRoutes.d.ts +44 -0
- package/dist/evalRoutes.d.ts +219 -0
- package/dist/fileStore.d.ts +14 -2
- package/dist/guardrails.d.ts +128 -0
- package/dist/incidentBundle.d.ts +116 -0
- package/dist/index.d.ts +144 -13
- package/dist/index.js +28070 -5219
- package/dist/latencySlo.d.ts +56 -0
- package/dist/liveLatency.d.ts +78 -0
- package/dist/liveOps.d.ts +190 -0
- package/dist/mediaPipeline.d.ts +125 -0
- package/dist/modelAdapters.d.ts +60 -2
- package/dist/observabilityExport.d.ts +481 -0
- package/dist/openaiTTS.d.ts +18 -0
- package/dist/operationsRecord.d.ts +254 -0
- package/dist/opsActionAuditRoutes.d.ts +99 -0
- package/dist/opsConsoleRoutes.d.ts +80 -0
- package/dist/opsRecovery.d.ts +137 -0
- package/dist/opsStatus.d.ts +76 -0
- package/dist/opsStatusRoutes.d.ts +33 -0
- package/dist/outcomeContract.d.ts +146 -0
- package/dist/phoneAgent.d.ts +139 -0
- package/dist/phoneAgentProductionSmoke.d.ts +115 -0
- package/dist/platformCoverage.d.ts +91 -0
- package/dist/postCallAnalysis.d.ts +98 -0
- package/dist/postgresStore.d.ts +13 -2
- package/dist/productionReadiness.d.ts +559 -0
- package/dist/proofTrends.d.ts +133 -0
- package/dist/providerAdapters.d.ts +48 -0
- package/dist/providerCapabilities.d.ts +92 -0
- package/dist/providerDecisionTraces.d.ts +130 -0
- package/dist/providerHealth.d.ts +1 -0
- package/dist/providerOrchestration.d.ts +109 -0
- package/dist/providerRoutingContract.d.ts +71 -0
- package/dist/providerSlo.d.ts +142 -0
- package/dist/providerStackRecommendations.d.ts +187 -0
- package/dist/qualityRoutes.d.ts +76 -0
- package/dist/queue.d.ts +9 -0
- package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
- package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
- package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
- package/dist/react/VoiceOpsStatus.d.ts +6 -0
- package/dist/react/VoicePlatformCoverage.d.ts +6 -0
- package/dist/react/VoiceProofTrends.d.ts +6 -0
- package/dist/react/VoiceProviderCapabilities.d.ts +6 -0
- package/dist/react/VoiceProviderContracts.d.ts +6 -0
- package/dist/react/VoiceProviderSimulationControls.d.ts +5 -0
- package/dist/react/VoiceProviderStatus.d.ts +6 -0
- package/dist/react/VoiceReadinessFailures.d.ts +6 -0
- package/dist/react/VoiceRoutingStatus.d.ts +6 -0
- package/dist/react/VoiceTraceTimeline.d.ts +6 -0
- package/dist/react/VoiceTurnLatency.d.ts +6 -0
- package/dist/react/VoiceTurnQuality.d.ts +6 -0
- package/dist/react/index.d.ts +32 -0
- package/dist/react/index.js +5059 -31
- package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
- package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
- package/dist/react/useVoiceController.d.ts +1 -0
- package/dist/react/useVoiceDeliveryRuntime.d.ts +13 -0
- package/dist/react/useVoiceLiveOps.d.ts +9 -0
- package/dist/react/useVoiceOpsActionCenter.d.ts +11 -0
- package/dist/react/useVoiceOpsStatus.d.ts +8 -0
- package/dist/react/useVoicePlatformCoverage.d.ts +8 -0
- package/dist/react/useVoiceProofTrends.d.ts +8 -0
- package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
- package/dist/react/useVoiceProviderContracts.d.ts +8 -0
- package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
- package/dist/react/useVoiceReadinessFailures.d.ts +8 -0
- package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
- package/dist/react/useVoiceStream.d.ts +1 -0
- package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
- package/dist/react/useVoiceTurnLatency.d.ts +9 -0
- package/dist/react/useVoiceTurnQuality.d.ts +8 -0
- package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
- package/dist/readinessProfiles.d.ts +38 -0
- package/dist/realtimeChannel.d.ts +136 -0
- package/dist/realtimeProviderContracts.d.ts +133 -0
- package/dist/reconnectContract.d.ts +88 -0
- package/dist/resilienceRoutes.d.ts +143 -0
- package/dist/sessionReplay.d.ts +12 -0
- package/dist/simulationSuite.d.ts +143 -0
- package/dist/sloCalibration.d.ts +185 -0
- package/dist/sqliteStore.d.ts +13 -2
- package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
- package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
- package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
- package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
- package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
- package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
- package/dist/svelte/createVoicePlatformCoverage.d.ts +7 -0
- package/dist/svelte/createVoiceProofTrends.d.ts +7 -0
- package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
- package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
- package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
- package/dist/svelte/createVoiceProviderStatus.d.ts +4 -2
- package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
- package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
- package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
- package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
- package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
- package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
- package/dist/svelte/index.d.ts +17 -0
- package/dist/svelte/index.js +4924 -420
- package/dist/telephony/contract.d.ts +61 -0
- package/dist/telephony/matrix.d.ts +97 -0
- package/dist/telephony/plivo.d.ts +303 -0
- package/dist/telephony/security.d.ts +182 -0
- package/dist/telephony/telnyx.d.ts +291 -0
- package/dist/telephony/twilio.d.ts +135 -2
- package/dist/telephonyOutcome.d.ts +273 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1767 -44
- package/dist/testing/ioProviderSimulator.d.ts +41 -0
- package/dist/toolContract.d.ts +161 -0
- package/dist/toolRuntime.d.ts +50 -0
- package/dist/trace.d.ts +19 -1
- package/dist/traceDeliveryRoutes.d.ts +86 -0
- package/dist/traceTimeline.d.ts +97 -0
- package/dist/turnLatency.d.ts +95 -0
- package/dist/turnQuality.d.ts +94 -0
- package/dist/types.d.ts +97 -3
- package/dist/voiceMonitoring.d.ts +444 -0
- package/dist/vue/VoiceDeliveryRuntime.d.ts +30 -0
- package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
- package/dist/vue/VoiceOpsStatus.d.ts +30 -0
- package/dist/vue/VoicePlatformCoverage.d.ts +23 -0
- package/dist/vue/VoiceProofTrends.d.ts +21 -0
- package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
- package/dist/vue/VoiceProviderContracts.d.ts +21 -0
- package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
- package/dist/vue/VoiceProviderStatus.d.ts +51 -0
- package/dist/vue/VoiceReadinessFailures.d.ts +21 -0
- package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
- package/dist/vue/VoiceTurnLatency.d.ts +69 -0
- package/dist/vue/VoiceTurnQuality.d.ts +51 -0
- package/dist/vue/index.d.ts +30 -0
- package/dist/vue/index.js +4828 -56
- package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
- package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
- package/dist/vue/useVoiceController.d.ts +2 -1
- package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
- package/dist/vue/useVoiceLiveOps.d.ts +9 -0
- package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
- package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
- package/dist/vue/useVoicePlatformCoverage.d.ts +9 -0
- package/dist/vue/useVoiceProofTrends.d.ts +9 -0
- package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
- package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
- package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
- package/dist/vue/useVoiceProviderStatus.d.ts +1 -1
- package/dist/vue/useVoiceReadinessFailures.d.ts +775 -0
- package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
- package/dist/vue/useVoiceStream.d.ts +2 -1
- package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
- package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
- package/dist/vue/useVoiceTurnQuality.d.ts +9 -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,11 @@ var serverMessageToAction = (message) => {
|
|
|
2105
2105
|
sessionId: message.sessionId,
|
|
2106
2106
|
type: "complete"
|
|
2107
2107
|
};
|
|
2108
|
+
case "connection":
|
|
2109
|
+
return {
|
|
2110
|
+
reconnect: message.reconnect,
|
|
2111
|
+
type: "connection"
|
|
2112
|
+
};
|
|
2108
2113
|
case "call_lifecycle":
|
|
2109
2114
|
return {
|
|
2110
2115
|
event: message.event,
|
|
@@ -2126,6 +2131,17 @@ var serverMessageToAction = (message) => {
|
|
|
2126
2131
|
transcript: message.transcript,
|
|
2127
2132
|
type: "partial"
|
|
2128
2133
|
};
|
|
2134
|
+
case "replay":
|
|
2135
|
+
return {
|
|
2136
|
+
assistantTexts: message.assistantTexts,
|
|
2137
|
+
call: message.call,
|
|
2138
|
+
partial: message.partial,
|
|
2139
|
+
scenarioId: message.scenarioId,
|
|
2140
|
+
sessionId: message.sessionId,
|
|
2141
|
+
status: message.status,
|
|
2142
|
+
turns: message.turns,
|
|
2143
|
+
type: "replay"
|
|
2144
|
+
};
|
|
2129
2145
|
case "session":
|
|
2130
2146
|
return {
|
|
2131
2147
|
sessionId: message.sessionId,
|
|
@@ -2186,10 +2202,12 @@ var isVoiceServerMessage = (value) => {
|
|
|
2186
2202
|
case "assistant":
|
|
2187
2203
|
case "call_lifecycle":
|
|
2188
2204
|
case "complete":
|
|
2205
|
+
case "connection":
|
|
2189
2206
|
case "error":
|
|
2190
2207
|
case "final":
|
|
2191
2208
|
case "partial":
|
|
2192
2209
|
case "pong":
|
|
2210
|
+
case "replay":
|
|
2193
2211
|
case "session":
|
|
2194
2212
|
case "turn":
|
|
2195
2213
|
return true;
|
|
@@ -2226,6 +2244,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
2226
2244
|
sessionId: options.sessionId ?? createSessionId(),
|
|
2227
2245
|
ws: null
|
|
2228
2246
|
};
|
|
2247
|
+
const emitConnection = (reconnect) => {
|
|
2248
|
+
listeners.forEach((listener) => listener(reconnect));
|
|
2249
|
+
};
|
|
2229
2250
|
const clearTimers = () => {
|
|
2230
2251
|
if (state.pingInterval) {
|
|
2231
2252
|
clearInterval(state.pingInterval);
|
|
@@ -2248,9 +2269,28 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
2248
2269
|
}
|
|
2249
2270
|
};
|
|
2250
2271
|
const scheduleReconnect = () => {
|
|
2272
|
+
const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
|
|
2251
2273
|
state.reconnectAttempts += 1;
|
|
2274
|
+
emitConnection({
|
|
2275
|
+
reconnect: {
|
|
2276
|
+
attempts: state.reconnectAttempts,
|
|
2277
|
+
lastDisconnectAt: Date.now(),
|
|
2278
|
+
maxAttempts: maxReconnectAttempts,
|
|
2279
|
+
nextAttemptAt,
|
|
2280
|
+
status: "reconnecting"
|
|
2281
|
+
},
|
|
2282
|
+
type: "connection"
|
|
2283
|
+
});
|
|
2252
2284
|
state.reconnectTimeout = setTimeout(() => {
|
|
2253
2285
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
2286
|
+
emitConnection({
|
|
2287
|
+
reconnect: {
|
|
2288
|
+
attempts: state.reconnectAttempts,
|
|
2289
|
+
maxAttempts: maxReconnectAttempts,
|
|
2290
|
+
status: "exhausted"
|
|
2291
|
+
},
|
|
2292
|
+
type: "connection"
|
|
2293
|
+
});
|
|
2254
2294
|
return;
|
|
2255
2295
|
}
|
|
2256
2296
|
connect();
|
|
@@ -2260,9 +2300,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
2260
2300
|
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
|
|
2261
2301
|
ws.binaryType = "arraybuffer";
|
|
2262
2302
|
ws.onopen = () => {
|
|
2303
|
+
const wasReconnecting = state.reconnectAttempts > 0;
|
|
2263
2304
|
state.isConnected = true;
|
|
2264
|
-
state.reconnectAttempts = 0;
|
|
2265
2305
|
flushPendingMessages();
|
|
2306
|
+
if (wasReconnecting) {
|
|
2307
|
+
emitConnection({
|
|
2308
|
+
reconnect: {
|
|
2309
|
+
attempts: state.reconnectAttempts,
|
|
2310
|
+
lastResumedAt: Date.now(),
|
|
2311
|
+
maxAttempts: maxReconnectAttempts,
|
|
2312
|
+
status: "resumed"
|
|
2313
|
+
},
|
|
2314
|
+
type: "connection"
|
|
2315
|
+
});
|
|
2316
|
+
state.reconnectAttempts = 0;
|
|
2317
|
+
}
|
|
2266
2318
|
listeners.forEach((listener) => listener({
|
|
2267
2319
|
scenarioId: state.scenarioId ?? undefined,
|
|
2268
2320
|
sessionId: state.sessionId,
|
|
@@ -2292,6 +2344,16 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
2292
2344
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
|
|
2293
2345
|
if (reconnectable) {
|
|
2294
2346
|
scheduleReconnect();
|
|
2347
|
+
} else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
|
|
2348
|
+
emitConnection({
|
|
2349
|
+
reconnect: {
|
|
2350
|
+
attempts: state.reconnectAttempts,
|
|
2351
|
+
lastDisconnectAt: Date.now(),
|
|
2352
|
+
maxAttempts: maxReconnectAttempts,
|
|
2353
|
+
status: "exhausted"
|
|
2354
|
+
},
|
|
2355
|
+
type: "connection"
|
|
2356
|
+
});
|
|
2295
2357
|
}
|
|
2296
2358
|
};
|
|
2297
2359
|
state.ws = ws;
|
|
@@ -2362,6 +2424,11 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
2362
2424
|
};
|
|
2363
2425
|
|
|
2364
2426
|
// src/client/store.ts
|
|
2427
|
+
var createInitialReconnectState = () => ({
|
|
2428
|
+
attempts: 0,
|
|
2429
|
+
maxAttempts: 0,
|
|
2430
|
+
status: "idle"
|
|
2431
|
+
});
|
|
2365
2432
|
var createInitialState2 = () => ({
|
|
2366
2433
|
assistantAudio: [],
|
|
2367
2434
|
assistantTexts: [],
|
|
@@ -2370,6 +2437,7 @@ var createInitialState2 = () => ({
|
|
|
2370
2437
|
isConnected: false,
|
|
2371
2438
|
scenarioId: null,
|
|
2372
2439
|
partial: "",
|
|
2440
|
+
reconnect: createInitialReconnectState(),
|
|
2373
2441
|
sessionId: null,
|
|
2374
2442
|
status: "idle",
|
|
2375
2443
|
turns: []
|
|
@@ -2426,7 +2494,19 @@ var createVoiceStreamStore = () => {
|
|
|
2426
2494
|
case "connected":
|
|
2427
2495
|
state = {
|
|
2428
2496
|
...state,
|
|
2429
|
-
isConnected: true
|
|
2497
|
+
isConnected: true,
|
|
2498
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
2499
|
+
...state.reconnect,
|
|
2500
|
+
lastResumedAt: Date.now(),
|
|
2501
|
+
nextAttemptAt: undefined,
|
|
2502
|
+
status: "resumed"
|
|
2503
|
+
} : state.reconnect
|
|
2504
|
+
};
|
|
2505
|
+
break;
|
|
2506
|
+
case "connection":
|
|
2507
|
+
state = {
|
|
2508
|
+
...state,
|
|
2509
|
+
reconnect: action.reconnect
|
|
2430
2510
|
};
|
|
2431
2511
|
break;
|
|
2432
2512
|
case "disconnected":
|
|
@@ -2454,6 +2534,26 @@ var createVoiceStreamStore = () => {
|
|
|
2454
2534
|
partial: action.transcript.text
|
|
2455
2535
|
};
|
|
2456
2536
|
break;
|
|
2537
|
+
case "replay":
|
|
2538
|
+
state = {
|
|
2539
|
+
...state,
|
|
2540
|
+
assistantTexts: [...action.assistantTexts],
|
|
2541
|
+
call: action.call ?? null,
|
|
2542
|
+
error: null,
|
|
2543
|
+
isConnected: action.status === "active",
|
|
2544
|
+
partial: action.partial,
|
|
2545
|
+
reconnect: state.reconnect.status === "reconnecting" ? {
|
|
2546
|
+
...state.reconnect,
|
|
2547
|
+
lastResumedAt: Date.now(),
|
|
2548
|
+
nextAttemptAt: undefined,
|
|
2549
|
+
status: "resumed"
|
|
2550
|
+
} : state.reconnect,
|
|
2551
|
+
scenarioId: action.scenarioId ?? state.scenarioId,
|
|
2552
|
+
sessionId: action.sessionId,
|
|
2553
|
+
status: action.status,
|
|
2554
|
+
turns: [...action.turns]
|
|
2555
|
+
};
|
|
2556
|
+
break;
|
|
2457
2557
|
case "session":
|
|
2458
2558
|
state = {
|
|
2459
2559
|
...state,
|
|
@@ -2501,10 +2601,34 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
2501
2601
|
const notify = () => {
|
|
2502
2602
|
subscribers.forEach((subscriber) => subscriber());
|
|
2503
2603
|
};
|
|
2604
|
+
const reportReconnect = () => {
|
|
2605
|
+
if (!options.reconnectReportPath || typeof fetch === "undefined") {
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
const snapshot = store.getSnapshot();
|
|
2609
|
+
const body = JSON.stringify({
|
|
2610
|
+
at: Date.now(),
|
|
2611
|
+
reconnect: snapshot.reconnect,
|
|
2612
|
+
scenarioId: snapshot.scenarioId,
|
|
2613
|
+
sessionId: connection.getSessionId(),
|
|
2614
|
+
turnIds: snapshot.turns.map((turn) => turn.id)
|
|
2615
|
+
});
|
|
2616
|
+
fetch(options.reconnectReportPath, {
|
|
2617
|
+
body,
|
|
2618
|
+
headers: {
|
|
2619
|
+
"Content-Type": "application/json"
|
|
2620
|
+
},
|
|
2621
|
+
keepalive: true,
|
|
2622
|
+
method: "POST"
|
|
2623
|
+
}).catch(() => {});
|
|
2624
|
+
};
|
|
2504
2625
|
const unsubscribeConnection = connection.subscribe((message) => {
|
|
2505
2626
|
const action = serverMessageToAction(message);
|
|
2506
2627
|
if (action) {
|
|
2507
2628
|
store.dispatch(action);
|
|
2629
|
+
if (message.type === "connection") {
|
|
2630
|
+
reportReconnect();
|
|
2631
|
+
}
|
|
2508
2632
|
notify();
|
|
2509
2633
|
}
|
|
2510
2634
|
});
|
|
@@ -2540,6 +2664,9 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
2540
2664
|
get partial() {
|
|
2541
2665
|
return store.getSnapshot().partial;
|
|
2542
2666
|
},
|
|
2667
|
+
get reconnect() {
|
|
2668
|
+
return store.getSnapshot().reconnect;
|
|
2669
|
+
},
|
|
2543
2670
|
get sessionId() {
|
|
2544
2671
|
return connection.getSessionId();
|
|
2545
2672
|
},
|
|
@@ -2895,6 +3022,7 @@ var createInitialState3 = (stream) => ({
|
|
|
2895
3022
|
isConnected: stream.isConnected,
|
|
2896
3023
|
isRecording: false,
|
|
2897
3024
|
partial: stream.partial,
|
|
3025
|
+
reconnect: stream.reconnect,
|
|
2898
3026
|
recordingError: null,
|
|
2899
3027
|
sessionId: stream.sessionId,
|
|
2900
3028
|
scenarioId: stream.scenarioId,
|
|
@@ -2924,6 +3052,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
2924
3052
|
error: stream.error,
|
|
2925
3053
|
isConnected: stream.isConnected,
|
|
2926
3054
|
partial: stream.partial,
|
|
3055
|
+
reconnect: stream.reconnect,
|
|
2927
3056
|
sessionId: stream.sessionId,
|
|
2928
3057
|
scenarioId: stream.scenarioId,
|
|
2929
3058
|
status: stream.status,
|
|
@@ -2948,7 +3077,13 @@ var createVoiceController = (path, options = {}) => {
|
|
|
2948
3077
|
capture = createMicrophoneCapture({
|
|
2949
3078
|
channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
|
|
2950
3079
|
onLevel: options.capture?.onLevel,
|
|
2951
|
-
onAudio: (audio) =>
|
|
3080
|
+
onAudio: (audio) => {
|
|
3081
|
+
if (options.capture?.onAudio) {
|
|
3082
|
+
options.capture.onAudio(audio, stream.sendAudio);
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
stream.sendAudio(audio);
|
|
3086
|
+
},
|
|
2952
3087
|
sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
|
|
2953
3088
|
});
|
|
2954
3089
|
return capture;
|
|
@@ -3018,6 +3153,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
3018
3153
|
get recordingError() {
|
|
3019
3154
|
return state.recordingError;
|
|
3020
3155
|
},
|
|
3156
|
+
get reconnect() {
|
|
3157
|
+
return state.reconnect;
|
|
3158
|
+
},
|
|
3021
3159
|
sendAudio: (audio) => stream.sendAudio(audio),
|
|
3022
3160
|
get sessionId() {
|
|
3023
3161
|
return state.sessionId;
|
|
@@ -3063,11 +3201,26 @@ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
|
|
|
3063
3201
|
var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
|
|
3064
3202
|
var bindVoiceBargeIn = (controller, player, options = {}) => {
|
|
3065
3203
|
let lastPartial = controller.partial;
|
|
3066
|
-
const interruptIfPlaying = () => {
|
|
3204
|
+
const interruptIfPlaying = (reason) => {
|
|
3067
3205
|
if (!player.isPlaying || options.enabled === false) {
|
|
3206
|
+
options.monitor?.recordSkipped({
|
|
3207
|
+
reason,
|
|
3208
|
+
sessionId: controller.sessionId
|
|
3209
|
+
});
|
|
3068
3210
|
return;
|
|
3069
3211
|
}
|
|
3070
|
-
|
|
3212
|
+
options.monitor?.recordRequested({
|
|
3213
|
+
reason,
|
|
3214
|
+
sessionId: controller.sessionId
|
|
3215
|
+
});
|
|
3216
|
+
player.interrupt().then(() => {
|
|
3217
|
+
options.monitor?.recordStopped({
|
|
3218
|
+
latencyMs: player.lastInterruptLatencyMs,
|
|
3219
|
+
playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
|
|
3220
|
+
reason,
|
|
3221
|
+
sessionId: controller.sessionId
|
|
3222
|
+
});
|
|
3223
|
+
});
|
|
3071
3224
|
};
|
|
3072
3225
|
const unsubscribe = controller.subscribe(() => {
|
|
3073
3226
|
if (options.interruptOnPartial === false) {
|
|
@@ -3075,7 +3228,7 @@ var bindVoiceBargeIn = (controller, player, options = {}) => {
|
|
|
3075
3228
|
return;
|
|
3076
3229
|
}
|
|
3077
3230
|
if (!lastPartial && controller.partial) {
|
|
3078
|
-
interruptIfPlaying();
|
|
3231
|
+
interruptIfPlaying("partial-transcript");
|
|
3079
3232
|
}
|
|
3080
3233
|
lastPartial = controller.partial;
|
|
3081
3234
|
});
|
|
@@ -3085,11 +3238,11 @@ var bindVoiceBargeIn = (controller, player, options = {}) => {
|
|
|
3085
3238
|
},
|
|
3086
3239
|
handleLevel: (level) => {
|
|
3087
3240
|
if (shouldInterruptForLevel(level, options)) {
|
|
3088
|
-
interruptIfPlaying();
|
|
3241
|
+
interruptIfPlaying("input-level");
|
|
3089
3242
|
}
|
|
3090
3243
|
},
|
|
3091
3244
|
sendAudio: (audio) => {
|
|
3092
|
-
interruptIfPlaying();
|
|
3245
|
+
interruptIfPlaying("manual-audio");
|
|
3093
3246
|
controller.sendAudio(audio);
|
|
3094
3247
|
}
|
|
3095
3248
|
};
|
|
@@ -3119,7 +3272,17 @@ var createVoiceDuplexController = (path, options = {}) => {
|
|
|
3119
3272
|
audioPlayer,
|
|
3120
3273
|
close,
|
|
3121
3274
|
interruptAssistant: async () => {
|
|
3275
|
+
options.bargeIn?.monitor?.recordRequested({
|
|
3276
|
+
reason: "manual-interrupt",
|
|
3277
|
+
sessionId: controller.sessionId
|
|
3278
|
+
});
|
|
3122
3279
|
await audioPlayer.interrupt();
|
|
3280
|
+
options.bargeIn?.monitor?.recordStopped({
|
|
3281
|
+
latencyMs: audioPlayer.lastInterruptLatencyMs,
|
|
3282
|
+
playbackStopLatencyMs: audioPlayer.lastPlaybackStopLatencyMs,
|
|
3283
|
+
reason: "manual-interrupt",
|
|
3284
|
+
sessionId: controller.sessionId
|
|
3285
|
+
});
|
|
3123
3286
|
},
|
|
3124
3287
|
sendAudio: (audio) => {
|
|
3125
3288
|
bargeInBinding?.sendAudio(audio);
|
|
@@ -3510,7 +3673,235 @@ var loadVoiceTestFixtures = async (fixtureDirectory) => {
|
|
|
3510
3673
|
}
|
|
3511
3674
|
return fixtures;
|
|
3512
3675
|
};
|
|
3676
|
+
// src/testing/ioProviderSimulator.ts
|
|
3677
|
+
var defaultFailureMessage = (input) => `Simulated ${input.provider} ${input.kind.toUpperCase()} ${input.operation} failure.`;
|
|
3678
|
+
var resolveRecoveryElapsedMs = (value, provider) => {
|
|
3679
|
+
if (typeof value === "number") {
|
|
3680
|
+
return value;
|
|
3681
|
+
}
|
|
3682
|
+
return value?.[provider] ?? 25;
|
|
3683
|
+
};
|
|
3684
|
+
var createHealth = (input) => ({
|
|
3685
|
+
consecutiveFailures: input.status === "healthy" ? 0 : 1,
|
|
3686
|
+
lastFailureAt: input.status === "healthy" ? undefined : input.now,
|
|
3687
|
+
provider: input.provider,
|
|
3688
|
+
status: input.status,
|
|
3689
|
+
suppressedUntil: input.suppressedUntil
|
|
3690
|
+
});
|
|
3691
|
+
var resolveFallback = async (options, provider) => {
|
|
3692
|
+
const configured = typeof options.fallback === "function" ? await options.fallback(provider) : options.fallback;
|
|
3693
|
+
return (configured ?? options.providers).find((candidate) => candidate !== provider);
|
|
3694
|
+
};
|
|
3695
|
+
var createVoiceIOProviderFailureSimulator = (options) => {
|
|
3696
|
+
if (options.providers.length === 0) {
|
|
3697
|
+
throw new Error("At least one provider is required.");
|
|
3698
|
+
}
|
|
3699
|
+
const now = options.now ?? Date.now;
|
|
3700
|
+
const operation = options.operation ?? "open";
|
|
3701
|
+
const cooldownMs = Math.max(0, options.cooldownMs ?? 30000);
|
|
3702
|
+
const emit = async (event, input) => {
|
|
3703
|
+
await options.onProviderEvent?.(event, input);
|
|
3704
|
+
};
|
|
3705
|
+
const run = async (provider, mode) => {
|
|
3706
|
+
if (!options.providers.includes(provider)) {
|
|
3707
|
+
throw new Error(`${provider} is not configured for simulation.`);
|
|
3708
|
+
}
|
|
3709
|
+
const startedAt = now();
|
|
3710
|
+
const sessionId = options.sessionId?.({ mode, now: startedAt, provider }) ?? `${options.kind}-provider-sim-${startedAt}`;
|
|
3711
|
+
if (mode === "recovery") {
|
|
3712
|
+
await emit({
|
|
3713
|
+
at: startedAt,
|
|
3714
|
+
attempt: 0,
|
|
3715
|
+
elapsedMs: resolveRecoveryElapsedMs(options.recoveryElapsedMs, provider),
|
|
3716
|
+
kind: options.kind,
|
|
3717
|
+
latencyBudgetMs: options.latencyBudgets?.[provider],
|
|
3718
|
+
operation,
|
|
3719
|
+
provider,
|
|
3720
|
+
providerHealth: createHealth({
|
|
3721
|
+
now: startedAt,
|
|
3722
|
+
provider,
|
|
3723
|
+
status: "healthy"
|
|
3724
|
+
}),
|
|
3725
|
+
selectedProvider: provider,
|
|
3726
|
+
status: "success"
|
|
3727
|
+
}, { mode, provider, sessionId });
|
|
3728
|
+
return {
|
|
3729
|
+
mode,
|
|
3730
|
+
provider,
|
|
3731
|
+
sessionId,
|
|
3732
|
+
status: "simulated"
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
const fallbackProvider = await resolveFallback(options, provider);
|
|
3736
|
+
const suppressedUntil = startedAt + cooldownMs;
|
|
3737
|
+
await emit({
|
|
3738
|
+
at: startedAt,
|
|
3739
|
+
attempt: 0,
|
|
3740
|
+
elapsedMs: options.failureElapsedMs ?? 10,
|
|
3741
|
+
error: (options.failureMessage ?? defaultFailureMessage)({
|
|
3742
|
+
kind: options.kind,
|
|
3743
|
+
operation,
|
|
3744
|
+
provider
|
|
3745
|
+
}),
|
|
3746
|
+
fallbackProvider,
|
|
3747
|
+
kind: options.kind,
|
|
3748
|
+
latencyBudgetMs: options.latencyBudgets?.[provider],
|
|
3749
|
+
operation,
|
|
3750
|
+
provider,
|
|
3751
|
+
providerHealth: createHealth({
|
|
3752
|
+
now: startedAt,
|
|
3753
|
+
provider,
|
|
3754
|
+
status: "suppressed",
|
|
3755
|
+
suppressedUntil
|
|
3756
|
+
}),
|
|
3757
|
+
selectedProvider: provider,
|
|
3758
|
+
status: "error",
|
|
3759
|
+
suppressedUntil
|
|
3760
|
+
}, { mode, provider, sessionId });
|
|
3761
|
+
if (fallbackProvider) {
|
|
3762
|
+
await emit({
|
|
3763
|
+
at: startedAt + 1,
|
|
3764
|
+
attempt: 1,
|
|
3765
|
+
elapsedMs: resolveRecoveryElapsedMs(options.recoveryElapsedMs, fallbackProvider),
|
|
3766
|
+
fallbackProvider,
|
|
3767
|
+
kind: options.kind,
|
|
3768
|
+
latencyBudgetMs: options.latencyBudgets?.[fallbackProvider],
|
|
3769
|
+
operation,
|
|
3770
|
+
provider: fallbackProvider,
|
|
3771
|
+
providerHealth: createHealth({
|
|
3772
|
+
now: startedAt + 1,
|
|
3773
|
+
provider: fallbackProvider,
|
|
3774
|
+
status: "healthy"
|
|
3775
|
+
}),
|
|
3776
|
+
selectedProvider: provider,
|
|
3777
|
+
status: "fallback"
|
|
3778
|
+
}, { mode, provider, sessionId });
|
|
3779
|
+
}
|
|
3780
|
+
return {
|
|
3781
|
+
fallbackProvider,
|
|
3782
|
+
mode,
|
|
3783
|
+
provider,
|
|
3784
|
+
sessionId,
|
|
3785
|
+
status: "simulated",
|
|
3786
|
+
suppressedUntil
|
|
3787
|
+
};
|
|
3788
|
+
};
|
|
3789
|
+
return {
|
|
3790
|
+
run
|
|
3791
|
+
};
|
|
3792
|
+
};
|
|
3513
3793
|
// src/modelAdapters.ts
|
|
3794
|
+
var isVoiceProviderRoutingPolicyPreset = (value) => value === "balanced" || value === "cost-cap" || value === "cost-first" || value === "latency-first" || value === "quality-first";
|
|
3795
|
+
var resolveVoiceProviderRoutingPolicyPreset = (preset, options = {}) => {
|
|
3796
|
+
switch (preset) {
|
|
3797
|
+
case "balanced":
|
|
3798
|
+
return {
|
|
3799
|
+
fallbackMode: "provider-error",
|
|
3800
|
+
strategy: "balanced",
|
|
3801
|
+
weights: {
|
|
3802
|
+
cost: 1,
|
|
3803
|
+
latencyMs: 0.005,
|
|
3804
|
+
priority: 1,
|
|
3805
|
+
quality: 10,
|
|
3806
|
+
...options.weights
|
|
3807
|
+
},
|
|
3808
|
+
...options
|
|
3809
|
+
};
|
|
3810
|
+
case "cost-cap":
|
|
3811
|
+
return {
|
|
3812
|
+
fallbackMode: "provider-error",
|
|
3813
|
+
strategy: "prefer-cheapest",
|
|
3814
|
+
...options
|
|
3815
|
+
};
|
|
3816
|
+
case "cost-first":
|
|
3817
|
+
return {
|
|
3818
|
+
fallbackMode: "provider-error",
|
|
3819
|
+
strategy: "prefer-cheapest",
|
|
3820
|
+
...options
|
|
3821
|
+
};
|
|
3822
|
+
case "latency-first":
|
|
3823
|
+
return {
|
|
3824
|
+
fallbackMode: "provider-error",
|
|
3825
|
+
strategy: "prefer-fastest",
|
|
3826
|
+
...options
|
|
3827
|
+
};
|
|
3828
|
+
case "quality-first":
|
|
3829
|
+
return {
|
|
3830
|
+
fallbackMode: "provider-error",
|
|
3831
|
+
strategy: "quality-first",
|
|
3832
|
+
...options
|
|
3833
|
+
};
|
|
3834
|
+
}
|
|
3835
|
+
};
|
|
3836
|
+
var resolveVoiceProviderRoutingPolicy = (policy) => {
|
|
3837
|
+
if (!policy) {
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
if (typeof policy === "string") {
|
|
3841
|
+
return isVoiceProviderRoutingPolicyPreset(policy) ? resolveVoiceProviderRoutingPolicyPreset(policy) : {
|
|
3842
|
+
strategy: policy
|
|
3843
|
+
};
|
|
3844
|
+
}
|
|
3845
|
+
return policy;
|
|
3846
|
+
};
|
|
3847
|
+
var mergeDefinedProviderPolicyFields = (base, surface) => {
|
|
3848
|
+
const next = {
|
|
3849
|
+
...base ?? {}
|
|
3850
|
+
};
|
|
3851
|
+
if (surface.allowProviders !== undefined) {
|
|
3852
|
+
next.allowProviders = surface.allowProviders;
|
|
3853
|
+
}
|
|
3854
|
+
if (surface.fallbackMode !== undefined) {
|
|
3855
|
+
next.fallbackMode = surface.fallbackMode;
|
|
3856
|
+
}
|
|
3857
|
+
if (surface.maxCost !== undefined) {
|
|
3858
|
+
next.maxCost = surface.maxCost;
|
|
3859
|
+
}
|
|
3860
|
+
if (surface.maxLatencyMs !== undefined) {
|
|
3861
|
+
next.maxLatencyMs = surface.maxLatencyMs;
|
|
3862
|
+
}
|
|
3863
|
+
if (surface.minQuality !== undefined) {
|
|
3864
|
+
next.minQuality = surface.minQuality;
|
|
3865
|
+
}
|
|
3866
|
+
if (surface.strategy !== undefined) {
|
|
3867
|
+
next.strategy = surface.strategy;
|
|
3868
|
+
}
|
|
3869
|
+
if (surface.weights !== undefined) {
|
|
3870
|
+
next.weights = {
|
|
3871
|
+
...base?.weights ?? {},
|
|
3872
|
+
...surface.weights
|
|
3873
|
+
};
|
|
3874
|
+
}
|
|
3875
|
+
return next;
|
|
3876
|
+
};
|
|
3877
|
+
var createVoiceProviderOrchestrationProfile = (options) => {
|
|
3878
|
+
const surfaceNames = Object.keys(options.surfaces);
|
|
3879
|
+
const defaultSurface = options.defaultSurface ?? surfaceNames[0];
|
|
3880
|
+
if (!defaultSurface || !options.surfaces[defaultSurface]) {
|
|
3881
|
+
throw new Error("Voice provider orchestration profile has no surfaces.");
|
|
3882
|
+
}
|
|
3883
|
+
return {
|
|
3884
|
+
defaultSurface,
|
|
3885
|
+
id: options.id,
|
|
3886
|
+
resolve: (surface = defaultSurface) => {
|
|
3887
|
+
const config = options.surfaces[surface];
|
|
3888
|
+
if (!config) {
|
|
3889
|
+
throw new Error(`Voice provider orchestration profile ${options.id} has no surface "${surface}".`);
|
|
3890
|
+
}
|
|
3891
|
+
const policy = mergeDefinedProviderPolicyFields(resolveVoiceProviderRoutingPolicy(config.policy), config);
|
|
3892
|
+
return {
|
|
3893
|
+
allowProviders: config.allowProviders,
|
|
3894
|
+
fallback: config.fallback,
|
|
3895
|
+
fallbackMode: config.fallbackMode,
|
|
3896
|
+
policy,
|
|
3897
|
+
providerHealth: config.providerHealth,
|
|
3898
|
+
providerProfiles: config.providerProfiles,
|
|
3899
|
+
timeoutMs: config.timeoutMs
|
|
3900
|
+
};
|
|
3901
|
+
},
|
|
3902
|
+
surfaces: options.surfaces
|
|
3903
|
+
};
|
|
3904
|
+
};
|
|
3514
3905
|
var OUTPUT_SCHEMA = {
|
|
3515
3906
|
additionalProperties: false,
|
|
3516
3907
|
properties: {
|
|
@@ -3601,6 +3992,17 @@ var parseJSONValue = (value) => {
|
|
|
3601
3992
|
return value;
|
|
3602
3993
|
}
|
|
3603
3994
|
};
|
|
3995
|
+
|
|
3996
|
+
class VoiceProviderTimeoutError extends Error {
|
|
3997
|
+
provider;
|
|
3998
|
+
timeoutMs;
|
|
3999
|
+
constructor(provider, timeoutMs) {
|
|
4000
|
+
super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
|
|
4001
|
+
this.name = "VoiceProviderTimeoutError";
|
|
4002
|
+
this.provider = provider;
|
|
4003
|
+
this.timeoutMs = timeoutMs;
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
3604
4006
|
var getMessageToolCalls = (message) => {
|
|
3605
4007
|
const toolCalls = message.metadata?.toolCalls;
|
|
3606
4008
|
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
@@ -3667,17 +4069,25 @@ var createJSONVoiceAssistantModel = (options) => ({
|
|
|
3667
4069
|
var createVoiceProviderRouter = (options) => {
|
|
3668
4070
|
const providerIds = Object.keys(options.providers);
|
|
3669
4071
|
const firstProvider = providerIds[0];
|
|
3670
|
-
const
|
|
3671
|
-
|
|
3672
|
-
} : options.policy;
|
|
4072
|
+
const orchestrationSurface = options.orchestrationProfile?.resolve(options.orchestrationSurface);
|
|
4073
|
+
const policy = resolveVoiceProviderRoutingPolicy(options.policy) ?? resolveVoiceProviderRoutingPolicy(orchestrationSurface?.policy);
|
|
3673
4074
|
const strategy = policy?.strategy ?? "prefer-selected";
|
|
3674
|
-
const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
|
|
3675
|
-
const
|
|
4075
|
+
const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? orchestrationSurface?.fallbackMode ?? "provider-error";
|
|
4076
|
+
const providerProfiles = {
|
|
4077
|
+
...orchestrationSurface?.providerProfiles ?? {},
|
|
4078
|
+
...options.providerProfiles ?? {}
|
|
4079
|
+
};
|
|
4080
|
+
const providerHealthOption = options.providerHealth ?? orchestrationSurface?.providerHealth;
|
|
4081
|
+
const healthOptions = typeof providerHealthOption === "object" ? providerHealthOption : providerHealthOption ? {} : undefined;
|
|
3676
4082
|
const healthState = new Map;
|
|
3677
4083
|
const now = () => healthOptions?.now?.() ?? Date.now();
|
|
3678
4084
|
const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
|
|
3679
4085
|
const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
|
|
3680
4086
|
const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
|
|
4087
|
+
const getProviderTimeoutMs = (provider) => {
|
|
4088
|
+
const timeoutMs = providerProfiles[provider]?.timeoutMs ?? options.timeoutMs ?? orchestrationSurface?.timeoutMs;
|
|
4089
|
+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
|
|
4090
|
+
};
|
|
3681
4091
|
const getHealth = (provider) => {
|
|
3682
4092
|
const existing = healthState.get(provider);
|
|
3683
4093
|
if (existing) {
|
|
@@ -3741,17 +4151,44 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3741
4151
|
return cloneHealth(provider);
|
|
3742
4152
|
};
|
|
3743
4153
|
const resolveAllowedProviders = async (input) => {
|
|
3744
|
-
const allowProviders = policy?.allowProviders ?? options.allowProviders;
|
|
4154
|
+
const allowProviders = policy?.allowProviders ?? options.allowProviders ?? orchestrationSurface?.allowProviders;
|
|
3745
4155
|
const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
|
|
3746
4156
|
return new Set(allowed ?? providerIds);
|
|
3747
4157
|
};
|
|
4158
|
+
const passesBudgetFilters = (provider) => {
|
|
4159
|
+
const profile = providerProfiles[provider];
|
|
4160
|
+
if (typeof policy?.maxCost === "number" && typeof profile?.cost === "number" && profile.cost > policy.maxCost) {
|
|
4161
|
+
return false;
|
|
4162
|
+
}
|
|
4163
|
+
if (typeof policy?.maxLatencyMs === "number" && typeof profile?.latencyMs === "number" && profile.latencyMs > policy.maxLatencyMs) {
|
|
4164
|
+
return false;
|
|
4165
|
+
}
|
|
4166
|
+
if (typeof policy?.minQuality === "number" && typeof profile?.quality === "number" && profile.quality < policy.minQuality) {
|
|
4167
|
+
return false;
|
|
4168
|
+
}
|
|
4169
|
+
return true;
|
|
4170
|
+
};
|
|
4171
|
+
const getBalancedScore = (provider) => {
|
|
4172
|
+
const profile = providerProfiles[provider];
|
|
4173
|
+
if (policy?.scoreProvider) {
|
|
4174
|
+
return policy.scoreProvider(provider, profile);
|
|
4175
|
+
}
|
|
4176
|
+
const weights = policy?.weights ?? {};
|
|
4177
|
+
return (profile?.cost ?? Number.MAX_SAFE_INTEGER) * (weights.cost ?? 1) + (profile?.latencyMs ?? Number.MAX_SAFE_INTEGER) * (weights.latencyMs ?? 0.005) + (profile?.priority ?? 0) * (weights.priority ?? 1) - (profile?.quality ?? 0) * (weights.quality ?? 10);
|
|
4178
|
+
};
|
|
3748
4179
|
const sortProviders = (providers) => {
|
|
3749
|
-
if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
|
|
4180
|
+
if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest" && strategy !== "quality-first" && strategy !== "balanced") {
|
|
3750
4181
|
return providers;
|
|
3751
4182
|
}
|
|
3752
4183
|
return [...providers].sort((left, right) => {
|
|
3753
|
-
const leftProfile =
|
|
3754
|
-
const rightProfile =
|
|
4184
|
+
const leftProfile = providerProfiles[left];
|
|
4185
|
+
const rightProfile = providerProfiles[right];
|
|
4186
|
+
if (strategy === "quality-first") {
|
|
4187
|
+
return (rightProfile?.quality ?? Number.MIN_SAFE_INTEGER) - (leftProfile?.quality ?? Number.MIN_SAFE_INTEGER) || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER) || (leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER) || (leftProfile?.cost ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.cost ?? Number.MAX_SAFE_INTEGER);
|
|
4188
|
+
}
|
|
4189
|
+
if (strategy === "balanced") {
|
|
4190
|
+
return getBalancedScore(left) - getBalancedScore(right);
|
|
4191
|
+
}
|
|
3755
4192
|
const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
3756
4193
|
const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
3757
4194
|
return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
|
|
@@ -3760,13 +4197,15 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3760
4197
|
const resolveOrder = async (input) => {
|
|
3761
4198
|
const selectedProvider = await options.selectProvider?.(input);
|
|
3762
4199
|
const allowedProviders = await resolveAllowedProviders(input);
|
|
3763
|
-
const
|
|
3764
|
-
const
|
|
4200
|
+
const fallbackSource = options.fallback ?? orchestrationSurface?.fallback;
|
|
4201
|
+
const fallbackOrder = typeof fallbackSource === "function" ? await fallbackSource(input) : fallbackSource;
|
|
4202
|
+
const allowedRankedProviders = sortProviders([
|
|
3765
4203
|
...fallbackOrder ?? providerIds
|
|
3766
4204
|
]).filter((provider) => allowedProviders.has(provider));
|
|
4205
|
+
const rankedProviders = allowedRankedProviders.filter(passesBudgetFilters);
|
|
3767
4206
|
const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
|
|
3768
4207
|
const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
|
|
3769
|
-
const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
|
|
4208
|
+
const preferred = selectedProvider && allowedProviders.has(selectedProvider) && passesBudgetFilters(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
|
|
3770
4209
|
const seen = new Set;
|
|
3771
4210
|
const order = [];
|
|
3772
4211
|
const candidates = strategy === "ordered" ? candidateRankedProviders : [
|
|
@@ -3789,6 +4228,25 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3789
4228
|
const emit = async (event, input) => {
|
|
3790
4229
|
await options.onProviderEvent?.(event, input);
|
|
3791
4230
|
};
|
|
4231
|
+
const runProvider = async (provider, model, input) => {
|
|
4232
|
+
const timeoutMs = getProviderTimeoutMs(provider);
|
|
4233
|
+
if (!timeoutMs) {
|
|
4234
|
+
return model.generate(input);
|
|
4235
|
+
}
|
|
4236
|
+
let timeout;
|
|
4237
|
+
try {
|
|
4238
|
+
return await Promise.race([
|
|
4239
|
+
model.generate(input),
|
|
4240
|
+
new Promise((_, reject) => {
|
|
4241
|
+
timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
|
|
4242
|
+
})
|
|
4243
|
+
]);
|
|
4244
|
+
} finally {
|
|
4245
|
+
if (timeout) {
|
|
4246
|
+
clearTimeout(timeout);
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
};
|
|
3792
4250
|
return {
|
|
3793
4251
|
generate: async (input) => {
|
|
3794
4252
|
const { order, selectedProvider } = await resolveOrder(input);
|
|
@@ -3803,12 +4261,14 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3803
4261
|
}
|
|
3804
4262
|
const startedAt = Date.now();
|
|
3805
4263
|
try {
|
|
3806
|
-
const output = await model
|
|
4264
|
+
const output = await runProvider(provider, model, input);
|
|
3807
4265
|
const providerHealth = recordProviderSuccess(provider);
|
|
3808
4266
|
await emit({
|
|
3809
4267
|
at: Date.now(),
|
|
4268
|
+
attempt: index + 1,
|
|
3810
4269
|
elapsedMs: Date.now() - startedAt,
|
|
3811
4270
|
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
4271
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
3812
4272
|
provider,
|
|
3813
4273
|
providerHealth,
|
|
3814
4274
|
recovered: provider !== selectedProvider,
|
|
@@ -3820,22 +4280,26 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3820
4280
|
lastError = error;
|
|
3821
4281
|
const hasNextProvider = index < order.length - 1;
|
|
3822
4282
|
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
4283
|
+
const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
|
|
3823
4284
|
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
3824
4285
|
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
3825
4286
|
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
3826
4287
|
const nextProvider = hasNextProvider ? order[index + 1] : undefined;
|
|
3827
4288
|
await emit({
|
|
3828
4289
|
at: Date.now(),
|
|
4290
|
+
attempt: index + 1,
|
|
3829
4291
|
elapsedMs: Date.now() - startedAt,
|
|
3830
4292
|
error: errorMessage(error),
|
|
3831
4293
|
fallbackProvider: shouldFallback ? nextProvider : undefined,
|
|
4294
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
3832
4295
|
provider,
|
|
3833
4296
|
providerHealth,
|
|
3834
4297
|
rateLimited,
|
|
3835
4298
|
selectedProvider,
|
|
3836
4299
|
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
3837
4300
|
suppressedUntil: providerHealth?.suppressedUntil,
|
|
3838
|
-
status: "error"
|
|
4301
|
+
status: "error",
|
|
4302
|
+
timedOut
|
|
3839
4303
|
}, input);
|
|
3840
4304
|
if (!hasNextProvider || !shouldFallback) {
|
|
3841
4305
|
throw error;
|
|
@@ -4457,7 +4921,7 @@ var createVoiceMemoryStore = () => {
|
|
|
4457
4921
|
};
|
|
4458
4922
|
|
|
4459
4923
|
// src/session.ts
|
|
4460
|
-
import { Buffer } from "buffer";
|
|
4924
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
4461
4925
|
|
|
4462
4926
|
// src/handoff.ts
|
|
4463
4927
|
var toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
@@ -4776,6 +5240,12 @@ var DEFAULT_FORMAT = {
|
|
|
4776
5240
|
encoding: "pcm_s16le",
|
|
4777
5241
|
sampleRateHz: 16000
|
|
4778
5242
|
};
|
|
5243
|
+
var DEFAULT_REALTIME_FORMAT = {
|
|
5244
|
+
channels: 1,
|
|
5245
|
+
container: "raw",
|
|
5246
|
+
encoding: "pcm_s16le",
|
|
5247
|
+
sampleRateHz: 24000
|
|
5248
|
+
};
|
|
4779
5249
|
var toError = (value) => value instanceof Error ? value : new Error(String(value));
|
|
4780
5250
|
var createEmptyCurrentTurn = () => ({
|
|
4781
5251
|
finalText: "",
|
|
@@ -4788,7 +5258,7 @@ var createEmptyCurrentTurn = () => ({
|
|
|
4788
5258
|
transcripts: []
|
|
4789
5259
|
});
|
|
4790
5260
|
var cloneTranscript = (transcript) => ({ ...transcript });
|
|
4791
|
-
var encodeBase64 = (chunk) =>
|
|
5261
|
+
var encodeBase64 = (chunk) => Buffer2.from(chunk).toString("base64");
|
|
4792
5262
|
var countWords2 = (text) => text.trim().split(/\s+/).filter(Boolean).length;
|
|
4793
5263
|
var normalizeText2 = (text) => text.trim().replace(/\s+/g, " ");
|
|
4794
5264
|
var getAudioChunkDurationMs = (chunk) => chunk.byteLength / (DEFAULT_FORMAT.sampleRateHz * DEFAULT_FORMAT.channels * 2) * 1000;
|
|
@@ -4959,7 +5429,7 @@ var createVoiceSession = (options) => {
|
|
|
4959
5429
|
} : undefined;
|
|
4960
5430
|
const appendTrace = async (input) => {
|
|
4961
5431
|
await options.trace?.append({
|
|
4962
|
-
at: Date.now(),
|
|
5432
|
+
at: input.at ?? Date.now(),
|
|
4963
5433
|
metadata: input.metadata,
|
|
4964
5434
|
payload: input.payload,
|
|
4965
5435
|
scenarioId: input.session?.scenarioId ?? options.scenarioId,
|
|
@@ -4968,6 +5438,13 @@ var createVoiceSession = (options) => {
|
|
|
4968
5438
|
type: input.type
|
|
4969
5439
|
});
|
|
4970
5440
|
};
|
|
5441
|
+
const appendTurnLatencyStage = async (input) => appendTrace({
|
|
5442
|
+
at: input.at,
|
|
5443
|
+
payload: { stage: input.stage },
|
|
5444
|
+
session: input.session,
|
|
5445
|
+
turnId: input.turnId,
|
|
5446
|
+
type: "turn_latency.stage"
|
|
5447
|
+
});
|
|
4971
5448
|
const phraseHints = options.phraseHints ?? [];
|
|
4972
5449
|
const lexicon = options.lexicon ?? [];
|
|
4973
5450
|
let socket = options.socket;
|
|
@@ -5046,6 +5523,18 @@ var createVoiceSession = (options) => {
|
|
|
5046
5523
|
type: "call_lifecycle"
|
|
5047
5524
|
});
|
|
5048
5525
|
};
|
|
5526
|
+
const sendReplay = async (session) => {
|
|
5527
|
+
await send({
|
|
5528
|
+
assistantTexts: session.turns.flatMap((turn) => turn.assistantText ? [turn.assistantText] : []),
|
|
5529
|
+
call: session.call,
|
|
5530
|
+
partial: session.currentTurn.partialText,
|
|
5531
|
+
scenarioId: session.scenarioId,
|
|
5532
|
+
sessionId: options.id,
|
|
5533
|
+
status: session.status,
|
|
5534
|
+
turns: session.turns,
|
|
5535
|
+
type: "replay"
|
|
5536
|
+
});
|
|
5537
|
+
};
|
|
5049
5538
|
const runHandoff = async (input) => {
|
|
5050
5539
|
const queuedDelivery = options.handoff?.deliveryQueue ? createVoiceHandoffDeliveryRecord({
|
|
5051
5540
|
action: input.action,
|
|
@@ -5149,6 +5638,23 @@ var createVoiceSession = (options) => {
|
|
|
5149
5638
|
});
|
|
5150
5639
|
}
|
|
5151
5640
|
};
|
|
5641
|
+
const sendAssistantAudio = async (chunk, input) => {
|
|
5642
|
+
const normalizedChunk = chunk instanceof Uint8Array ? new Uint8Array(chunk) : chunk instanceof ArrayBuffer ? new Uint8Array(chunk.slice(0)) : new Uint8Array(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
|
|
5643
|
+
await send({
|
|
5644
|
+
chunkBase64: encodeBase64(normalizedChunk),
|
|
5645
|
+
format: input.format,
|
|
5646
|
+
receivedAt: input.receivedAt,
|
|
5647
|
+
turnId: activeTTSTurnId,
|
|
5648
|
+
type: "audio"
|
|
5649
|
+
});
|
|
5650
|
+
if (activeTTSTurnId) {
|
|
5651
|
+
await appendTurnLatencyStage({
|
|
5652
|
+
at: input.receivedAt,
|
|
5653
|
+
stage: "assistant_audio_received",
|
|
5654
|
+
turnId: activeTTSTurnId
|
|
5655
|
+
});
|
|
5656
|
+
}
|
|
5657
|
+
};
|
|
5152
5658
|
const scheduleTurnCommit = (delayMs, reason, reset = true) => {
|
|
5153
5659
|
if (!reset && silenceTimer) {
|
|
5154
5660
|
return;
|
|
@@ -5850,8 +6356,12 @@ var createVoiceSession = (options) => {
|
|
|
5850
6356
|
if (sttSession) {
|
|
5851
6357
|
return sttSession;
|
|
5852
6358
|
}
|
|
5853
|
-
const
|
|
5854
|
-
|
|
6359
|
+
const inputAdapter = options.realtime ?? options.stt;
|
|
6360
|
+
if (!inputAdapter) {
|
|
6361
|
+
throw new Error("Voice session requires either an stt or realtime adapter.");
|
|
6362
|
+
}
|
|
6363
|
+
const openedSession = await inputAdapter.open({
|
|
6364
|
+
format: options.realtime ? options.realtimeInputFormat ?? DEFAULT_REALTIME_FORMAT : DEFAULT_FORMAT,
|
|
5855
6365
|
languageStrategy: options.languageStrategy,
|
|
5856
6366
|
lexicon,
|
|
5857
6367
|
phraseHints,
|
|
@@ -5886,6 +6396,16 @@ var createVoiceSession = (options) => {
|
|
|
5886
6396
|
openedSession.on("close", (event) => {
|
|
5887
6397
|
runAdapterEvent("adapter.close", () => handleClose(event));
|
|
5888
6398
|
});
|
|
6399
|
+
if (options.realtime) {
|
|
6400
|
+
openedSession.on("audio", ({ chunk, format, receivedAt }) => {
|
|
6401
|
+
runAdapterEvent("adapter.audio", async () => {
|
|
6402
|
+
await sendAssistantAudio(chunk, {
|
|
6403
|
+
format,
|
|
6404
|
+
receivedAt
|
|
6405
|
+
});
|
|
6406
|
+
});
|
|
6407
|
+
});
|
|
6408
|
+
}
|
|
5889
6409
|
return openedSession;
|
|
5890
6410
|
};
|
|
5891
6411
|
const ensureTTSSession = async () => {
|
|
@@ -5910,13 +6430,9 @@ var createVoiceSession = (options) => {
|
|
|
5910
6430
|
if (ttsSession !== openedSession) {
|
|
5911
6431
|
return;
|
|
5912
6432
|
}
|
|
5913
|
-
|
|
5914
|
-
await send({
|
|
5915
|
-
chunkBase64: encodeBase64(normalizedChunk),
|
|
6433
|
+
await sendAssistantAudio(chunk, {
|
|
5916
6434
|
format,
|
|
5917
|
-
receivedAt
|
|
5918
|
-
turnId: activeTTSTurnId,
|
|
5919
|
-
type: "audio"
|
|
6435
|
+
receivedAt
|
|
5920
6436
|
});
|
|
5921
6437
|
});
|
|
5922
6438
|
});
|
|
@@ -5960,9 +6476,32 @@ var createVoiceSession = (options) => {
|
|
|
5960
6476
|
});
|
|
5961
6477
|
};
|
|
5962
6478
|
const completeTurn = async (session, turn) => {
|
|
6479
|
+
const liveOpsControl = await options.liveOps?.getControl(options.id);
|
|
6480
|
+
if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
|
|
6481
|
+
await appendTrace({
|
|
6482
|
+
metadata: {
|
|
6483
|
+
source: "voice-live-ops"
|
|
6484
|
+
},
|
|
6485
|
+
payload: {
|
|
6486
|
+
action: "turn.skipped",
|
|
6487
|
+
control: liveOpsControl,
|
|
6488
|
+
reason: liveOpsControl.operatorTakeover ? "operator-takeover" : "assistant-paused",
|
|
6489
|
+
status: "skipped"
|
|
6490
|
+
},
|
|
6491
|
+
session,
|
|
6492
|
+
turnId: turn.id,
|
|
6493
|
+
type: "operator.action"
|
|
6494
|
+
});
|
|
6495
|
+
return;
|
|
6496
|
+
}
|
|
6497
|
+
const injectedInstruction = liveOpsControl?.injectedInstruction?.trim();
|
|
5963
6498
|
const committedOutput = await options.route.onTurn({
|
|
5964
6499
|
api,
|
|
5965
6500
|
context: options.context,
|
|
6501
|
+
liveOps: liveOpsControl ? {
|
|
6502
|
+
control: liveOpsControl,
|
|
6503
|
+
injectedInstruction
|
|
6504
|
+
} : undefined,
|
|
5966
6505
|
session,
|
|
5967
6506
|
turn
|
|
5968
6507
|
});
|
|
@@ -5976,6 +6515,7 @@ var createVoiceSession = (options) => {
|
|
|
5976
6515
|
voicemail: committedOutput?.voicemail
|
|
5977
6516
|
};
|
|
5978
6517
|
if (output?.assistantText) {
|
|
6518
|
+
const assistantTextStartedAt = Date.now();
|
|
5979
6519
|
await writeSession((currentSession) => {
|
|
5980
6520
|
setTurnResult(currentSession, turn.id, {
|
|
5981
6521
|
assistantText: output.assistantText
|
|
@@ -5986,10 +6526,17 @@ var createVoiceSession = (options) => {
|
|
|
5986
6526
|
turnId: turn.id,
|
|
5987
6527
|
type: "assistant"
|
|
5988
6528
|
});
|
|
6529
|
+
await appendTurnLatencyStage({
|
|
6530
|
+
at: assistantTextStartedAt,
|
|
6531
|
+
session,
|
|
6532
|
+
stage: "assistant_text_started",
|
|
6533
|
+
turnId: turn.id
|
|
6534
|
+
});
|
|
5989
6535
|
await appendTrace({
|
|
5990
6536
|
payload: {
|
|
5991
6537
|
text: output.assistantText,
|
|
5992
|
-
ttsConfigured: Boolean(options.tts)
|
|
6538
|
+
ttsConfigured: Boolean(options.tts),
|
|
6539
|
+
realtimeConfigured: Boolean(options.realtime)
|
|
5993
6540
|
},
|
|
5994
6541
|
session,
|
|
5995
6542
|
turnId: turn.id,
|
|
@@ -6000,7 +6547,18 @@ var createVoiceSession = (options) => {
|
|
|
6000
6547
|
if (activeTTSSession) {
|
|
6001
6548
|
const ttsStartedAt = Date.now();
|
|
6002
6549
|
activeTTSTurnId = turn.id;
|
|
6550
|
+
await appendTurnLatencyStage({
|
|
6551
|
+
at: ttsStartedAt,
|
|
6552
|
+
session,
|
|
6553
|
+
stage: "tts_send_started",
|
|
6554
|
+
turnId: turn.id
|
|
6555
|
+
});
|
|
6003
6556
|
await activeTTSSession.send(output.assistantText);
|
|
6557
|
+
await appendTurnLatencyStage({
|
|
6558
|
+
session,
|
|
6559
|
+
stage: "tts_send_completed",
|
|
6560
|
+
turnId: turn.id
|
|
6561
|
+
});
|
|
6004
6562
|
await appendTrace({
|
|
6005
6563
|
payload: {
|
|
6006
6564
|
elapsedMs: Date.now() - ttsStartedAt,
|
|
@@ -6010,9 +6568,35 @@ var createVoiceSession = (options) => {
|
|
|
6010
6568
|
turnId: turn.id,
|
|
6011
6569
|
type: "turn.assistant"
|
|
6012
6570
|
});
|
|
6571
|
+
} else if (options.realtime) {
|
|
6572
|
+
const activeRealtimeSession = await ensureAdapter();
|
|
6573
|
+
const realtimeStartedAt = Date.now();
|
|
6574
|
+
activeTTSTurnId = turn.id;
|
|
6575
|
+
await appendTurnLatencyStage({
|
|
6576
|
+
at: realtimeStartedAt,
|
|
6577
|
+
session,
|
|
6578
|
+
stage: "tts_send_started",
|
|
6579
|
+
turnId: turn.id
|
|
6580
|
+
});
|
|
6581
|
+
await activeRealtimeSession.send(output.assistantText);
|
|
6582
|
+
await appendTurnLatencyStage({
|
|
6583
|
+
session,
|
|
6584
|
+
stage: "tts_send_completed",
|
|
6585
|
+
turnId: turn.id
|
|
6586
|
+
});
|
|
6587
|
+
await appendTrace({
|
|
6588
|
+
payload: {
|
|
6589
|
+
elapsedMs: Date.now() - realtimeStartedAt,
|
|
6590
|
+
mode: "realtime",
|
|
6591
|
+
status: "sent"
|
|
6592
|
+
},
|
|
6593
|
+
session,
|
|
6594
|
+
turnId: turn.id,
|
|
6595
|
+
type: "turn.assistant"
|
|
6596
|
+
});
|
|
6013
6597
|
}
|
|
6014
6598
|
} catch (error) {
|
|
6015
|
-
logger.warn("voice
|
|
6599
|
+
logger.warn("voice assistant audio send failed", {
|
|
6016
6600
|
error: toError(error).message,
|
|
6017
6601
|
sessionId: options.id,
|
|
6018
6602
|
turnId: turn.id
|
|
@@ -6020,7 +6604,7 @@ var createVoiceSession = (options) => {
|
|
|
6020
6604
|
await appendTrace({
|
|
6021
6605
|
payload: {
|
|
6022
6606
|
error: toError(error).message,
|
|
6023
|
-
status: "tts-send-failed"
|
|
6607
|
+
status: options.realtime ? "realtime-send-failed" : "tts-send-failed"
|
|
6024
6608
|
},
|
|
6025
6609
|
session,
|
|
6026
6610
|
turnId: turn.id,
|
|
@@ -6197,11 +6781,35 @@ var createVoiceSession = (options) => {
|
|
|
6197
6781
|
turnId: turn.id,
|
|
6198
6782
|
type: "turn.cost"
|
|
6199
6783
|
});
|
|
6784
|
+
const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
|
|
6785
|
+
const finalTranscriptAt = turn.transcripts.filter((transcript) => transcript.isFinal).map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
|
|
6786
|
+
if (firstTranscriptAt !== undefined) {
|
|
6787
|
+
await appendTurnLatencyStage({
|
|
6788
|
+
at: firstTranscriptAt,
|
|
6789
|
+
session: updatedSession,
|
|
6790
|
+
stage: "speech_detected",
|
|
6791
|
+
turnId: turn.id
|
|
6792
|
+
});
|
|
6793
|
+
}
|
|
6794
|
+
if (finalTranscriptAt !== undefined) {
|
|
6795
|
+
await appendTurnLatencyStage({
|
|
6796
|
+
at: finalTranscriptAt,
|
|
6797
|
+
session: updatedSession,
|
|
6798
|
+
stage: "final_transcript",
|
|
6799
|
+
turnId: turn.id
|
|
6800
|
+
});
|
|
6801
|
+
}
|
|
6802
|
+
await appendTurnLatencyStage({
|
|
6803
|
+
at: turn.committedAt,
|
|
6804
|
+
session: updatedSession,
|
|
6805
|
+
stage: "turn_committed",
|
|
6806
|
+
turnId: turn.id
|
|
6807
|
+
});
|
|
6200
6808
|
await send({
|
|
6201
6809
|
turn,
|
|
6202
6810
|
type: "turn"
|
|
6203
6811
|
});
|
|
6204
|
-
if (options.sttLifecycle === "turn-scoped") {
|
|
6812
|
+
if (options.stt && options.sttLifecycle === "turn-scoped") {
|
|
6205
6813
|
await closeAdapter("turn-commit");
|
|
6206
6814
|
}
|
|
6207
6815
|
await completeTurn(updatedSession, turn);
|
|
@@ -6264,6 +6872,7 @@ var createVoiceSession = (options) => {
|
|
|
6264
6872
|
scenarioId: session.scenarioId,
|
|
6265
6873
|
type: "session"
|
|
6266
6874
|
});
|
|
6875
|
+
await sendReplay(session);
|
|
6267
6876
|
if (shouldFireOnSession) {
|
|
6268
6877
|
await options.route.onCallStart?.({
|
|
6269
6878
|
api,
|
|
@@ -7640,10 +8249,981 @@ var runVoiceSessionBenchmarkSeries = async (input) => {
|
|
|
7640
8249
|
});
|
|
7641
8250
|
};
|
|
7642
8251
|
// src/telephony/twilio.ts
|
|
7643
|
-
import { Buffer as
|
|
8252
|
+
import { Buffer as Buffer3 } from "buffer";
|
|
8253
|
+
import { Elysia as Elysia2 } from "elysia";
|
|
8254
|
+
|
|
8255
|
+
// src/telephonyOutcome.ts
|
|
8256
|
+
import { Elysia } from "elysia";
|
|
8257
|
+
var DEFAULT_COMPLETED_STATUSES = [
|
|
8258
|
+
"answered",
|
|
8259
|
+
"completed",
|
|
8260
|
+
"complete",
|
|
8261
|
+
"connected",
|
|
8262
|
+
"in-progress",
|
|
8263
|
+
"live"
|
|
8264
|
+
];
|
|
8265
|
+
var DEFAULT_NO_ANSWER_STATUSES = [
|
|
8266
|
+
"busy",
|
|
8267
|
+
"canceled",
|
|
8268
|
+
"cancelled",
|
|
8269
|
+
"failed",
|
|
8270
|
+
"no-answer",
|
|
8271
|
+
"no_answer",
|
|
8272
|
+
"not-answered",
|
|
8273
|
+
"ring-no-answer",
|
|
8274
|
+
"timeout",
|
|
8275
|
+
"unanswered"
|
|
8276
|
+
];
|
|
8277
|
+
var DEFAULT_VOICEMAIL_STATUSES = [
|
|
8278
|
+
"answering-machine",
|
|
8279
|
+
"machine",
|
|
8280
|
+
"voicemail",
|
|
8281
|
+
"voice-mail"
|
|
8282
|
+
];
|
|
8283
|
+
var DEFAULT_TRANSFER_STATUSES = ["bridged", "forwarded", "transferred"];
|
|
8284
|
+
var DEFAULT_ESCALATION_STATUSES = ["escalated", "human-required", "operator"];
|
|
8285
|
+
var DEFAULT_FAILED_STATUSES = ["busy", "failed", "no-answer"];
|
|
8286
|
+
var DEFAULT_MACHINE_VOICEMAIL_VALUES = [
|
|
8287
|
+
"answering-machine",
|
|
8288
|
+
"fax",
|
|
8289
|
+
"machine",
|
|
8290
|
+
"machine-end-beep",
|
|
8291
|
+
"machine-end-other",
|
|
8292
|
+
"machine-start",
|
|
8293
|
+
"voicemail"
|
|
8294
|
+
];
|
|
8295
|
+
var DEFAULT_NO_ANSWER_SIP_CODES = [408, 480, 486, 487, 603];
|
|
8296
|
+
var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
8297
|
+
var uniqueSorted = (values) => Array.from(new Set(values)).sort();
|
|
8298
|
+
var findMissing = (values, required) => {
|
|
8299
|
+
if (!required?.length) {
|
|
8300
|
+
return [];
|
|
8301
|
+
}
|
|
8302
|
+
const valueSet = new Set(values);
|
|
8303
|
+
return required.filter((value) => !valueSet.has(value));
|
|
8304
|
+
};
|
|
8305
|
+
|
|
8306
|
+
class VoiceTelephonyWebhookVerificationError extends Error {
|
|
8307
|
+
result;
|
|
8308
|
+
constructor(result) {
|
|
8309
|
+
super(result.ok ? "telephony webhook verified" : result.reason);
|
|
8310
|
+
this.name = "VoiceTelephonyWebhookVerificationError";
|
|
8311
|
+
this.result = result;
|
|
8312
|
+
}
|
|
8313
|
+
}
|
|
8314
|
+
var createMemoryVoiceTelephonyWebhookIdempotencyStore = () => {
|
|
8315
|
+
const decisions = new Map;
|
|
8316
|
+
return {
|
|
8317
|
+
get: (key) => decisions.get(key),
|
|
8318
|
+
set: (key, decision) => {
|
|
8319
|
+
decisions.set(key, decision);
|
|
8320
|
+
}
|
|
8321
|
+
};
|
|
8322
|
+
};
|
|
8323
|
+
var isTelephonyWebhookProvider = (value) => value === "generic" || value === "plivo" || value === "telnyx" || value === "twilio";
|
|
8324
|
+
var isTelephonyOutcomeAction = (value) => value === "complete" || value === "escalate" || value === "ignore" || value === "no-answer" || value === "transfer" || value === "voicemail";
|
|
8325
|
+
var isCallDisposition = (value) => value === "completed" || value === "escalated" || value === "failed" || value === "no-answer" || value === "transferred" || value === "voicemail";
|
|
8326
|
+
var evaluateVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
|
|
8327
|
+
const issues = [];
|
|
8328
|
+
const decisions = input.decisions ?? [];
|
|
8329
|
+
const verificationAttempts = input.verificationAttempts ?? [];
|
|
8330
|
+
const actions = uniqueSorted(decisions.map((decision) => decision.decision?.action ?? decision.action).filter(isTelephonyOutcomeAction));
|
|
8331
|
+
const dispositions = uniqueSorted(decisions.map((decision) => decision.decision?.disposition ?? decision.disposition).filter(isCallDisposition));
|
|
8332
|
+
const providers = uniqueSorted(decisions.map((decision) => decision.provider ?? decision.event?.provider).filter(isTelephonyWebhookProvider));
|
|
8333
|
+
const sources = uniqueSorted(decisions.map((decision) => decision.decision?.source ?? decision.source).filter((source) => typeof source === "string"));
|
|
8334
|
+
const applied = decisions.filter((decision) => decision.applied === true).length;
|
|
8335
|
+
const duplicateDecisions = decisions.filter((decision) => decision.duplicate === true);
|
|
8336
|
+
const duplicateProviders = uniqueSorted(duplicateDecisions.map((decision) => decision.provider ?? decision.event?.provider).filter(isTelephonyWebhookProvider));
|
|
8337
|
+
const duplicateIdempotencyKeys = new Set(duplicateDecisions.map((decision) => decision.idempotencyKey).filter((key) => typeof key === "string" && key.length > 0)).size;
|
|
8338
|
+
const duplicateCampaignOutcomesApplied = duplicateDecisions.filter((decision) => isRecord(decision.campaignOutcome) && decision.campaignOutcome.applied === true).length;
|
|
8339
|
+
const duplicateOutcomeReasons = uniqueSorted(duplicateDecisions.map((decision) => isRecord(decision.campaignOutcome) ? decision.campaignOutcome.reason : undefined).filter((reason) => typeof reason === "string"));
|
|
8340
|
+
const routeResults = decisions.filter((decision) => isRecord(decision.routeResult)).length;
|
|
8341
|
+
const missingSessionIds = decisions.filter((decision) => !decision.sessionId).length;
|
|
8342
|
+
const rejectedVerificationAttempts = verificationAttempts.filter((attempt) => attempt.rejected === true || attempt.status === 401 || attempt.verification?.ok === false && attempt.verification.reason === "invalid-signature");
|
|
8343
|
+
const rejectedVerificationProviders = uniqueSorted(rejectedVerificationAttempts.map((attempt) => attempt.provider).filter(isTelephonyWebhookProvider));
|
|
8344
|
+
const replayRejectedVerificationAttempts = rejectedVerificationAttempts.filter((attempt) => attempt.replayRejected === true);
|
|
8345
|
+
const replayRejectedVerificationProviders = uniqueSorted(replayRejectedVerificationAttempts.map((attempt) => attempt.provider).filter(isTelephonyWebhookProvider));
|
|
8346
|
+
const rejectedVerificationSideEffects = rejectedVerificationAttempts.reduce((total, attempt) => total + Math.max(0, attempt.sideEffects ?? 0), 0);
|
|
8347
|
+
if (input.minDecisions !== undefined && decisions.length < input.minDecisions) {
|
|
8348
|
+
issues.push(`Expected at least ${String(input.minDecisions)} telephony webhook decision(s), found ${String(decisions.length)}.`);
|
|
8349
|
+
}
|
|
8350
|
+
if (input.minApplied !== undefined && applied < input.minApplied) {
|
|
8351
|
+
issues.push(`Expected at least ${String(input.minApplied)} applied telephony webhook decision(s), found ${String(applied)}.`);
|
|
8352
|
+
}
|
|
8353
|
+
if (input.minDuplicates !== undefined && duplicateDecisions.length < input.minDuplicates) {
|
|
8354
|
+
issues.push(`Expected at least ${String(input.minDuplicates)} duplicate telephony webhook decision(s), found ${String(duplicateDecisions.length)}.`);
|
|
8355
|
+
}
|
|
8356
|
+
if (input.minDuplicateIdempotencyKeys !== undefined && duplicateIdempotencyKeys < input.minDuplicateIdempotencyKeys) {
|
|
8357
|
+
issues.push(`Expected at least ${String(input.minDuplicateIdempotencyKeys)} duplicate telephony webhook idempotency key(s), found ${String(duplicateIdempotencyKeys)}.`);
|
|
8358
|
+
}
|
|
8359
|
+
if (input.maxDuplicateCampaignOutcomesApplied !== undefined && duplicateCampaignOutcomesApplied > input.maxDuplicateCampaignOutcomesApplied) {
|
|
8360
|
+
issues.push(`Expected at most ${String(input.maxDuplicateCampaignOutcomesApplied)} duplicate telephony webhook campaign outcome application(s), found ${String(duplicateCampaignOutcomesApplied)}.`);
|
|
8361
|
+
}
|
|
8362
|
+
if (input.minRejectedVerificationAttempts !== undefined && rejectedVerificationAttempts.length < input.minRejectedVerificationAttempts) {
|
|
8363
|
+
issues.push(`Expected at least ${String(input.minRejectedVerificationAttempts)} rejected telephony webhook verification attempt(s), found ${String(rejectedVerificationAttempts.length)}.`);
|
|
8364
|
+
}
|
|
8365
|
+
if (input.maxRejectedVerificationSideEffects !== undefined && rejectedVerificationSideEffects > input.maxRejectedVerificationSideEffects) {
|
|
8366
|
+
issues.push(`Expected at most ${String(input.maxRejectedVerificationSideEffects)} rejected telephony webhook side effect(s), found ${String(rejectedVerificationSideEffects)}.`);
|
|
8367
|
+
}
|
|
8368
|
+
if (input.minReplayRejectedVerificationAttempts !== undefined && replayRejectedVerificationAttempts.length < input.minReplayRejectedVerificationAttempts) {
|
|
8369
|
+
issues.push(`Expected at least ${String(input.minReplayRejectedVerificationAttempts)} replay-rejected telephony webhook verification attempt(s), found ${String(replayRejectedVerificationAttempts.length)}.`);
|
|
8370
|
+
}
|
|
8371
|
+
if (input.maxMissingSessionIds !== undefined && missingSessionIds > input.maxMissingSessionIds) {
|
|
8372
|
+
issues.push(`Expected at most ${String(input.maxMissingSessionIds)} telephony webhook decision(s) without sessionId, found ${String(missingSessionIds)}.`);
|
|
8373
|
+
}
|
|
8374
|
+
if (input.requireRouteResults && routeResults < decisions.length) {
|
|
8375
|
+
issues.push(`Expected every telephony webhook decision to include a route result, found ${String(routeResults)} of ${String(decisions.length)}.`);
|
|
8376
|
+
}
|
|
8377
|
+
for (const provider of findMissing(providers, input.requiredProviders)) {
|
|
8378
|
+
issues.push(`Missing telephony webhook provider: ${provider}.`);
|
|
8379
|
+
}
|
|
8380
|
+
for (const provider of findMissing(duplicateProviders, input.requiredDuplicateProviders)) {
|
|
8381
|
+
issues.push(`Missing duplicate telephony webhook provider: ${provider}.`);
|
|
8382
|
+
}
|
|
8383
|
+
for (const provider of findMissing(rejectedVerificationProviders, input.requiredRejectedVerificationProviders)) {
|
|
8384
|
+
issues.push(`Missing rejected telephony webhook verification provider: ${provider}.`);
|
|
8385
|
+
}
|
|
8386
|
+
for (const provider of findMissing(replayRejectedVerificationProviders, input.requiredReplayRejectedVerificationProviders)) {
|
|
8387
|
+
issues.push(`Missing replay-rejected telephony webhook verification provider: ${provider}.`);
|
|
8388
|
+
}
|
|
8389
|
+
for (const action of findMissing(actions, input.requiredActions)) {
|
|
8390
|
+
issues.push(`Missing telephony webhook action: ${action}.`);
|
|
8391
|
+
}
|
|
8392
|
+
for (const disposition of findMissing(dispositions, input.requiredDispositions)) {
|
|
8393
|
+
issues.push(`Missing telephony webhook disposition: ${disposition}.`);
|
|
8394
|
+
}
|
|
8395
|
+
return {
|
|
8396
|
+
actions,
|
|
8397
|
+
applied,
|
|
8398
|
+
decisions: decisions.length,
|
|
8399
|
+
dispositions,
|
|
8400
|
+
duplicateCampaignOutcomesApplied,
|
|
8401
|
+
duplicateIdempotencyKeys,
|
|
8402
|
+
duplicateOutcomeReasons,
|
|
8403
|
+
duplicateProviders,
|
|
8404
|
+
duplicates: duplicateDecisions.length,
|
|
8405
|
+
issues,
|
|
8406
|
+
missingSessionIds,
|
|
8407
|
+
ok: issues.length === 0,
|
|
8408
|
+
providers,
|
|
8409
|
+
rejectedVerificationAttempts: rejectedVerificationAttempts.length,
|
|
8410
|
+
rejectedVerificationProviders,
|
|
8411
|
+
rejectedVerificationSideEffects,
|
|
8412
|
+
replayRejectedVerificationAttempts: replayRejectedVerificationAttempts.length,
|
|
8413
|
+
replayRejectedVerificationProviders,
|
|
8414
|
+
routeResults,
|
|
8415
|
+
sources,
|
|
8416
|
+
verificationAttempts: verificationAttempts.length
|
|
8417
|
+
};
|
|
8418
|
+
};
|
|
8419
|
+
var assertVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
|
|
8420
|
+
const assertion = evaluateVoiceTelephonyWebhookNormalizationEvidence(input);
|
|
8421
|
+
if (!assertion.ok) {
|
|
8422
|
+
throw new Error(`Voice telephony webhook normalization evidence assertion failed: ${assertion.issues.join(" ")}`);
|
|
8423
|
+
}
|
|
8424
|
+
return assertion;
|
|
8425
|
+
};
|
|
8426
|
+
var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
|
|
8427
|
+
var firstString = (source, keys) => {
|
|
8428
|
+
for (const key of keys) {
|
|
8429
|
+
const value = source[key];
|
|
8430
|
+
if (typeof value === "string" && value.trim()) {
|
|
8431
|
+
return value.trim();
|
|
8432
|
+
}
|
|
8433
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
8434
|
+
return String(value);
|
|
8435
|
+
}
|
|
8436
|
+
}
|
|
8437
|
+
};
|
|
8438
|
+
var firstNumber = (source, keys) => {
|
|
8439
|
+
for (const key of keys) {
|
|
8440
|
+
const value = source[key];
|
|
8441
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
8442
|
+
return value;
|
|
8443
|
+
}
|
|
8444
|
+
if (typeof value === "string" && value.trim()) {
|
|
8445
|
+
const parsed = Number(value);
|
|
8446
|
+
if (Number.isFinite(parsed)) {
|
|
8447
|
+
return parsed;
|
|
8448
|
+
}
|
|
8449
|
+
}
|
|
8450
|
+
}
|
|
8451
|
+
};
|
|
8452
|
+
var parseMaybeJSON = (value) => {
|
|
8453
|
+
try {
|
|
8454
|
+
return JSON.parse(value);
|
|
8455
|
+
} catch {
|
|
8456
|
+
return;
|
|
8457
|
+
}
|
|
8458
|
+
};
|
|
8459
|
+
var flattenPayload = (value) => {
|
|
8460
|
+
if (!isRecord(value)) {
|
|
8461
|
+
return {};
|
|
8462
|
+
}
|
|
8463
|
+
const data = isRecord(value.data) ? value.data : undefined;
|
|
8464
|
+
const payload = isRecord(value.payload) ? value.payload : undefined;
|
|
8465
|
+
const event = isRecord(value.event) ? value.event : undefined;
|
|
8466
|
+
return {
|
|
8467
|
+
...value,
|
|
8468
|
+
...payload,
|
|
8469
|
+
...event,
|
|
8470
|
+
...data,
|
|
8471
|
+
...isRecord(data?.payload) ? data.payload : undefined
|
|
8472
|
+
};
|
|
8473
|
+
};
|
|
8474
|
+
var toBase64 = (bytes) => Buffer.from(new Uint8Array(bytes)).toString("base64");
|
|
8475
|
+
var timingSafeEqual = (left, right) => {
|
|
8476
|
+
const encoder = new TextEncoder;
|
|
8477
|
+
const leftBytes = encoder.encode(left);
|
|
8478
|
+
const rightBytes = encoder.encode(right);
|
|
8479
|
+
if (leftBytes.length !== rightBytes.length) {
|
|
8480
|
+
return false;
|
|
8481
|
+
}
|
|
8482
|
+
let diff = 0;
|
|
8483
|
+
for (let index = 0;index < leftBytes.length; index += 1) {
|
|
8484
|
+
diff |= leftBytes[index] ^ rightBytes[index];
|
|
8485
|
+
}
|
|
8486
|
+
return diff === 0;
|
|
8487
|
+
};
|
|
8488
|
+
var signHmacSHA1Base64 = async (secret, payload) => {
|
|
8489
|
+
const encoder = new TextEncoder;
|
|
8490
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
8491
|
+
hash: "SHA-1",
|
|
8492
|
+
name: "HMAC"
|
|
8493
|
+
}, false, ["sign"]);
|
|
8494
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
8495
|
+
return toBase64(signature);
|
|
8496
|
+
};
|
|
8497
|
+
var sortedParamsForSignature = (body) => Object.entries(flattenPayload(body)).filter(([, value]) => value !== undefined && value !== null).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}${String(value)}`).join("");
|
|
8498
|
+
var normalizeList = (values, fallback) => new Set((values ?? fallback).map(normalizeToken).filter(Boolean));
|
|
8499
|
+
var metadataValue = (metadata, keys) => {
|
|
8500
|
+
for (const key of keys) {
|
|
8501
|
+
const value = metadata?.[key];
|
|
8502
|
+
if (typeof value === "string" && value.trim()) {
|
|
8503
|
+
return value.trim();
|
|
8504
|
+
}
|
|
8505
|
+
}
|
|
8506
|
+
};
|
|
8507
|
+
var resolveTransferTarget = (event, policy) => {
|
|
8508
|
+
if (typeof event.target === "string" && event.target.trim()) {
|
|
8509
|
+
return event.target.trim();
|
|
8510
|
+
}
|
|
8511
|
+
const metadataTarget = metadataValue(event.metadata, [
|
|
8512
|
+
"transferTarget",
|
|
8513
|
+
"target",
|
|
8514
|
+
"queue",
|
|
8515
|
+
"department"
|
|
8516
|
+
]);
|
|
8517
|
+
if (metadataTarget) {
|
|
8518
|
+
return metadataTarget;
|
|
8519
|
+
}
|
|
8520
|
+
if (typeof policy.transferTarget === "function") {
|
|
8521
|
+
const target = policy.transferTarget(event);
|
|
8522
|
+
return typeof target === "string" && target.trim() ? target.trim() : undefined;
|
|
8523
|
+
}
|
|
8524
|
+
return typeof policy.transferTarget === "string" && policy.transferTarget.trim() ? policy.transferTarget.trim() : undefined;
|
|
8525
|
+
};
|
|
8526
|
+
var mergeMetadata = (event, policy) => ({
|
|
8527
|
+
...policy.includeProviderPayload ? {
|
|
8528
|
+
answeredBy: event.answeredBy,
|
|
8529
|
+
durationMs: event.durationMs,
|
|
8530
|
+
provider: event.provider,
|
|
8531
|
+
reason: event.reason,
|
|
8532
|
+
sipCode: event.sipCode,
|
|
8533
|
+
status: event.status
|
|
8534
|
+
} : undefined,
|
|
8535
|
+
...policy.metadata,
|
|
8536
|
+
...event.metadata
|
|
8537
|
+
});
|
|
8538
|
+
var withDecisionDefaults = (decision, input) => {
|
|
8539
|
+
if (typeof decision === "string") {
|
|
8540
|
+
return buildDecision(decision, input);
|
|
8541
|
+
}
|
|
8542
|
+
return {
|
|
8543
|
+
...buildDecision(decision.action, input),
|
|
8544
|
+
...decision,
|
|
8545
|
+
confidence: decision.confidence ?? "high",
|
|
8546
|
+
metadata: {
|
|
8547
|
+
...mergeMetadata(input.event, input.policy),
|
|
8548
|
+
...decision.metadata
|
|
8549
|
+
},
|
|
8550
|
+
source: decision.source ?? input.source,
|
|
8551
|
+
target: decision.target ?? (decision.action === "transfer" ? resolveTransferTarget(input.event, input.policy) : undefined)
|
|
8552
|
+
};
|
|
8553
|
+
};
|
|
8554
|
+
var dispositionForAction = (action) => {
|
|
8555
|
+
switch (action) {
|
|
8556
|
+
case "complete":
|
|
8557
|
+
return "completed";
|
|
8558
|
+
case "escalate":
|
|
8559
|
+
return "escalated";
|
|
8560
|
+
case "no-answer":
|
|
8561
|
+
return "no-answer";
|
|
8562
|
+
case "transfer":
|
|
8563
|
+
return "transferred";
|
|
8564
|
+
case "voicemail":
|
|
8565
|
+
return "voicemail";
|
|
8566
|
+
default:
|
|
8567
|
+
return;
|
|
8568
|
+
}
|
|
8569
|
+
};
|
|
8570
|
+
var buildDecision = (action, input) => ({
|
|
8571
|
+
action,
|
|
8572
|
+
confidence: action === "ignore" ? "low" : "high",
|
|
8573
|
+
disposition: dispositionForAction(action),
|
|
8574
|
+
metadata: mergeMetadata(input.event, input.policy),
|
|
8575
|
+
reason: input.event.reason,
|
|
8576
|
+
source: input.source,
|
|
8577
|
+
target: action === "transfer" ? resolveTransferTarget(input.event, input.policy) : undefined
|
|
8578
|
+
});
|
|
8579
|
+
var createVoiceTelephonyOutcomePolicy = (policy = {}) => ({
|
|
8580
|
+
completedStatuses: policy.completedStatuses ?? DEFAULT_COMPLETED_STATUSES,
|
|
8581
|
+
escalationStatuses: policy.escalationStatuses ?? DEFAULT_ESCALATION_STATUSES,
|
|
8582
|
+
failedAsNoAnswer: policy.failedAsNoAnswer ?? true,
|
|
8583
|
+
failedStatuses: policy.failedStatuses ?? DEFAULT_FAILED_STATUSES,
|
|
8584
|
+
includeProviderPayload: policy.includeProviderPayload ?? true,
|
|
8585
|
+
machineDetectionVoicemailValues: policy.machineDetectionVoicemailValues ?? DEFAULT_MACHINE_VOICEMAIL_VALUES,
|
|
8586
|
+
metadata: policy.metadata,
|
|
8587
|
+
minAnsweredDurationMs: policy.minAnsweredDurationMs,
|
|
8588
|
+
noAnswerOnZeroDuration: policy.noAnswerOnZeroDuration ?? true,
|
|
8589
|
+
noAnswerSipCodes: policy.noAnswerSipCodes ?? DEFAULT_NO_ANSWER_SIP_CODES,
|
|
8590
|
+
noAnswerStatuses: policy.noAnswerStatuses ?? DEFAULT_NO_ANSWER_STATUSES,
|
|
8591
|
+
statusMap: policy.statusMap,
|
|
8592
|
+
transferStatuses: policy.transferStatuses ?? DEFAULT_TRANSFER_STATUSES,
|
|
8593
|
+
transferTarget: policy.transferTarget,
|
|
8594
|
+
voicemailStatuses: policy.voicemailStatuses ?? DEFAULT_VOICEMAIL_STATUSES
|
|
8595
|
+
});
|
|
8596
|
+
var resolveVoiceTelephonyOutcome = (event, policyInput = {}) => {
|
|
8597
|
+
const policy = createVoiceTelephonyOutcomePolicy(policyInput);
|
|
8598
|
+
const status = normalizeToken(event.status);
|
|
8599
|
+
const provider = normalizeToken(event.provider);
|
|
8600
|
+
const answeredBy = normalizeToken(event.answeredBy);
|
|
8601
|
+
const target = resolveTransferTarget(event, policy);
|
|
8602
|
+
if (status) {
|
|
8603
|
+
const mapped = policy.statusMap?.[status] ?? (provider ? policy.statusMap?.[`${provider}:${status}`] : undefined);
|
|
8604
|
+
if (mapped) {
|
|
8605
|
+
return withDecisionDefaults(mapped, {
|
|
8606
|
+
event,
|
|
8607
|
+
policy,
|
|
8608
|
+
source: "policy"
|
|
8609
|
+
});
|
|
8610
|
+
}
|
|
8611
|
+
}
|
|
8612
|
+
if (answeredBy && normalizeList(policy.machineDetectionVoicemailValues, []).has(answeredBy)) {
|
|
8613
|
+
return buildDecision("voicemail", { event, policy, source: "answered-by" });
|
|
8614
|
+
}
|
|
8615
|
+
if (typeof event.sipCode === "number" && policy.noAnswerSipCodes.includes(event.sipCode)) {
|
|
8616
|
+
return buildDecision("no-answer", { event, policy, source: "sip" });
|
|
8617
|
+
}
|
|
8618
|
+
if (target && status && normalizeList(policy.transferStatuses, []).has(status)) {
|
|
8619
|
+
return buildDecision("transfer", { event, policy, source: "status" });
|
|
8620
|
+
}
|
|
8621
|
+
if (status && normalizeList(policy.voicemailStatuses, []).has(status)) {
|
|
8622
|
+
return buildDecision("voicemail", { event, policy, source: "status" });
|
|
8623
|
+
}
|
|
8624
|
+
if (status && normalizeList(policy.escalationStatuses, []).has(status)) {
|
|
8625
|
+
return buildDecision("escalate", { event, policy, source: "status" });
|
|
8626
|
+
}
|
|
8627
|
+
if (status && (policy.failedAsNoAnswer ? normalizeList(policy.noAnswerStatuses, []).has(status) || normalizeList(policy.failedStatuses, []).has(status) : normalizeList(policy.noAnswerStatuses, []).has(status))) {
|
|
8628
|
+
return buildDecision("no-answer", { event, policy, source: "status" });
|
|
8629
|
+
}
|
|
8630
|
+
if (policy.noAnswerOnZeroDuration && typeof event.durationMs === "number" && event.durationMs <= 0) {
|
|
8631
|
+
return buildDecision("no-answer", { event, policy, source: "duration" });
|
|
8632
|
+
}
|
|
8633
|
+
if (typeof policy.minAnsweredDurationMs === "number" && typeof event.durationMs === "number" && event.durationMs < policy.minAnsweredDurationMs) {
|
|
8634
|
+
return {
|
|
8635
|
+
...buildDecision("no-answer", { event, policy, source: "duration" }),
|
|
8636
|
+
confidence: "medium"
|
|
8637
|
+
};
|
|
8638
|
+
}
|
|
8639
|
+
if (status && normalizeList(policy.completedStatuses, []).has(status)) {
|
|
8640
|
+
return buildDecision("complete", { event, policy, source: "status" });
|
|
8641
|
+
}
|
|
8642
|
+
if (target) {
|
|
8643
|
+
return {
|
|
8644
|
+
...buildDecision("transfer", { event, policy, source: "explicit-target" }),
|
|
8645
|
+
confidence: "medium"
|
|
8646
|
+
};
|
|
8647
|
+
}
|
|
8648
|
+
return buildDecision("ignore", { event, policy, source: "status" });
|
|
8649
|
+
};
|
|
8650
|
+
var voiceTelephonyOutcomeToRouteResult = (decision, result) => {
|
|
8651
|
+
switch (decision.action) {
|
|
8652
|
+
case "complete":
|
|
8653
|
+
return { complete: true, result };
|
|
8654
|
+
case "escalate":
|
|
8655
|
+
return {
|
|
8656
|
+
escalate: {
|
|
8657
|
+
metadata: decision.metadata,
|
|
8658
|
+
reason: decision.reason ?? "telephony-escalation"
|
|
8659
|
+
},
|
|
8660
|
+
result
|
|
8661
|
+
};
|
|
8662
|
+
case "no-answer":
|
|
8663
|
+
return {
|
|
8664
|
+
noAnswer: {
|
|
8665
|
+
metadata: decision.metadata
|
|
8666
|
+
},
|
|
8667
|
+
result
|
|
8668
|
+
};
|
|
8669
|
+
case "transfer":
|
|
8670
|
+
if (!decision.target) {
|
|
8671
|
+
return { result };
|
|
8672
|
+
}
|
|
8673
|
+
return {
|
|
8674
|
+
result,
|
|
8675
|
+
transfer: {
|
|
8676
|
+
metadata: decision.metadata,
|
|
8677
|
+
reason: decision.reason,
|
|
8678
|
+
target: decision.target
|
|
8679
|
+
}
|
|
8680
|
+
};
|
|
8681
|
+
case "voicemail":
|
|
8682
|
+
return {
|
|
8683
|
+
result,
|
|
8684
|
+
voicemail: {
|
|
8685
|
+
metadata: decision.metadata
|
|
8686
|
+
}
|
|
8687
|
+
};
|
|
8688
|
+
default:
|
|
8689
|
+
return { result };
|
|
8690
|
+
}
|
|
8691
|
+
};
|
|
8692
|
+
var applyVoiceTelephonyOutcome = async (api, decision, result) => {
|
|
8693
|
+
switch (decision.action) {
|
|
8694
|
+
case "complete":
|
|
8695
|
+
await api.complete(result);
|
|
8696
|
+
break;
|
|
8697
|
+
case "escalate":
|
|
8698
|
+
await api.escalate({
|
|
8699
|
+
metadata: decision.metadata,
|
|
8700
|
+
reason: decision.reason ?? "telephony-escalation",
|
|
8701
|
+
result
|
|
8702
|
+
});
|
|
8703
|
+
break;
|
|
8704
|
+
case "no-answer":
|
|
8705
|
+
await api.markNoAnswer({
|
|
8706
|
+
metadata: decision.metadata,
|
|
8707
|
+
result
|
|
8708
|
+
});
|
|
8709
|
+
break;
|
|
8710
|
+
case "transfer":
|
|
8711
|
+
if (!decision.target) {
|
|
8712
|
+
return;
|
|
8713
|
+
}
|
|
8714
|
+
await api.transfer({
|
|
8715
|
+
metadata: decision.metadata,
|
|
8716
|
+
reason: decision.reason,
|
|
8717
|
+
result,
|
|
8718
|
+
target: decision.target
|
|
8719
|
+
});
|
|
8720
|
+
break;
|
|
8721
|
+
case "voicemail":
|
|
8722
|
+
await api.markVoicemail({
|
|
8723
|
+
metadata: decision.metadata,
|
|
8724
|
+
result
|
|
8725
|
+
});
|
|
8726
|
+
break;
|
|
8727
|
+
default:
|
|
8728
|
+
break;
|
|
8729
|
+
}
|
|
8730
|
+
};
|
|
8731
|
+
var parseRequestBodyText = (input) => {
|
|
8732
|
+
const { contentType, text } = input;
|
|
8733
|
+
if (!text) {
|
|
8734
|
+
return {};
|
|
8735
|
+
}
|
|
8736
|
+
if (contentType.includes("application/json")) {
|
|
8737
|
+
return parseMaybeJSON(text) ?? {};
|
|
8738
|
+
}
|
|
8739
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
8740
|
+
return Object.fromEntries(new URLSearchParams(text));
|
|
8741
|
+
}
|
|
8742
|
+
return parseMaybeJSON(text) ?? Object.fromEntries(new URLSearchParams(text));
|
|
8743
|
+
};
|
|
8744
|
+
var readRequestBody = async (request) => {
|
|
8745
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
8746
|
+
const text = await request.text();
|
|
8747
|
+
return {
|
|
8748
|
+
body: parseRequestBodyText({ contentType, text }),
|
|
8749
|
+
rawBody: text
|
|
8750
|
+
};
|
|
8751
|
+
};
|
|
8752
|
+
var signVoiceTwilioWebhook = async (input) => signHmacSHA1Base64(input.authToken, `${input.url}${sortedParamsForSignature(input.body ?? {})}`);
|
|
8753
|
+
var verifyVoiceTwilioWebhookSignature = async (input) => {
|
|
8754
|
+
if (!input.authToken) {
|
|
8755
|
+
return { ok: false, reason: "missing-secret" };
|
|
8756
|
+
}
|
|
8757
|
+
const signature = input.headers.get("x-twilio-signature");
|
|
8758
|
+
if (!signature) {
|
|
8759
|
+
return { ok: false, reason: "missing-signature" };
|
|
8760
|
+
}
|
|
8761
|
+
const expected = await signVoiceTwilioWebhook({
|
|
8762
|
+
authToken: input.authToken,
|
|
8763
|
+
body: input.body,
|
|
8764
|
+
url: input.url
|
|
8765
|
+
});
|
|
8766
|
+
return timingSafeEqual(signature, expected) ? { ok: true } : { ok: false, reason: "invalid-signature" };
|
|
8767
|
+
};
|
|
8768
|
+
var resolveVerificationUrl = (option, input) => typeof option === "function" ? option(input) : option ?? input.request.url;
|
|
8769
|
+
var verifyVoiceTelephonyWebhook = async (input) => {
|
|
8770
|
+
if (input.options.verify) {
|
|
8771
|
+
return input.options.verify({
|
|
8772
|
+
body: input.body,
|
|
8773
|
+
headers: input.request.headers,
|
|
8774
|
+
provider: input.provider,
|
|
8775
|
+
query: input.query,
|
|
8776
|
+
rawBody: input.rawBody,
|
|
8777
|
+
request: input.request
|
|
8778
|
+
});
|
|
8779
|
+
}
|
|
8780
|
+
if (!input.options.signingSecret) {
|
|
8781
|
+
return input.options.requireVerification ? { ok: false, reason: "missing-secret" } : { ok: true };
|
|
8782
|
+
}
|
|
8783
|
+
if (input.provider !== "twilio") {
|
|
8784
|
+
return { ok: false, reason: "unsupported-provider" };
|
|
8785
|
+
}
|
|
8786
|
+
return verifyVoiceTwilioWebhookSignature({
|
|
8787
|
+
authToken: input.options.signingSecret,
|
|
8788
|
+
body: input.body,
|
|
8789
|
+
headers: input.request.headers,
|
|
8790
|
+
url: resolveVerificationUrl(input.options.verificationUrl, {
|
|
8791
|
+
query: input.query,
|
|
8792
|
+
request: input.request
|
|
8793
|
+
})
|
|
8794
|
+
});
|
|
8795
|
+
};
|
|
8796
|
+
var durationMsFromSeconds = (value) => typeof value === "number" ? value * 1000 : undefined;
|
|
8797
|
+
var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
8798
|
+
const payload = flattenPayload(input.body);
|
|
8799
|
+
const provider = firstString(payload, ["provider", "Provider"]) ?? input.provider;
|
|
8800
|
+
const status = firstString(payload, [
|
|
8801
|
+
"CallStatus",
|
|
8802
|
+
"call_status",
|
|
8803
|
+
"callStatus",
|
|
8804
|
+
"DialCallStatus",
|
|
8805
|
+
"dial_call_status",
|
|
8806
|
+
"status",
|
|
8807
|
+
"event_type",
|
|
8808
|
+
"type"
|
|
8809
|
+
]);
|
|
8810
|
+
const durationMs = firstNumber(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber(payload, [
|
|
8811
|
+
"CallDuration",
|
|
8812
|
+
"call_duration",
|
|
8813
|
+
"callDuration",
|
|
8814
|
+
"DialCallDuration",
|
|
8815
|
+
"dial_call_duration",
|
|
8816
|
+
"duration"
|
|
8817
|
+
]));
|
|
8818
|
+
const sipCode = firstNumber(payload, [
|
|
8819
|
+
"SipResponseCode",
|
|
8820
|
+
"sip_response_code",
|
|
8821
|
+
"sipCode",
|
|
8822
|
+
"sip_code",
|
|
8823
|
+
"hangupCauseCode"
|
|
8824
|
+
]);
|
|
8825
|
+
const from = firstString(payload, ["From", "from", "caller_id", "callerId"]);
|
|
8826
|
+
const to = firstString(payload, ["To", "to", "called_number", "calledNumber"]);
|
|
8827
|
+
const target = firstString(payload, [
|
|
8828
|
+
"transferTarget",
|
|
8829
|
+
"TransferTarget",
|
|
8830
|
+
"target",
|
|
8831
|
+
"queue",
|
|
8832
|
+
"department"
|
|
8833
|
+
]);
|
|
8834
|
+
return {
|
|
8835
|
+
answeredBy: firstString(payload, [
|
|
8836
|
+
"AnsweredBy",
|
|
8837
|
+
"answered_by",
|
|
8838
|
+
"answeredBy",
|
|
8839
|
+
"machineDetection",
|
|
8840
|
+
"machine_detection"
|
|
8841
|
+
]),
|
|
8842
|
+
durationMs,
|
|
8843
|
+
from,
|
|
8844
|
+
metadata: {
|
|
8845
|
+
...input.query,
|
|
8846
|
+
...payload
|
|
8847
|
+
},
|
|
8848
|
+
provider,
|
|
8849
|
+
reason: firstString(payload, [
|
|
8850
|
+
"Reason",
|
|
8851
|
+
"reason",
|
|
8852
|
+
"HangupCause",
|
|
8853
|
+
"hangup_cause",
|
|
8854
|
+
"hangupCause"
|
|
8855
|
+
]),
|
|
8856
|
+
sipCode,
|
|
8857
|
+
status,
|
|
8858
|
+
target,
|
|
8859
|
+
to
|
|
8860
|
+
};
|
|
8861
|
+
};
|
|
8862
|
+
var defaultSessionId = (input) => {
|
|
8863
|
+
const payload = flattenPayload(input.body);
|
|
8864
|
+
const metadataSessionId = input.event.metadata?.sessionId;
|
|
8865
|
+
return firstString(input.query, ["sessionId", "session_id"]) ?? firstString(payload, [
|
|
8866
|
+
"sessionId",
|
|
8867
|
+
"session_id",
|
|
8868
|
+
"SessionId",
|
|
8869
|
+
"CallSid",
|
|
8870
|
+
"call_sid",
|
|
8871
|
+
"callSid",
|
|
8872
|
+
"CallUUID",
|
|
8873
|
+
"call_uuid",
|
|
8874
|
+
"callControlId",
|
|
8875
|
+
"call_control_id"
|
|
8876
|
+
]) ?? (typeof metadataSessionId === "string" ? metadataSessionId : undefined);
|
|
8877
|
+
};
|
|
8878
|
+
var defaultIdempotencyKey = (input) => {
|
|
8879
|
+
const payload = flattenPayload(input.body);
|
|
8880
|
+
const eventId = firstString(payload, [
|
|
8881
|
+
"id",
|
|
8882
|
+
"event_id",
|
|
8883
|
+
"eventId",
|
|
8884
|
+
"EventSid",
|
|
8885
|
+
"event_sid",
|
|
8886
|
+
"MessageSid",
|
|
8887
|
+
"message_sid",
|
|
8888
|
+
"CallSid",
|
|
8889
|
+
"call_sid",
|
|
8890
|
+
"CallUUID",
|
|
8891
|
+
"call_uuid",
|
|
8892
|
+
"callControlId",
|
|
8893
|
+
"call_control_id"
|
|
8894
|
+
]);
|
|
8895
|
+
const status = normalizeToken(input.event.status) ?? "unknown";
|
|
8896
|
+
if (eventId) {
|
|
8897
|
+
return `${input.provider}:${eventId}:${status}`;
|
|
8898
|
+
}
|
|
8899
|
+
if (input.sessionId) {
|
|
8900
|
+
return `${input.provider}:${input.sessionId}:${status}`;
|
|
8901
|
+
}
|
|
8902
|
+
};
|
|
8903
|
+
var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
|
|
8904
|
+
const provider = options.provider ?? "generic";
|
|
8905
|
+
const query = input.query ?? {};
|
|
8906
|
+
const { body, rawBody } = await readRequestBody(input.request);
|
|
8907
|
+
const verification = await verifyVoiceTelephonyWebhook({
|
|
8908
|
+
body,
|
|
8909
|
+
options,
|
|
8910
|
+
provider,
|
|
8911
|
+
query,
|
|
8912
|
+
rawBody,
|
|
8913
|
+
request: input.request
|
|
8914
|
+
});
|
|
8915
|
+
if (!verification.ok) {
|
|
8916
|
+
throw new VoiceTelephonyWebhookVerificationError(verification);
|
|
8917
|
+
}
|
|
8918
|
+
const event = options.parse ? await options.parse({
|
|
8919
|
+
body,
|
|
8920
|
+
headers: input.request.headers,
|
|
8921
|
+
provider,
|
|
8922
|
+
query,
|
|
8923
|
+
request: input.request
|
|
8924
|
+
}) : parseVoiceTelephonyWebhookEvent({
|
|
8925
|
+
body,
|
|
8926
|
+
headers: input.request.headers,
|
|
8927
|
+
provider,
|
|
8928
|
+
query,
|
|
8929
|
+
request: input.request
|
|
8930
|
+
});
|
|
8931
|
+
const sessionId = await (options.resolveSessionId?.({
|
|
8932
|
+
body,
|
|
8933
|
+
event,
|
|
8934
|
+
query,
|
|
8935
|
+
request: input.request
|
|
8936
|
+
}) ?? defaultSessionId({ body, event, query }));
|
|
8937
|
+
const idempotencyEnabled = options.idempotency?.enabled !== false;
|
|
8938
|
+
const idempotencyKey = idempotencyEnabled ? await (options.idempotency?.key?.({
|
|
8939
|
+
body,
|
|
8940
|
+
event,
|
|
8941
|
+
provider,
|
|
8942
|
+
query,
|
|
8943
|
+
request: input.request,
|
|
8944
|
+
sessionId
|
|
8945
|
+
}) ?? defaultIdempotencyKey({ body, event, provider, sessionId })) : undefined;
|
|
8946
|
+
const idempotencyStore = options.idempotency?.store;
|
|
8947
|
+
if (idempotencyKey && idempotencyStore) {
|
|
8948
|
+
const existing = await idempotencyStore.get(idempotencyKey);
|
|
8949
|
+
if (existing) {
|
|
8950
|
+
const duplicateDecision = {
|
|
8951
|
+
...existing,
|
|
8952
|
+
duplicate: true
|
|
8953
|
+
};
|
|
8954
|
+
await options.onDecision?.({
|
|
8955
|
+
...duplicateDecision,
|
|
8956
|
+
context: options.context,
|
|
8957
|
+
request: input.request
|
|
8958
|
+
});
|
|
8959
|
+
return duplicateDecision;
|
|
8960
|
+
}
|
|
8961
|
+
}
|
|
8962
|
+
const decision = resolveVoiceTelephonyOutcome(event, options.policy);
|
|
8963
|
+
const resultResolver = options.result;
|
|
8964
|
+
const result = typeof resultResolver === "function" ? await resultResolver({
|
|
8965
|
+
decision,
|
|
8966
|
+
event,
|
|
8967
|
+
sessionId
|
|
8968
|
+
}) : resultResolver;
|
|
8969
|
+
const routeResult = voiceTelephonyOutcomeToRouteResult(decision, result);
|
|
8970
|
+
const shouldApply = typeof options.apply === "function" ? options.apply({
|
|
8971
|
+
applied: false,
|
|
8972
|
+
decision,
|
|
8973
|
+
event,
|
|
8974
|
+
routeResult,
|
|
8975
|
+
sessionId
|
|
8976
|
+
}) : options.apply === true;
|
|
8977
|
+
let applied = false;
|
|
8978
|
+
if (shouldApply && decision.action !== "ignore" && options.getSessionHandle) {
|
|
8979
|
+
const api = await options.getSessionHandle({
|
|
8980
|
+
context: options.context,
|
|
8981
|
+
decision,
|
|
8982
|
+
event,
|
|
8983
|
+
request: input.request,
|
|
8984
|
+
sessionId
|
|
8985
|
+
});
|
|
8986
|
+
if (api) {
|
|
8987
|
+
await applyVoiceTelephonyOutcome(api, decision, result);
|
|
8988
|
+
applied = true;
|
|
8989
|
+
}
|
|
8990
|
+
}
|
|
8991
|
+
const webhookDecision = {
|
|
8992
|
+
applied,
|
|
8993
|
+
decision,
|
|
8994
|
+
event,
|
|
8995
|
+
idempotencyKey,
|
|
8996
|
+
routeResult,
|
|
8997
|
+
sessionId
|
|
8998
|
+
};
|
|
8999
|
+
if (idempotencyKey && idempotencyStore) {
|
|
9000
|
+
const now = Date.now();
|
|
9001
|
+
await idempotencyStore.set(idempotencyKey, {
|
|
9002
|
+
...webhookDecision,
|
|
9003
|
+
createdAt: now,
|
|
9004
|
+
updatedAt: now
|
|
9005
|
+
});
|
|
9006
|
+
}
|
|
9007
|
+
await options.onDecision?.({
|
|
9008
|
+
...webhookDecision,
|
|
9009
|
+
context: options.context,
|
|
9010
|
+
request: input.request
|
|
9011
|
+
});
|
|
9012
|
+
return webhookDecision;
|
|
9013
|
+
};
|
|
9014
|
+
var createVoiceTelephonyWebhookRoutes = (options = {}) => {
|
|
9015
|
+
const path = options.path ?? "/api/voice/telephony/webhook";
|
|
9016
|
+
const handler = createVoiceTelephonyWebhookHandler(options);
|
|
9017
|
+
return new Elysia({
|
|
9018
|
+
name: options.name ?? "absolutejs-voice-telephony-webhooks"
|
|
9019
|
+
}).post(path, async ({ query, request }) => {
|
|
9020
|
+
try {
|
|
9021
|
+
return await handler({ query, request });
|
|
9022
|
+
} catch (error) {
|
|
9023
|
+
if (error instanceof VoiceTelephonyWebhookVerificationError) {
|
|
9024
|
+
return new Response(JSON.stringify({ verification: error.result }), {
|
|
9025
|
+
headers: {
|
|
9026
|
+
"content-type": "application/json"
|
|
9027
|
+
},
|
|
9028
|
+
status: 401
|
|
9029
|
+
});
|
|
9030
|
+
}
|
|
9031
|
+
throw error;
|
|
9032
|
+
}
|
|
9033
|
+
}, {
|
|
9034
|
+
parse: "none"
|
|
9035
|
+
});
|
|
9036
|
+
};
|
|
9037
|
+
|
|
9038
|
+
// src/telephony/twilio.ts
|
|
7644
9039
|
var TWILIO_MULAW_SAMPLE_RATE = 8000;
|
|
7645
9040
|
var VOICE_PCM_SAMPLE_RATE = 16000;
|
|
7646
9041
|
var escapeXml2 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
9042
|
+
var resolveRequestOrigin = (request) => {
|
|
9043
|
+
const url = new URL(request.url);
|
|
9044
|
+
const forwardedHost = request.headers.get("x-forwarded-host");
|
|
9045
|
+
const forwardedProto = request.headers.get("x-forwarded-proto");
|
|
9046
|
+
const host = forwardedHost ?? request.headers.get("host") ?? url.host;
|
|
9047
|
+
const protocol = forwardedProto ?? url.protocol.replace(":", "");
|
|
9048
|
+
return `${protocol}://${host}`;
|
|
9049
|
+
};
|
|
9050
|
+
var resolveTwilioStreamUrl = async (options, input) => {
|
|
9051
|
+
if (typeof options.twiml?.streamUrl === "function") {
|
|
9052
|
+
return options.twiml.streamUrl(input);
|
|
9053
|
+
}
|
|
9054
|
+
if (typeof options.twiml?.streamUrl === "string") {
|
|
9055
|
+
return options.twiml.streamUrl;
|
|
9056
|
+
}
|
|
9057
|
+
const origin = resolveRequestOrigin(input.request);
|
|
9058
|
+
const wsOrigin = origin.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
|
|
9059
|
+
return `${wsOrigin}${input.streamPath}`;
|
|
9060
|
+
};
|
|
9061
|
+
var resolveTwilioStreamParameters = async (parameters, input) => {
|
|
9062
|
+
if (typeof parameters === "function") {
|
|
9063
|
+
return parameters(input);
|
|
9064
|
+
}
|
|
9065
|
+
return parameters;
|
|
9066
|
+
};
|
|
9067
|
+
var joinUrlPath = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
9068
|
+
var escapeHtml2 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
9069
|
+
var getWebhookVerificationUrl = (webhook, input) => {
|
|
9070
|
+
if (!webhook?.verificationUrl) {
|
|
9071
|
+
return;
|
|
9072
|
+
}
|
|
9073
|
+
if (typeof webhook.verificationUrl === "function") {
|
|
9074
|
+
return webhook.verificationUrl(input);
|
|
9075
|
+
}
|
|
9076
|
+
return webhook.verificationUrl;
|
|
9077
|
+
};
|
|
9078
|
+
var buildTwilioVoiceSetupStatus = async (options, input) => {
|
|
9079
|
+
const origin = resolveRequestOrigin(input.request);
|
|
9080
|
+
const stream = await resolveTwilioStreamUrl(options, input);
|
|
9081
|
+
const twiml = joinUrlPath(origin, input.twimlPath);
|
|
9082
|
+
const webhook = joinUrlPath(origin, input.webhookPath);
|
|
9083
|
+
const verificationUrl = getWebhookVerificationUrl(options.webhook, input);
|
|
9084
|
+
const missing = Object.entries(options.setup?.requiredEnv ?? {}).filter((entry) => !entry[1]).map(([name]) => name);
|
|
9085
|
+
const signingConfigured = Boolean(options.webhook?.signingSecret || options.webhook?.verify);
|
|
9086
|
+
const warnings = [
|
|
9087
|
+
...stream.startsWith("wss://") ? [] : ["Twilio media streams should use wss:// in production."],
|
|
9088
|
+
...signingConfigured ? [] : ["Webhook signature verification is not configured."],
|
|
9089
|
+
...verificationUrl || !signingConfigured ? [] : ["Webhook signing is configured without an explicit verification URL."]
|
|
9090
|
+
];
|
|
9091
|
+
return {
|
|
9092
|
+
generatedAt: Date.now(),
|
|
9093
|
+
missing,
|
|
9094
|
+
provider: "twilio",
|
|
9095
|
+
ready: missing.length === 0 && signingConfigured && warnings.length === 0,
|
|
9096
|
+
signing: {
|
|
9097
|
+
configured: signingConfigured,
|
|
9098
|
+
mode: options.webhook?.verify ? "custom" : options.webhook?.signingSecret ? "twilio-signature" : "none",
|
|
9099
|
+
verificationUrl
|
|
9100
|
+
},
|
|
9101
|
+
urls: {
|
|
9102
|
+
stream,
|
|
9103
|
+
twiml,
|
|
9104
|
+
webhook
|
|
9105
|
+
},
|
|
9106
|
+
warnings
|
|
9107
|
+
};
|
|
9108
|
+
};
|
|
9109
|
+
var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
9110
|
+
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
|
|
9111
|
+
<h1>${escapeHtml2(title)}</h1>
|
|
9112
|
+
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
9113
|
+
<section>
|
|
9114
|
+
<h2>URLs</h2>
|
|
9115
|
+
<ul>
|
|
9116
|
+
<li><strong>TwiML:</strong> <code>${escapeHtml2(status.urls.twiml)}</code></li>
|
|
9117
|
+
<li><strong>Media stream:</strong> <code>${escapeHtml2(status.urls.stream)}</code></li>
|
|
9118
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml2(status.urls.webhook)}</code></li>
|
|
9119
|
+
</ul>
|
|
9120
|
+
</section>
|
|
9121
|
+
<section>
|
|
9122
|
+
<h2>Signing</h2>
|
|
9123
|
+
<p>Mode: <code>${status.signing.mode}</code></p>
|
|
9124
|
+
${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml2(status.signing.verificationUrl)}</code></p>` : ""}
|
|
9125
|
+
</section>
|
|
9126
|
+
${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml2(name)}</code></li>`).join("")}</ul></section>` : ""}
|
|
9127
|
+
${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml2(warning)}</li>`).join("")}</ul></section>` : ""}
|
|
9128
|
+
</main>`;
|
|
9129
|
+
var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&", "&");
|
|
9130
|
+
var createSmokeCheck = (name, status, message, details) => ({
|
|
9131
|
+
details,
|
|
9132
|
+
message,
|
|
9133
|
+
name,
|
|
9134
|
+
status
|
|
9135
|
+
});
|
|
9136
|
+
var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
9137
|
+
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
|
|
9138
|
+
<h1>${escapeHtml2(title)}</h1>
|
|
9139
|
+
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
9140
|
+
<section>
|
|
9141
|
+
<h2>Checks</h2>
|
|
9142
|
+
<ul>
|
|
9143
|
+
${report.checks.map((check) => `<li><strong>${escapeHtml2(check.name)}</strong>: ${escapeHtml2(check.status)}${check.message ? ` - ${escapeHtml2(check.message)}` : ""}</li>`).join("")}
|
|
9144
|
+
</ul>
|
|
9145
|
+
</section>
|
|
9146
|
+
<section>
|
|
9147
|
+
<h2>Observed URLs</h2>
|
|
9148
|
+
<ul>
|
|
9149
|
+
<li><strong>TwiML:</strong> <code>${escapeHtml2(report.setup.urls.twiml)}</code></li>
|
|
9150
|
+
<li><strong>Stream:</strong> <code>${escapeHtml2(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
|
|
9151
|
+
<li><strong>Webhook:</strong> <code>${escapeHtml2(report.setup.urls.webhook)}</code></li>
|
|
9152
|
+
</ul>
|
|
9153
|
+
</section>
|
|
9154
|
+
</main>`;
|
|
9155
|
+
var runTwilioVoiceSmokeTest = async (input) => {
|
|
9156
|
+
const setup = await buildTwilioVoiceSetupStatus(input.options, input);
|
|
9157
|
+
const checks = [];
|
|
9158
|
+
const twimlUrl = new URL(setup.urls.twiml);
|
|
9159
|
+
twimlUrl.searchParams.set("scenarioId", input.options.smoke?.scenarioId ?? "smoke");
|
|
9160
|
+
twimlUrl.searchParams.set("sessionId", input.options.smoke?.sessionId ?? "smoke-session");
|
|
9161
|
+
const twimlResponse = await input.app.handle(new Request(twimlUrl, {
|
|
9162
|
+
headers: input.request.headers
|
|
9163
|
+
}));
|
|
9164
|
+
const twiml = await twimlResponse.text();
|
|
9165
|
+
const streamUrl = extractTwilioStreamUrl(twiml);
|
|
9166
|
+
checks.push(createSmokeCheck("twiml", twimlResponse.ok && Boolean(streamUrl) ? "pass" : "fail", streamUrl ? "TwiML includes a media stream URL." : 'TwiML is missing <Stream url="...">.', {
|
|
9167
|
+
status: twimlResponse.status,
|
|
9168
|
+
streamUrl
|
|
9169
|
+
}));
|
|
9170
|
+
checks.push(createSmokeCheck("stream-url", streamUrl?.startsWith("wss://") ? "pass" : "fail", streamUrl?.startsWith("wss://") ? "Media stream URL uses wss://." : "Media stream URL should use wss:// for Twilio.", {
|
|
9171
|
+
streamUrl
|
|
9172
|
+
}));
|
|
9173
|
+
const webhookBody = {
|
|
9174
|
+
CallSid: input.options.smoke?.callSid ?? "CA_SMOKE_TEST",
|
|
9175
|
+
CallStatus: input.options.smoke?.status ?? "busy",
|
|
9176
|
+
SipResponseCode: String(input.options.smoke?.sipCode ?? 486)
|
|
9177
|
+
};
|
|
9178
|
+
const webhookHeaders = new Headers({
|
|
9179
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
9180
|
+
});
|
|
9181
|
+
const verificationUrl = setup.signing.verificationUrl ?? setup.urls.webhook;
|
|
9182
|
+
if (input.options.webhook?.signingSecret) {
|
|
9183
|
+
webhookHeaders.set("x-twilio-signature", await signVoiceTwilioWebhook({
|
|
9184
|
+
authToken: input.options.webhook.signingSecret,
|
|
9185
|
+
body: webhookBody,
|
|
9186
|
+
url: verificationUrl
|
|
9187
|
+
}));
|
|
9188
|
+
}
|
|
9189
|
+
const webhookResponse = await input.app.handle(new Request(setup.urls.webhook, {
|
|
9190
|
+
body: new URLSearchParams(webhookBody),
|
|
9191
|
+
headers: webhookHeaders,
|
|
9192
|
+
method: "POST"
|
|
9193
|
+
}));
|
|
9194
|
+
const webhookText = await webhookResponse.text();
|
|
9195
|
+
const webhookPayload = (() => {
|
|
9196
|
+
try {
|
|
9197
|
+
return JSON.parse(webhookText);
|
|
9198
|
+
} catch {
|
|
9199
|
+
return webhookText;
|
|
9200
|
+
}
|
|
9201
|
+
})();
|
|
9202
|
+
checks.push(createSmokeCheck("webhook", webhookResponse.ok ? "pass" : "fail", webhookResponse.ok ? "Synthetic Twilio status callback was accepted." : "Synthetic Twilio status callback failed.", {
|
|
9203
|
+
status: webhookResponse.status
|
|
9204
|
+
}));
|
|
9205
|
+
for (const warning of setup.warnings) {
|
|
9206
|
+
checks.push(createSmokeCheck("setup-warning", "warn", warning));
|
|
9207
|
+
}
|
|
9208
|
+
for (const name of setup.missing) {
|
|
9209
|
+
checks.push(createSmokeCheck("missing-env", "fail", `${name} is missing.`));
|
|
9210
|
+
}
|
|
9211
|
+
return {
|
|
9212
|
+
checks,
|
|
9213
|
+
generatedAt: Date.now(),
|
|
9214
|
+
pass: checks.every((check) => check.status !== "fail"),
|
|
9215
|
+
provider: "twilio",
|
|
9216
|
+
setup,
|
|
9217
|
+
twiml: {
|
|
9218
|
+
status: twimlResponse.status,
|
|
9219
|
+
streamUrl
|
|
9220
|
+
},
|
|
9221
|
+
webhook: {
|
|
9222
|
+
body: webhookPayload,
|
|
9223
|
+
status: webhookResponse.status
|
|
9224
|
+
}
|
|
9225
|
+
};
|
|
9226
|
+
};
|
|
7647
9227
|
var normalizeOnTurn = (handler) => {
|
|
7648
9228
|
if (handler.length > 1) {
|
|
7649
9229
|
const directHandler = handler;
|
|
@@ -7745,7 +9325,7 @@ var bytesToInt16Array = (bytes) => {
|
|
|
7745
9325
|
return output;
|
|
7746
9326
|
};
|
|
7747
9327
|
var decodeTwilioMulawBase64 = (payload) => {
|
|
7748
|
-
const bytes = Uint8Array.from(
|
|
9328
|
+
const bytes = Uint8Array.from(Buffer3.from(payload, "base64"));
|
|
7749
9329
|
const samples = new Int16Array(bytes.length);
|
|
7750
9330
|
for (let index = 0;index < bytes.length; index += 1) {
|
|
7751
9331
|
samples[index] = decodeMulawSample(bytes[index] ?? 0);
|
|
@@ -7757,7 +9337,7 @@ var encodeTwilioMulawBase64 = (samples) => {
|
|
|
7757
9337
|
for (let index = 0;index < samples.length; index += 1) {
|
|
7758
9338
|
bytes[index] = encodeMulawSample(samples[index] ?? 0);
|
|
7759
9339
|
}
|
|
7760
|
-
return
|
|
9340
|
+
return Buffer3.from(bytes).toString("base64");
|
|
7761
9341
|
};
|
|
7762
9342
|
var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
|
|
7763
9343
|
const narrowband = decodeTwilioMulawBase64(payload);
|
|
@@ -7766,7 +9346,7 @@ var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
|
|
|
7766
9346
|
};
|
|
7767
9347
|
var transcodePCMToTwilioOutboundPayload = (chunk, format) => {
|
|
7768
9348
|
if (format.container === "raw" && format.encoding === "mulaw" && format.channels === 1 && format.sampleRateHz === TWILIO_MULAW_SAMPLE_RATE) {
|
|
7769
|
-
return
|
|
9349
|
+
return Buffer3.from(chunk).toString("base64");
|
|
7770
9350
|
}
|
|
7771
9351
|
if (format.encoding !== "pcm_s16le") {
|
|
7772
9352
|
throw new Error(`Unsupported outbound telephony audio format: ${format.container}/${format.encoding}`);
|
|
@@ -7807,7 +9387,7 @@ var createTwilioSocketAdapter = (socket, getState) => ({
|
|
|
7807
9387
|
return;
|
|
7808
9388
|
}
|
|
7809
9389
|
if (message.type === "audio") {
|
|
7810
|
-
const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(
|
|
9390
|
+
const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer3.from(message.chunkBase64, "base64")), message.format);
|
|
7811
9391
|
state.hasOutboundAudioSinceLastInbound = true;
|
|
7812
9392
|
state.reviewRecorder?.recordTwilioOutbound({
|
|
7813
9393
|
bytes: payload.length,
|
|
@@ -8020,6 +9600,148 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
8020
9600
|
}
|
|
8021
9601
|
};
|
|
8022
9602
|
};
|
|
9603
|
+
var createTwilioVoiceRoutes = (options) => {
|
|
9604
|
+
const streamPath = options.streamPath ?? "/api/voice/twilio/stream";
|
|
9605
|
+
const twimlPath = options.twiml?.path ?? "/api/voice/twilio";
|
|
9606
|
+
const webhookPath = options.webhook?.path ?? "/api/voice/twilio/webhook";
|
|
9607
|
+
const setupPath = options.setup?.path === false ? false : options.setup?.path ?? "/api/voice/twilio/setup";
|
|
9608
|
+
const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
|
|
9609
|
+
const bridges = new WeakMap;
|
|
9610
|
+
const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
|
|
9611
|
+
const app = new Elysia2({
|
|
9612
|
+
name: options.name ?? "absolutejs-voice-twilio"
|
|
9613
|
+
}).get(twimlPath, async ({ query, request }) => {
|
|
9614
|
+
const streamUrl = await resolveTwilioStreamUrl(options, {
|
|
9615
|
+
query,
|
|
9616
|
+
request,
|
|
9617
|
+
streamPath
|
|
9618
|
+
});
|
|
9619
|
+
const parameters = await resolveTwilioStreamParameters(options.twiml?.parameters, {
|
|
9620
|
+
query,
|
|
9621
|
+
request
|
|
9622
|
+
});
|
|
9623
|
+
return new Response(createTwilioVoiceResponse({
|
|
9624
|
+
parameters,
|
|
9625
|
+
streamName: options.twiml?.streamName,
|
|
9626
|
+
streamUrl,
|
|
9627
|
+
track: options.twiml?.track
|
|
9628
|
+
}), {
|
|
9629
|
+
headers: {
|
|
9630
|
+
"content-type": "text/xml; charset=utf-8"
|
|
9631
|
+
}
|
|
9632
|
+
});
|
|
9633
|
+
}).post(twimlPath, async ({ query, request }) => {
|
|
9634
|
+
const streamUrl = await resolveTwilioStreamUrl(options, {
|
|
9635
|
+
query,
|
|
9636
|
+
request,
|
|
9637
|
+
streamPath
|
|
9638
|
+
});
|
|
9639
|
+
const parameters = await resolveTwilioStreamParameters(options.twiml?.parameters, {
|
|
9640
|
+
query,
|
|
9641
|
+
request
|
|
9642
|
+
});
|
|
9643
|
+
return new Response(createTwilioVoiceResponse({
|
|
9644
|
+
parameters,
|
|
9645
|
+
streamName: options.twiml?.streamName,
|
|
9646
|
+
streamUrl,
|
|
9647
|
+
track: options.twiml?.track
|
|
9648
|
+
}), {
|
|
9649
|
+
headers: {
|
|
9650
|
+
"content-type": "text/xml; charset=utf-8"
|
|
9651
|
+
}
|
|
9652
|
+
});
|
|
9653
|
+
}).ws(streamPath, {
|
|
9654
|
+
close: async (ws, _code, reason) => {
|
|
9655
|
+
const bridge = bridges.get(ws);
|
|
9656
|
+
bridges.delete(ws);
|
|
9657
|
+
await bridge?.close(reason);
|
|
9658
|
+
},
|
|
9659
|
+
message: async (ws, raw) => {
|
|
9660
|
+
let bridge = bridges.get(ws);
|
|
9661
|
+
if (!bridge) {
|
|
9662
|
+
bridge = createTwilioMediaStreamBridge({
|
|
9663
|
+
close: (code, reason) => {
|
|
9664
|
+
ws.close(code, reason);
|
|
9665
|
+
},
|
|
9666
|
+
send: (data) => {
|
|
9667
|
+
ws.send(data);
|
|
9668
|
+
}
|
|
9669
|
+
}, options);
|
|
9670
|
+
bridges.set(ws, bridge);
|
|
9671
|
+
}
|
|
9672
|
+
await bridge.handleMessage(raw);
|
|
9673
|
+
}
|
|
9674
|
+
}).use(createVoiceTelephonyWebhookRoutes({
|
|
9675
|
+
...options.webhook ?? {},
|
|
9676
|
+
context: options.context,
|
|
9677
|
+
path: webhookPath,
|
|
9678
|
+
policy: webhookPolicy,
|
|
9679
|
+
provider: "twilio"
|
|
9680
|
+
}));
|
|
9681
|
+
if (!setupPath) {
|
|
9682
|
+
if (!smokePath) {
|
|
9683
|
+
return app;
|
|
9684
|
+
}
|
|
9685
|
+
return app.get(smokePath, async ({ query, request }) => {
|
|
9686
|
+
const report = await runTwilioVoiceSmokeTest({
|
|
9687
|
+
app,
|
|
9688
|
+
options,
|
|
9689
|
+
query,
|
|
9690
|
+
request,
|
|
9691
|
+
streamPath,
|
|
9692
|
+
twimlPath,
|
|
9693
|
+
webhookPath
|
|
9694
|
+
});
|
|
9695
|
+
if (query.format === "html") {
|
|
9696
|
+
return new Response(renderTwilioVoiceSmokeHTML(report, options.smoke?.title ?? "AbsoluteJS Twilio Voice Smoke Test"), {
|
|
9697
|
+
headers: {
|
|
9698
|
+
"content-type": "text/html; charset=utf-8"
|
|
9699
|
+
}
|
|
9700
|
+
});
|
|
9701
|
+
}
|
|
9702
|
+
return report;
|
|
9703
|
+
});
|
|
9704
|
+
}
|
|
9705
|
+
const withSetup = app.get(setupPath, async ({ query, request }) => {
|
|
9706
|
+
const status = await buildTwilioVoiceSetupStatus(options, {
|
|
9707
|
+
query,
|
|
9708
|
+
request,
|
|
9709
|
+
streamPath,
|
|
9710
|
+
twimlPath,
|
|
9711
|
+
webhookPath
|
|
9712
|
+
});
|
|
9713
|
+
if (query.format === "html") {
|
|
9714
|
+
return new Response(renderTwilioVoiceSetupHTML(status, options.setup?.title ?? "AbsoluteJS Twilio Voice Setup"), {
|
|
9715
|
+
headers: {
|
|
9716
|
+
"content-type": "text/html; charset=utf-8"
|
|
9717
|
+
}
|
|
9718
|
+
});
|
|
9719
|
+
}
|
|
9720
|
+
return status;
|
|
9721
|
+
});
|
|
9722
|
+
if (!smokePath) {
|
|
9723
|
+
return withSetup;
|
|
9724
|
+
}
|
|
9725
|
+
return withSetup.get(smokePath, async ({ query, request }) => {
|
|
9726
|
+
const report = await runTwilioVoiceSmokeTest({
|
|
9727
|
+
app,
|
|
9728
|
+
options,
|
|
9729
|
+
query,
|
|
9730
|
+
request,
|
|
9731
|
+
streamPath,
|
|
9732
|
+
twimlPath,
|
|
9733
|
+
webhookPath
|
|
9734
|
+
});
|
|
9735
|
+
if (query.format === "html") {
|
|
9736
|
+
return new Response(renderTwilioVoiceSmokeHTML(report, options.smoke?.title ?? "AbsoluteJS Twilio Voice Smoke Test"), {
|
|
9737
|
+
headers: {
|
|
9738
|
+
"content-type": "text/html; charset=utf-8"
|
|
9739
|
+
}
|
|
9740
|
+
});
|
|
9741
|
+
}
|
|
9742
|
+
return report;
|
|
9743
|
+
});
|
|
9744
|
+
};
|
|
8023
9745
|
|
|
8024
9746
|
// src/testing/telephony.ts
|
|
8025
9747
|
var DEFAULT_PCM16_FORMAT = {
|
|
@@ -8285,7 +10007,7 @@ var runVoiceTelephonyBenchmark = async (scenarios = getDefaultVoiceTelephonyBenc
|
|
|
8285
10007
|
};
|
|
8286
10008
|
};
|
|
8287
10009
|
// src/testing/tts.ts
|
|
8288
|
-
var
|
|
10010
|
+
var DEFAULT_REALTIME_FORMAT2 = {
|
|
8289
10011
|
channels: 1,
|
|
8290
10012
|
container: "raw",
|
|
8291
10013
|
encoding: "pcm_s16le",
|
|
@@ -8344,7 +10066,7 @@ var runTTSAdapterFixture = async (adapter, fixture, options = {}) => {
|
|
|
8344
10066
|
let audioDurationMs = 0;
|
|
8345
10067
|
let audioChunkCount = 0;
|
|
8346
10068
|
const session = adapter.kind === "realtime" ? await adapter.open({
|
|
8347
|
-
format: options.realtimeFormat ??
|
|
10069
|
+
format: options.realtimeFormat ?? DEFAULT_REALTIME_FORMAT2,
|
|
8348
10070
|
sessionId: `tts-benchmark:${fixture.id}`,
|
|
8349
10071
|
...openOptions ?? {}
|
|
8350
10072
|
}) : await adapter.open({
|
|
@@ -8511,6 +10233,7 @@ export {
|
|
|
8511
10233
|
getDefaultTTSBenchmarkFixtures,
|
|
8512
10234
|
evaluateSTTBenchmarkAcceptance,
|
|
8513
10235
|
createVoiceProviderFailureSimulator,
|
|
10236
|
+
createVoiceIOProviderFailureSimulator,
|
|
8514
10237
|
createVoiceCallReviewRecorder,
|
|
8515
10238
|
createVoiceCallReviewFromLiveTelephonyReport,
|
|
8516
10239
|
createTelephonyVoiceTestFixtures,
|