@absolutejs/voice 0.0.22-beta.36 → 0.0.22-beta.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +341 -25
- package/dist/resilienceRoutes.d.ts +106 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap
|
|
|
7
7
|
export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
|
|
8
8
|
export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, createVoiceProviderRouter } from './modelAdapters';
|
|
9
9
|
export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
|
|
10
|
+
export { createVoiceResilienceRoutes, listVoiceRoutingEvents, renderVoiceResilienceHTML } from './resilienceRoutes';
|
|
10
11
|
export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
|
|
11
12
|
export { buildVoiceTraceReplay, createVoiceMemoryTraceSinkDeliveryStore, createVoiceTraceHTTPSink, createVoiceMemoryTraceEventStore, createVoiceTraceSinkDeliveryId, createVoiceTraceSinkDeliveryRecord, createVoiceTraceSinkStore, createVoiceTraceEvent, createVoiceTraceEventId, deliverVoiceTraceEventsToSinks, evaluateVoiceTrace, exportVoiceTrace, filterVoiceTraceEvents, pruneVoiceTraceEvents, redactVoiceTraceEvent, redactVoiceTraceEvents, redactVoiceTraceText, renderVoiceTraceHTML, renderVoiceTraceMarkdown, resolveVoiceTraceRedactionOptions, selectVoiceTraceEventsForPrune, summarizeVoiceTrace } from './trace';
|
|
12
13
|
export { createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore } from './sqliteStore';
|
|
@@ -37,6 +38,7 @@ export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssi
|
|
|
37
38
|
export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
|
|
38
39
|
export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
|
|
39
40
|
export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
|
|
41
|
+
export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingEvent, VoiceRoutingEventKind } from './resilienceRoutes';
|
|
40
42
|
export type { VoiceIOProviderRouterEvent, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
|
|
41
43
|
export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
|
|
42
44
|
export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
|
package/dist/index.js
CHANGED
|
@@ -8858,6 +8858,319 @@ var createGeminiVoiceAssistantModel = (options) => {
|
|
|
8858
8858
|
}
|
|
8859
8859
|
};
|
|
8860
8860
|
};
|
|
8861
|
+
// src/resilienceRoutes.ts
|
|
8862
|
+
import { Elysia as Elysia5 } from "elysia";
|
|
8863
|
+
var escapeHtml7 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
8864
|
+
var getString4 = (value) => typeof value === "string" ? value : undefined;
|
|
8865
|
+
var getNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
8866
|
+
var getBoolean = (value) => value === true;
|
|
8867
|
+
var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
|
|
8868
|
+
var listVoiceRoutingEvents = (events) => {
|
|
8869
|
+
const routingEvents = [];
|
|
8870
|
+
for (const event of events) {
|
|
8871
|
+
if (event.type !== "session.error") {
|
|
8872
|
+
continue;
|
|
8873
|
+
}
|
|
8874
|
+
const provider = getString4(event.payload.provider);
|
|
8875
|
+
const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
8876
|
+
if (!provider || !providerStatus) {
|
|
8877
|
+
continue;
|
|
8878
|
+
}
|
|
8879
|
+
const kind = getString4(event.payload.kind);
|
|
8880
|
+
routingEvents.push({
|
|
8881
|
+
at: event.at,
|
|
8882
|
+
attempt: getNumber2(event.payload.attempt),
|
|
8883
|
+
elapsedMs: getNumber2(event.payload.elapsedMs),
|
|
8884
|
+
error: getString4(event.payload.error),
|
|
8885
|
+
fallbackProvider: getString4(event.payload.fallbackProvider),
|
|
8886
|
+
kind: kind === "stt" || kind === "tts" ? kind : "llm",
|
|
8887
|
+
latencyBudgetMs: getNumber2(event.payload.latencyBudgetMs),
|
|
8888
|
+
operation: getString4(event.payload.operation),
|
|
8889
|
+
provider,
|
|
8890
|
+
selectedProvider: getString4(event.payload.selectedProvider),
|
|
8891
|
+
sessionId: event.sessionId,
|
|
8892
|
+
status: providerStatus,
|
|
8893
|
+
timedOut: getBoolean(event.payload.timedOut),
|
|
8894
|
+
turnId: event.turnId
|
|
8895
|
+
});
|
|
8896
|
+
}
|
|
8897
|
+
return routingEvents.sort((left, right) => right.at - left.at);
|
|
8898
|
+
};
|
|
8899
|
+
var summarizeRoutingEvents = (events) => {
|
|
8900
|
+
const byKind = new Map;
|
|
8901
|
+
let errors = 0;
|
|
8902
|
+
let fallbacks = 0;
|
|
8903
|
+
let timeouts = 0;
|
|
8904
|
+
for (const event of events) {
|
|
8905
|
+
byKind.set(event.kind, (byKind.get(event.kind) ?? 0) + 1);
|
|
8906
|
+
if (event.status === "error") {
|
|
8907
|
+
errors += 1;
|
|
8908
|
+
}
|
|
8909
|
+
if (event.status === "fallback") {
|
|
8910
|
+
fallbacks += 1;
|
|
8911
|
+
}
|
|
8912
|
+
if (event.timedOut) {
|
|
8913
|
+
timeouts += 1;
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
return {
|
|
8917
|
+
byKind,
|
|
8918
|
+
errors,
|
|
8919
|
+
fallbacks,
|
|
8920
|
+
timeouts,
|
|
8921
|
+
total: events.length
|
|
8922
|
+
};
|
|
8923
|
+
};
|
|
8924
|
+
var renderProviderCards = (title, providers) => {
|
|
8925
|
+
if (providers.length === 0) {
|
|
8926
|
+
return `<p class="muted">No ${escapeHtml7(title)} provider health yet.</p>`;
|
|
8927
|
+
}
|
|
8928
|
+
return `<div class="provider-grid">${providers.map((provider) => `
|
|
8929
|
+
<article class="card provider ${escapeHtml7(provider.status)}">
|
|
8930
|
+
<div class="card-header">
|
|
8931
|
+
<strong>${escapeHtml7(provider.provider)}</strong>
|
|
8932
|
+
<span>${escapeHtml7(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
|
|
8933
|
+
</div>
|
|
8934
|
+
<dl>
|
|
8935
|
+
<div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
|
|
8936
|
+
<div><dt>Avg latency</dt><dd>${provider.averageElapsedMs ?? 0}ms</dd></div>
|
|
8937
|
+
<div><dt>Errors</dt><dd>${provider.errorCount}</dd></div>
|
|
8938
|
+
<div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
|
|
8939
|
+
<div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
|
|
8940
|
+
</dl>
|
|
8941
|
+
${provider.lastError ? `<p class="muted">${escapeHtml7(provider.lastError)}</p>` : ""}
|
|
8942
|
+
</article>
|
|
8943
|
+
`).join("")}</div>`;
|
|
8944
|
+
};
|
|
8945
|
+
var renderTimeline2 = (events) => {
|
|
8946
|
+
if (events.length === 0) {
|
|
8947
|
+
return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
|
|
8948
|
+
}
|
|
8949
|
+
return `<div class="timeline">${events.slice(0, 40).map((event) => `
|
|
8950
|
+
<article class="card event ${escapeHtml7(event.status ?? "unknown")}">
|
|
8951
|
+
<div class="card-header">
|
|
8952
|
+
<strong>${escapeHtml7(event.kind.toUpperCase())} ${escapeHtml7(event.operation ?? "generate")}</strong>
|
|
8953
|
+
<span>${new Date(event.at).toLocaleString()}</span>
|
|
8954
|
+
</div>
|
|
8955
|
+
<p>
|
|
8956
|
+
<span class="pill">${escapeHtml7(event.status ?? "unknown")}</span>
|
|
8957
|
+
<span class="pill">provider: ${escapeHtml7(event.provider ?? "unknown")}</span>
|
|
8958
|
+
${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml7(event.fallbackProvider)}</span>` : ""}
|
|
8959
|
+
${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
|
|
8960
|
+
</p>
|
|
8961
|
+
<dl>
|
|
8962
|
+
<div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
|
|
8963
|
+
<div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
|
|
8964
|
+
<div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
|
|
8965
|
+
<div><dt>Session</dt><dd>${escapeHtml7(event.sessionId)}</dd></div>
|
|
8966
|
+
</dl>
|
|
8967
|
+
${event.error ? `<p class="muted">${escapeHtml7(event.error)}</p>` : ""}
|
|
8968
|
+
</article>
|
|
8969
|
+
`).join("")}</div>`;
|
|
8970
|
+
};
|
|
8971
|
+
var renderSimulationControls = (kind, simulation) => {
|
|
8972
|
+
if (!simulation) {
|
|
8973
|
+
return "";
|
|
8974
|
+
}
|
|
8975
|
+
const configuredProviders = simulation.providers.filter((provider) => provider.configured !== false);
|
|
8976
|
+
if (configuredProviders.length === 0) {
|
|
8977
|
+
return `<p class="muted">No ${kind.toUpperCase()} providers are configured for simulation.</p>`;
|
|
8978
|
+
}
|
|
8979
|
+
const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
|
|
8980
|
+
const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
|
|
8981
|
+
const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
|
|
8982
|
+
return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml7(pathPrefix)}">
|
|
8983
|
+
<p class="muted">${escapeHtml7(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
|
|
8984
|
+
<div class="simulate-actions">
|
|
8985
|
+
${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml7(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml7(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
|
|
8986
|
+
${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml7(provider.provider)}">Mark ${escapeHtml7(provider.provider)} recovered</button>`).join("")}
|
|
8987
|
+
</div>
|
|
8988
|
+
${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml7(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
|
|
8989
|
+
<pre class="simulate-output" hidden></pre>
|
|
8990
|
+
</div>`;
|
|
8991
|
+
};
|
|
8992
|
+
var renderVoiceResilienceHTML = (input) => {
|
|
8993
|
+
const summary = summarizeRoutingEvents(input.routingEvents);
|
|
8994
|
+
const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml7(kind)}: ${String(count)}</span>`).join("");
|
|
8995
|
+
const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml7(link.href)}">${escapeHtml7(link.label)}</a>`).join(" \xB7 ") : "";
|
|
8996
|
+
return `<!doctype html>
|
|
8997
|
+
<html lang="en">
|
|
8998
|
+
<head>
|
|
8999
|
+
<meta charset="utf-8" />
|
|
9000
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
9001
|
+
<title>${escapeHtml7(input.title ?? "AbsoluteJS Voice Resilience")}</title>
|
|
9002
|
+
<style>
|
|
9003
|
+
:root { color-scheme: dark; }
|
|
9004
|
+
body { background: radial-gradient(circle at top left, #172554, #09090b 36%, #050505); color: #f4f4f5; font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; padding: 24px; }
|
|
9005
|
+
main { display: grid; gap: 16px; margin: 0 auto; max-width: 1180px; }
|
|
9006
|
+
section, .card { background: rgba(19, 22, 27, 0.92); border: 1px solid #27272a; border-radius: 20px; padding: 20px; }
|
|
9007
|
+
.hero { background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(245, 158, 11, 0.12)); }
|
|
9008
|
+
.grid, .provider-grid { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
9009
|
+
.timeline { display: grid; gap: 12px; }
|
|
9010
|
+
.card-header { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
|
|
9011
|
+
.card-header strong { font-size: 1.05rem; }
|
|
9012
|
+
.metric strong { display: block; font-size: 2rem; margin-top: 6px; }
|
|
9013
|
+
.muted, dt, span { color: #a1a1aa; }
|
|
9014
|
+
dl { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
9015
|
+
dl div { background: #0f1217; border: 1px solid #27272a; border-radius: 12px; padding: 10px; }
|
|
9016
|
+
dd { font-weight: 800; margin: 4px 0 0; }
|
|
9017
|
+
.pill { background: #0f1217; border: 1px solid #3f3f46; border-radius: 999px; color: #d4d4d8; display: inline-flex; margin: 3px 4px 3px 0; padding: 5px 9px; }
|
|
9018
|
+
.danger { border-color: rgba(239, 68, 68, 0.75); color: #fecaca; }
|
|
9019
|
+
.event.error { border-color: rgba(239, 68, 68, 0.7); }
|
|
9020
|
+
.event.fallback { border-color: rgba(245, 158, 11, 0.7); }
|
|
9021
|
+
.event.success, .provider.healthy { border-color: rgba(34, 197, 94, 0.5); }
|
|
9022
|
+
.provider.suppressed, .provider.degraded, .provider.rate-limited { border-color: rgba(239, 68, 68, 0.7); }
|
|
9023
|
+
.provider.recoverable { border-color: rgba(59, 130, 246, 0.7); }
|
|
9024
|
+
button { background: #f59e0b; border: 0; border-radius: 999px; color: #111827; cursor: pointer; font-weight: 800; padding: 10px 14px; }
|
|
9025
|
+
button:disabled { cursor: not-allowed; opacity: 0.45; }
|
|
9026
|
+
.simulate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
|
|
9027
|
+
.simulate-output { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #d4d4d8; overflow: auto; padding: 12px; white-space: pre-wrap; }
|
|
9028
|
+
a { color: #f59e0b; }
|
|
9029
|
+
@media (max-width: 850px) { .grid, .provider-grid, dl { grid-template-columns: 1fr; } }
|
|
9030
|
+
</style>
|
|
9031
|
+
</head>
|
|
9032
|
+
<body>
|
|
9033
|
+
<main>
|
|
9034
|
+
<section class="hero">
|
|
9035
|
+
<h1>Provider routing and resilience</h1>
|
|
9036
|
+
<p>One view for the production reliability story: LLM failover, STT/TTS routing, latency budgets, timeouts, and fallback decisions.</p>
|
|
9037
|
+
${links ? `<p>${links}</p>` : ""}
|
|
9038
|
+
<p>${kindCounts || '<span class="pill">No routing events yet</span>'}</p>
|
|
9039
|
+
</section>
|
|
9040
|
+
<section class="grid">
|
|
9041
|
+
<article class="card metric"><span>Total routing events</span><strong>${summary.total}</strong></article>
|
|
9042
|
+
<article class="card metric"><span>Fallbacks</span><strong>${summary.fallbacks}</strong></article>
|
|
9043
|
+
<article class="card metric"><span>Errors</span><strong>${summary.errors}</strong></article>
|
|
9044
|
+
<article class="card metric"><span>Timeouts</span><strong>${summary.timeouts}</strong></article>
|
|
9045
|
+
</section>
|
|
9046
|
+
<section>
|
|
9047
|
+
<h2>LLM provider health</h2>
|
|
9048
|
+
${renderProviderCards("LLM", input.llmProviderHealth)}
|
|
9049
|
+
</section>
|
|
9050
|
+
<section>
|
|
9051
|
+
<h2>STT provider health</h2>
|
|
9052
|
+
${renderSimulationControls("stt", input.sttSimulation)}
|
|
9053
|
+
${renderProviderCards("STT", input.sttProviderHealth)}
|
|
9054
|
+
</section>
|
|
9055
|
+
<section>
|
|
9056
|
+
<h2>TTS provider health</h2>
|
|
9057
|
+
${renderSimulationControls("tts", input.ttsSimulation)}
|
|
9058
|
+
${renderProviderCards("TTS", input.ttsProviderHealth)}
|
|
9059
|
+
</section>
|
|
9060
|
+
<section>
|
|
9061
|
+
<h2>Routing timeline</h2>
|
|
9062
|
+
${renderTimeline2(input.routingEvents)}
|
|
9063
|
+
</section>
|
|
9064
|
+
</main>
|
|
9065
|
+
<script>
|
|
9066
|
+
const showResult = (panel, result) => {
|
|
9067
|
+
const output = panel.querySelector(".simulate-output");
|
|
9068
|
+
if (!output) return;
|
|
9069
|
+
output.hidden = false;
|
|
9070
|
+
output.textContent = JSON.stringify(result, null, 2);
|
|
9071
|
+
};
|
|
9072
|
+
document.querySelectorAll("[data-sim-prefix]").forEach((panel) => {
|
|
9073
|
+
const prefix = panel.getAttribute("data-sim-prefix");
|
|
9074
|
+
panel.querySelectorAll("[data-provider-fail]").forEach((button) => {
|
|
9075
|
+
button.addEventListener("click", async () => {
|
|
9076
|
+
const provider = button.getAttribute("data-provider-fail");
|
|
9077
|
+
const response = await fetch(prefix + "/failure?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
|
|
9078
|
+
showResult(panel, await response.json());
|
|
9079
|
+
if (response.ok) window.setTimeout(() => window.location.reload(), 450);
|
|
9080
|
+
});
|
|
9081
|
+
});
|
|
9082
|
+
panel.querySelectorAll("[data-provider-recover]").forEach((button) => {
|
|
9083
|
+
button.addEventListener("click", async () => {
|
|
9084
|
+
const provider = button.getAttribute("data-provider-recover");
|
|
9085
|
+
const response = await fetch(prefix + "/recovery?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
|
|
9086
|
+
showResult(panel, await response.json());
|
|
9087
|
+
if (response.ok) window.setTimeout(() => window.location.reload(), 450);
|
|
9088
|
+
});
|
|
9089
|
+
});
|
|
9090
|
+
});
|
|
9091
|
+
</script>
|
|
9092
|
+
</body>
|
|
9093
|
+
</html>`;
|
|
9094
|
+
};
|
|
9095
|
+
var providerFromQuery = (value, providers) => typeof value === "string" && providers.some((provider) => provider.provider === value && provider.configured !== false) ? value : undefined;
|
|
9096
|
+
var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
|
|
9097
|
+
if (!simulation) {
|
|
9098
|
+
return routes;
|
|
9099
|
+
}
|
|
9100
|
+
const pathPrefix = simulation.pathPrefix ?? defaultPathPrefix;
|
|
9101
|
+
routes.post(`${pathPrefix}/failure`, async ({ query, set }) => {
|
|
9102
|
+
const provider = providerFromQuery(query.provider, simulation.providers);
|
|
9103
|
+
if (!provider) {
|
|
9104
|
+
set.status = 400;
|
|
9105
|
+
return {
|
|
9106
|
+
error: "Provider is not configured for simulation."
|
|
9107
|
+
};
|
|
9108
|
+
}
|
|
9109
|
+
if (simulation.failureProviders && !simulation.failureProviders.includes(provider)) {
|
|
9110
|
+
set.status = 400;
|
|
9111
|
+
return {
|
|
9112
|
+
error: `${provider} is not configured for failure simulation.`
|
|
9113
|
+
};
|
|
9114
|
+
}
|
|
9115
|
+
if (simulation.fallbackRequiredProvider && !simulation.providers.some((entry) => entry.provider === simulation.fallbackRequiredProvider && entry.configured !== false)) {
|
|
9116
|
+
set.status = 400;
|
|
9117
|
+
return {
|
|
9118
|
+
error: simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} before simulating fallback.`
|
|
9119
|
+
};
|
|
9120
|
+
}
|
|
9121
|
+
return simulation.run(provider, "failure");
|
|
9122
|
+
});
|
|
9123
|
+
routes.post(`${pathPrefix}/recovery`, async ({ query, set }) => {
|
|
9124
|
+
const provider = providerFromQuery(query.provider, simulation.providers);
|
|
9125
|
+
if (!provider) {
|
|
9126
|
+
set.status = 400;
|
|
9127
|
+
return {
|
|
9128
|
+
error: "Provider is not configured for simulation."
|
|
9129
|
+
};
|
|
9130
|
+
}
|
|
9131
|
+
return simulation.run(provider, "recovery");
|
|
9132
|
+
});
|
|
9133
|
+
return routes;
|
|
9134
|
+
};
|
|
9135
|
+
var createVoiceResilienceRoutes = (options) => {
|
|
9136
|
+
const path = options.path ?? "/resilience";
|
|
9137
|
+
const routes = new Elysia5({
|
|
9138
|
+
name: options.name ?? "absolutejs-voice-resilience"
|
|
9139
|
+
}).get(path, async () => {
|
|
9140
|
+
const events = await options.store.list();
|
|
9141
|
+
const sttEvents = events.filter((event) => event.payload.kind === "stt");
|
|
9142
|
+
const ttsEvents = events.filter((event) => event.payload.kind === "tts");
|
|
9143
|
+
const data = {
|
|
9144
|
+
links: options.links,
|
|
9145
|
+
llmProviderHealth: await summarizeVoiceProviderHealth({
|
|
9146
|
+
events,
|
|
9147
|
+
providers: options.llmProviders ?? []
|
|
9148
|
+
}),
|
|
9149
|
+
routingEvents: listVoiceRoutingEvents(events),
|
|
9150
|
+
sttProviderHealth: await summarizeVoiceProviderHealth({
|
|
9151
|
+
events: sttEvents,
|
|
9152
|
+
providers: options.sttProviders ?? []
|
|
9153
|
+
}),
|
|
9154
|
+
sttSimulation: options.sttSimulation,
|
|
9155
|
+
title: options.title,
|
|
9156
|
+
ttsProviderHealth: await summarizeVoiceProviderHealth({
|
|
9157
|
+
events: ttsEvents,
|
|
9158
|
+
providers: options.ttsProviders ?? []
|
|
9159
|
+
}),
|
|
9160
|
+
ttsSimulation: options.ttsSimulation
|
|
9161
|
+
};
|
|
9162
|
+
const body = await (options.render ?? renderVoiceResilienceHTML)(data);
|
|
9163
|
+
return new Response(body, {
|
|
9164
|
+
headers: {
|
|
9165
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9166
|
+
...options.headers
|
|
9167
|
+
}
|
|
9168
|
+
});
|
|
9169
|
+
});
|
|
9170
|
+
registerSimulationRoutes(routes, options.sttSimulation, "/api/stt-simulate");
|
|
9171
|
+
registerSimulationRoutes(routes, options.ttsSimulation, "/api/tts-simulate");
|
|
9172
|
+
return routes;
|
|
9173
|
+
};
|
|
8861
9174
|
// src/providerAdapters.ts
|
|
8862
9175
|
class VoiceIOProviderTimeoutError extends Error {
|
|
8863
9176
|
provider;
|
|
@@ -9702,7 +10015,7 @@ var createVoiceMemoryStore = () => {
|
|
|
9702
10015
|
return { get, getOrCreate, list, remove, set };
|
|
9703
10016
|
};
|
|
9704
10017
|
// src/opsWebhook.ts
|
|
9705
|
-
import { Elysia as
|
|
10018
|
+
import { Elysia as Elysia6 } from "elysia";
|
|
9706
10019
|
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
9707
10020
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
9708
10021
|
const encoder = new TextEncoder;
|
|
@@ -9832,7 +10145,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
|
9832
10145
|
};
|
|
9833
10146
|
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
9834
10147
|
const path = options.path ?? "/api/voice-ops/webhook";
|
|
9835
|
-
return new
|
|
10148
|
+
return new Elysia6().post(path, async ({ body, request, set }) => {
|
|
9836
10149
|
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
9837
10150
|
if (options.signingSecret) {
|
|
9838
10151
|
const verification = await verifyVoiceOpsWebhookSignature({
|
|
@@ -9865,9 +10178,9 @@ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
|
9865
10178
|
});
|
|
9866
10179
|
};
|
|
9867
10180
|
// src/handoffHealth.ts
|
|
9868
|
-
import { Elysia as
|
|
9869
|
-
var
|
|
9870
|
-
var
|
|
10181
|
+
import { Elysia as Elysia7 } from "elysia";
|
|
10182
|
+
var escapeHtml8 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10183
|
+
var getString5 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9871
10184
|
var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
|
|
9872
10185
|
var increment3 = (record, key) => {
|
|
9873
10186
|
record[key] = (record[key] ?? 0) + 1;
|
|
@@ -9875,11 +10188,11 @@ var increment3 = (record, key) => {
|
|
|
9875
10188
|
var normalizeDelivery = (adapterId, value) => {
|
|
9876
10189
|
const record = value && typeof value === "object" ? value : {};
|
|
9877
10190
|
return {
|
|
9878
|
-
adapterId:
|
|
9879
|
-
adapterKind:
|
|
10191
|
+
adapterId: getString5(record.adapterId) ?? adapterId,
|
|
10192
|
+
adapterKind: getString5(record.adapterKind),
|
|
9880
10193
|
deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
|
|
9881
|
-
deliveredTo:
|
|
9882
|
-
error:
|
|
10194
|
+
deliveredTo: getString5(record.deliveredTo),
|
|
10195
|
+
error: getString5(record.error),
|
|
9883
10196
|
status: isStatus(record.status) ? record.status : "failed"
|
|
9884
10197
|
};
|
|
9885
10198
|
};
|
|
@@ -9913,13 +10226,13 @@ var summarizeVoiceHandoffHealth = async (options = {}) => {
|
|
|
9913
10226
|
const status = isStatus(event.payload.status) ? event.payload.status : "failed";
|
|
9914
10227
|
const deliveries = normalizeDeliveries(event.payload);
|
|
9915
10228
|
const item = {
|
|
9916
|
-
action:
|
|
10229
|
+
action: getString5(event.payload.action),
|
|
9917
10230
|
at: event.at,
|
|
9918
10231
|
deliveries,
|
|
9919
|
-
reason:
|
|
10232
|
+
reason: getString5(event.payload.reason),
|
|
9920
10233
|
sessionId: event.sessionId,
|
|
9921
10234
|
status,
|
|
9922
|
-
target:
|
|
10235
|
+
target: getString5(event.payload.target)
|
|
9923
10236
|
};
|
|
9924
10237
|
return {
|
|
9925
10238
|
...item,
|
|
@@ -9984,10 +10297,10 @@ var renderActionSummary = (summary) => {
|
|
|
9984
10297
|
return [
|
|
9985
10298
|
'<section class="voice-handoff-health-columns">',
|
|
9986
10299
|
"<article><h3>Actions</h3>",
|
|
9987
|
-
actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${
|
|
10300
|
+
actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml8(action)}: ${String(count)}</li>`).join("")}</ul>`,
|
|
9988
10301
|
"</article>",
|
|
9989
10302
|
"<article><h3>Adapters</h3>",
|
|
9990
|
-
adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${
|
|
10303
|
+
adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml8(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
|
|
9991
10304
|
"</article>",
|
|
9992
10305
|
"</section>"
|
|
9993
10306
|
].join("");
|
|
@@ -10001,22 +10314,22 @@ var renderVoiceHandoffHealthHTML = (summary) => [
|
|
|
10001
10314
|
summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
|
|
10002
10315
|
'<div class="voice-handoff-health-events">',
|
|
10003
10316
|
...summary.events.map((event) => [
|
|
10004
|
-
`<article class="${
|
|
10317
|
+
`<article class="${escapeHtml8(event.status)}">`,
|
|
10005
10318
|
'<div class="voice-handoff-health-event-header">',
|
|
10006
|
-
`<strong>${
|
|
10007
|
-
`<span>${
|
|
10319
|
+
`<strong>${escapeHtml8(event.action ?? "handoff")}</strong>`,
|
|
10320
|
+
`<span>${escapeHtml8(event.status)}</span>`,
|
|
10008
10321
|
"</div>",
|
|
10009
|
-
`<p><small>${
|
|
10010
|
-
event.target ? `<p>Target: ${
|
|
10011
|
-
event.reason ? `<p>Reason: ${
|
|
10322
|
+
`<p><small>${escapeHtml8(event.sessionId)}</small></p>`,
|
|
10323
|
+
event.target ? `<p>Target: ${escapeHtml8(event.target)}</p>` : "",
|
|
10324
|
+
event.reason ? `<p>Reason: ${escapeHtml8(event.reason)}</p>` : "",
|
|
10012
10325
|
event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
|
|
10013
10326
|
"<li>",
|
|
10014
|
-
`${
|
|
10015
|
-
delivery.deliveredTo ? ` to ${
|
|
10016
|
-
delivery.error ? ` (${
|
|
10327
|
+
`${escapeHtml8(delivery.adapterId)}: ${escapeHtml8(delivery.status)}`,
|
|
10328
|
+
delivery.deliveredTo ? ` to ${escapeHtml8(delivery.deliveredTo)}` : "",
|
|
10329
|
+
delivery.error ? ` (${escapeHtml8(delivery.error)})` : "",
|
|
10017
10330
|
"</li>"
|
|
10018
10331
|
].join("")).join("")}</ul>` : "",
|
|
10019
|
-
event.replayHref ? `<p><a href="${
|
|
10332
|
+
event.replayHref ? `<p><a href="${escapeHtml8(event.replayHref)}">Open replay</a></p>` : "",
|
|
10020
10333
|
"</article>"
|
|
10021
10334
|
].join("")),
|
|
10022
10335
|
"</div>"
|
|
@@ -10048,7 +10361,7 @@ var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) =>
|
|
|
10048
10361
|
var createVoiceHandoffHealthRoutes = (options = {}) => {
|
|
10049
10362
|
const path = options.path ?? "/api/voice-handoffs";
|
|
10050
10363
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
10051
|
-
const routes = new
|
|
10364
|
+
const routes = new Elysia7({
|
|
10052
10365
|
name: options.name ?? "absolutejs-voice-handoff-health"
|
|
10053
10366
|
}).get(path, createVoiceHandoffHealthJSONHandler(options));
|
|
10054
10367
|
if (htmlPath) {
|
|
@@ -12202,6 +12515,7 @@ export {
|
|
|
12202
12515
|
renderVoiceTraceMarkdown,
|
|
12203
12516
|
renderVoiceTraceHTML,
|
|
12204
12517
|
renderVoiceSessionsHTML,
|
|
12518
|
+
renderVoiceResilienceHTML,
|
|
12205
12519
|
renderVoiceProviderHealthHTML,
|
|
12206
12520
|
renderVoiceHandoffHealthHTML,
|
|
12207
12521
|
renderVoiceCallReviewMarkdown,
|
|
@@ -12214,6 +12528,7 @@ export {
|
|
|
12214
12528
|
pruneVoiceTraceEvents,
|
|
12215
12529
|
matchesVoiceOpsTaskAssignmentRule,
|
|
12216
12530
|
markVoiceOpsTaskSLABreached,
|
|
12531
|
+
listVoiceRoutingEvents,
|
|
12217
12532
|
listVoiceOpsTasks,
|
|
12218
12533
|
isVoiceOpsTaskOverdue,
|
|
12219
12534
|
heartbeatVoiceOpsTask,
|
|
@@ -12269,6 +12584,7 @@ export {
|
|
|
12269
12584
|
createVoiceSQLiteExternalObjectMapStore,
|
|
12270
12585
|
createVoiceS3ReviewStore,
|
|
12271
12586
|
createVoiceReviewSavedEvent,
|
|
12587
|
+
createVoiceResilienceRoutes,
|
|
12272
12588
|
createVoiceRedisTaskLeaseCoordinator,
|
|
12273
12589
|
createVoiceRedisIdempotencyStore,
|
|
12274
12590
|
createVoiceProviderRouter,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { type VoiceProviderHealthSummary } from './providerHealth';
|
|
3
|
+
import type { StoredVoiceTraceEvent, VoiceTraceEventStore } from './trace';
|
|
4
|
+
import type { VoiceIOProviderFailureSimulationMode, VoiceIOProviderFailureSimulationResult } from './testing/ioProviderSimulator';
|
|
5
|
+
export type VoiceRoutingEventKind = 'llm' | 'stt' | 'tts';
|
|
6
|
+
export type VoiceRoutingEvent = {
|
|
7
|
+
at: number;
|
|
8
|
+
attempt?: number;
|
|
9
|
+
elapsedMs?: number;
|
|
10
|
+
error?: string;
|
|
11
|
+
fallbackProvider?: string;
|
|
12
|
+
kind: VoiceRoutingEventKind;
|
|
13
|
+
latencyBudgetMs?: number;
|
|
14
|
+
operation?: string;
|
|
15
|
+
provider?: string;
|
|
16
|
+
selectedProvider?: string;
|
|
17
|
+
sessionId: string;
|
|
18
|
+
status?: string;
|
|
19
|
+
timedOut: boolean;
|
|
20
|
+
turnId?: string;
|
|
21
|
+
};
|
|
22
|
+
export type VoiceResilienceLink = {
|
|
23
|
+
href: string;
|
|
24
|
+
label: string;
|
|
25
|
+
};
|
|
26
|
+
export type VoiceResilienceSimulationProvider<TProvider extends string = string> = {
|
|
27
|
+
configured?: boolean;
|
|
28
|
+
provider: TProvider;
|
|
29
|
+
};
|
|
30
|
+
export type VoiceResilienceIOSimulator<TProvider extends string = string> = {
|
|
31
|
+
failureProviders?: readonly TProvider[];
|
|
32
|
+
fallbackRequiredProvider?: TProvider;
|
|
33
|
+
fallbackRequiredMessage?: string;
|
|
34
|
+
failureMessage?: string;
|
|
35
|
+
label?: string;
|
|
36
|
+
pathPrefix?: string;
|
|
37
|
+
providers: readonly VoiceResilienceSimulationProvider<TProvider>[];
|
|
38
|
+
recoveryMessage?: string;
|
|
39
|
+
run: (provider: TProvider, mode: VoiceIOProviderFailureSimulationMode) => Promise<VoiceIOProviderFailureSimulationResult<TProvider>>;
|
|
40
|
+
};
|
|
41
|
+
export type VoiceResiliencePageData = {
|
|
42
|
+
links?: readonly VoiceResilienceLink[];
|
|
43
|
+
llmProviderHealth: VoiceProviderHealthSummary<string>[];
|
|
44
|
+
routingEvents: VoiceRoutingEvent[];
|
|
45
|
+
sttProviderHealth: VoiceProviderHealthSummary<string>[];
|
|
46
|
+
sttSimulation?: VoiceResilienceIOSimulator<string>;
|
|
47
|
+
title?: string;
|
|
48
|
+
ttsProviderHealth: VoiceProviderHealthSummary<string>[];
|
|
49
|
+
ttsSimulation?: VoiceResilienceIOSimulator<string>;
|
|
50
|
+
};
|
|
51
|
+
export type VoiceResilienceRoutesOptions = {
|
|
52
|
+
headers?: HeadersInit;
|
|
53
|
+
links?: readonly VoiceResilienceLink[];
|
|
54
|
+
llmProviders?: readonly string[];
|
|
55
|
+
name?: string;
|
|
56
|
+
path?: string;
|
|
57
|
+
render?: (input: VoiceResiliencePageData) => string | Promise<string>;
|
|
58
|
+
sttProviders?: readonly string[];
|
|
59
|
+
sttSimulation?: VoiceResilienceIOSimulator<string>;
|
|
60
|
+
store: VoiceTraceEventStore;
|
|
61
|
+
title?: string;
|
|
62
|
+
ttsProviders?: readonly string[];
|
|
63
|
+
ttsSimulation?: VoiceResilienceIOSimulator<string>;
|
|
64
|
+
};
|
|
65
|
+
export declare const listVoiceRoutingEvents: (events: StoredVoiceTraceEvent[]) => VoiceRoutingEvent[];
|
|
66
|
+
export declare const renderVoiceResilienceHTML: (input: VoiceResiliencePageData) => string;
|
|
67
|
+
export declare const createVoiceResilienceRoutes: (options: VoiceResilienceRoutesOptions) => Elysia<"", {
|
|
68
|
+
decorator: {};
|
|
69
|
+
store: {};
|
|
70
|
+
derive: {};
|
|
71
|
+
resolve: {};
|
|
72
|
+
}, {
|
|
73
|
+
typebox: {};
|
|
74
|
+
error: {};
|
|
75
|
+
}, {
|
|
76
|
+
schema: {};
|
|
77
|
+
standaloneSchema: {};
|
|
78
|
+
macro: {};
|
|
79
|
+
macroFn: {};
|
|
80
|
+
parser: {};
|
|
81
|
+
response: {};
|
|
82
|
+
}, {
|
|
83
|
+
[x: string]: {
|
|
84
|
+
get: {
|
|
85
|
+
body: unknown;
|
|
86
|
+
params: {};
|
|
87
|
+
query: unknown;
|
|
88
|
+
headers: unknown;
|
|
89
|
+
response: {
|
|
90
|
+
200: Response;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
}, {
|
|
95
|
+
derive: {};
|
|
96
|
+
resolve: {};
|
|
97
|
+
schema: {};
|
|
98
|
+
standaloneSchema: {};
|
|
99
|
+
response: {};
|
|
100
|
+
}, {
|
|
101
|
+
derive: {};
|
|
102
|
+
resolve: {};
|
|
103
|
+
schema: {};
|
|
104
|
+
standaloneSchema: {};
|
|
105
|
+
response: {};
|
|
106
|
+
}>;
|