@absolutejs/voice 0.0.22-beta.36 → 0.0.22-beta.38
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/diagnosticsRoutes.d.ts +44 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +495 -41
- package/dist/resilienceRoutes.d.ts +106 -0
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { evaluateVoiceTrace, type StoredVoiceTraceEvent, type VoiceTraceEventFilter, type VoiceTraceEventStore, type VoiceTraceRedactionConfig } from './trace';
|
|
3
|
+
export type VoiceDiagnosticsRoutesOptions = {
|
|
4
|
+
evaluation?: Parameters<typeof evaluateVoiceTrace>[1];
|
|
5
|
+
headers?: HeadersInit;
|
|
6
|
+
name?: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
redact?: VoiceTraceRedactionConfig;
|
|
9
|
+
store: VoiceTraceEventStore;
|
|
10
|
+
title?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const resolveVoiceDiagnosticsTraceFilter: (query: Record<string, unknown>) => VoiceTraceEventFilter;
|
|
13
|
+
export declare const buildVoiceDiagnosticsMarkdown: (events: StoredVoiceTraceEvent[], options?: {
|
|
14
|
+
evaluation?: Parameters<typeof evaluateVoiceTrace>[1];
|
|
15
|
+
title?: string;
|
|
16
|
+
}) => string;
|
|
17
|
+
export declare const createVoiceDiagnosticsRoutes: (options: VoiceDiagnosticsRoutesOptions) => Elysia<"", {
|
|
18
|
+
decorator: {};
|
|
19
|
+
store: {};
|
|
20
|
+
derive: {};
|
|
21
|
+
resolve: {};
|
|
22
|
+
}, {
|
|
23
|
+
typebox: {};
|
|
24
|
+
error: {};
|
|
25
|
+
}, {
|
|
26
|
+
schema: {};
|
|
27
|
+
standaloneSchema: {};
|
|
28
|
+
macro: {};
|
|
29
|
+
macroFn: {};
|
|
30
|
+
parser: {};
|
|
31
|
+
response: {};
|
|
32
|
+
}, {}, {
|
|
33
|
+
derive: {};
|
|
34
|
+
resolve: {};
|
|
35
|
+
schema: {};
|
|
36
|
+
standaloneSchema: {};
|
|
37
|
+
response: {};
|
|
38
|
+
}, {
|
|
39
|
+
derive: {};
|
|
40
|
+
resolve: {};
|
|
41
|
+
schema: {};
|
|
42
|
+
standaloneSchema: {};
|
|
43
|
+
response: {};
|
|
44
|
+
}>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export { voice } from './plugin';
|
|
2
2
|
export { createVoiceAssistant, createVoiceExperiment, summarizeVoiceAssistantRuns } from './assistant';
|
|
3
3
|
export { createVoiceAssistantHealthHTMLHandler, createVoiceAssistantHealthJSONHandler, createVoiceAssistantHealthRoutes, renderVoiceAssistantHealthHTML, summarizeVoiceAssistantHealth } from './assistantHealth';
|
|
4
|
+
export { buildVoiceDiagnosticsMarkdown, createVoiceDiagnosticsRoutes, resolveVoiceDiagnosticsTraceFilter } from './diagnosticsRoutes';
|
|
4
5
|
export { createVoiceSessionListRoutes, createVoiceSessionReplayHTMLHandler, createVoiceSessionReplayJSONHandler, createVoiceSessionReplayRoutes, createVoiceSessionsHTMLHandler, createVoiceSessionsJSONHandler, renderVoiceSessionsHTML, summarizeVoiceSessions, summarizeVoiceSessionReplay } from './sessionReplay';
|
|
5
6
|
export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from './agent';
|
|
6
7
|
export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore } from './fileStore';
|
|
7
8
|
export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
|
|
8
9
|
export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, createVoiceProviderRouter } from './modelAdapters';
|
|
9
10
|
export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
|
|
11
|
+
export { createVoiceResilienceRoutes, listVoiceRoutingEvents, renderVoiceResilienceHTML } from './resilienceRoutes';
|
|
10
12
|
export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
|
|
11
13
|
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
14
|
export { createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore } from './sqliteStore';
|
|
@@ -34,9 +36,11 @@ export { createVoiceCallReviewFromLiveTelephonyReport, createVoiceCallReviewReco
|
|
|
34
36
|
export type { VoiceAssistant, VoiceAssistantArtifactPlan, VoiceAssistantExperiment, VoiceAssistantExperimentOptions, VoiceAssistantGuardrailInput, VoiceAssistantGuardrails, VoiceAssistantMemoryLifecycle, VoiceAssistantMemoryLifecycleInput, VoiceAssistantOptions, VoiceAssistantOutputGuardrailInput, VoiceAssistantPreset, VoiceAssistantRunsSummary, VoiceAssistantRunSummary, VoiceAssistantVariant } from './assistant';
|
|
35
37
|
export type { VoiceAssistantHealthFailure, VoiceAssistantHealthHTMLHandlerOptions, VoiceAssistantHealthRoutesOptions, VoiceAssistantHealthSummary, VoiceAssistantHealthSummaryOptions } from './assistantHealth';
|
|
36
38
|
export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssistantMemoryOptions, VoiceAssistantMemoryRecord, VoiceAssistantMemoryStore } from './assistantMemory';
|
|
39
|
+
export type { VoiceDiagnosticsRoutesOptions } from './diagnosticsRoutes';
|
|
37
40
|
export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
|
|
38
41
|
export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
|
|
39
42
|
export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
|
|
43
|
+
export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingEvent, VoiceRoutingEventKind } from './resilienceRoutes';
|
|
40
44
|
export type { VoiceIOProviderRouterEvent, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
|
|
41
45
|
export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
|
|
42
46
|
export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
|
package/dist/index.js
CHANGED
|
@@ -6822,7 +6822,7 @@ var createVoiceAssistantHealthRoutes = (options) => {
|
|
|
6822
6822
|
}
|
|
6823
6823
|
return routes;
|
|
6824
6824
|
};
|
|
6825
|
-
// src/
|
|
6825
|
+
// src/diagnosticsRoutes.ts
|
|
6826
6826
|
import { Elysia as Elysia4 } from "elysia";
|
|
6827
6827
|
|
|
6828
6828
|
// src/trace.ts
|
|
@@ -7489,9 +7489,144 @@ var buildVoiceTraceReplay = (events, options = {}) => ({
|
|
|
7489
7489
|
summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
|
|
7490
7490
|
});
|
|
7491
7491
|
|
|
7492
|
-
// src/
|
|
7493
|
-
var getString3 = (value) => typeof value === "string" ? value : undefined;
|
|
7492
|
+
// src/diagnosticsRoutes.ts
|
|
7494
7493
|
var escapeHtml6 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
7494
|
+
var getString3 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
7495
|
+
var getNumber2 = (value) => {
|
|
7496
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : undefined;
|
|
7497
|
+
return typeof parsed === "number" && Number.isFinite(parsed) ? parsed : undefined;
|
|
7498
|
+
};
|
|
7499
|
+
var getBoolean = (value) => value === true || value === "true" || value === "1";
|
|
7500
|
+
var parseTraceTypeFilter = (value) => {
|
|
7501
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
7502
|
+
return;
|
|
7503
|
+
}
|
|
7504
|
+
const types = value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
7505
|
+
return types.length <= 1 ? types[0] : types;
|
|
7506
|
+
};
|
|
7507
|
+
var resolveVoiceDiagnosticsTraceFilter = (query) => ({
|
|
7508
|
+
limit: getNumber2(query.limit),
|
|
7509
|
+
scenarioId: getString3(query.scenarioId),
|
|
7510
|
+
sessionId: getString3(query.sessionId),
|
|
7511
|
+
traceId: getString3(query.traceId),
|
|
7512
|
+
turnId: getString3(query.turnId),
|
|
7513
|
+
type: parseTraceTypeFilter(query.type)
|
|
7514
|
+
});
|
|
7515
|
+
var filterByDiagnosticsQuery = (events, query) => {
|
|
7516
|
+
const provider = getString3(query.provider);
|
|
7517
|
+
const status = getString3(query.status);
|
|
7518
|
+
const since = getNumber2(query.since);
|
|
7519
|
+
const until = getNumber2(query.until);
|
|
7520
|
+
return filterVoiceTraceEvents(events, resolveVoiceDiagnosticsTraceFilter(query)).filter((event) => (!provider || event.payload.provider === provider) && (!status || event.payload.providerStatus === status || event.payload.status === status) && (since === undefined || event.at >= since) && (until === undefined || event.at <= until));
|
|
7521
|
+
};
|
|
7522
|
+
var buildVoiceDiagnosticsMarkdown = (events, options = {}) => {
|
|
7523
|
+
const summary = summarizeVoiceTrace(events);
|
|
7524
|
+
const evaluation = evaluateVoiceTrace(events, options.evaluation);
|
|
7525
|
+
const trace = renderVoiceTraceMarkdown(events, {
|
|
7526
|
+
evaluation: options.evaluation,
|
|
7527
|
+
title: options.title ?? `Voice Diagnostics ${summary.sessionId ?? ""}`.trim()
|
|
7528
|
+
});
|
|
7529
|
+
return [
|
|
7530
|
+
`# ${options.title ?? "Voice Diagnostics Bug Report"}`,
|
|
7531
|
+
"",
|
|
7532
|
+
`Session: ${summary.sessionId ?? "unknown"}`,
|
|
7533
|
+
`Pass: ${evaluation.pass ? "yes" : "no"}`,
|
|
7534
|
+
`Events: ${summary.eventCount}`,
|
|
7535
|
+
`Turns: ${summary.turnCount}`,
|
|
7536
|
+
`Errors: ${summary.errorCount}`,
|
|
7537
|
+
`Tool errors: ${summary.toolErrorCount}`,
|
|
7538
|
+
`Estimated cost units: ${summary.cost.estimatedRelativeCostUnits}`,
|
|
7539
|
+
"",
|
|
7540
|
+
"## Issues",
|
|
7541
|
+
"",
|
|
7542
|
+
evaluation.issues.length ? evaluation.issues.map((issue) => `- [${issue.severity}] ${issue.code}: ${issue.message}`).join(`
|
|
7543
|
+
`) : "- none",
|
|
7544
|
+
"",
|
|
7545
|
+
"## Trace",
|
|
7546
|
+
"",
|
|
7547
|
+
trace
|
|
7548
|
+
].join(`
|
|
7549
|
+
`);
|
|
7550
|
+
};
|
|
7551
|
+
var renderDiagnosticsIndex = (input) => {
|
|
7552
|
+
const sessions = new Map;
|
|
7553
|
+
for (const event of input.events) {
|
|
7554
|
+
sessions.set(event.sessionId, [...sessions.get(event.sessionId) ?? [], event]);
|
|
7555
|
+
}
|
|
7556
|
+
const rows = [...sessions.entries()].sort(([, left], [, right]) => (right.at(-1)?.at ?? 0) - (left.at(-1)?.at ?? 0)).slice(0, 50).map(([sessionId, events]) => {
|
|
7557
|
+
const summary = summarizeVoiceTrace(events);
|
|
7558
|
+
const encoded = encodeURIComponent(sessionId);
|
|
7559
|
+
return `<tr><td>${escapeHtml6(sessionId)}</td><td>${summary.eventCount}</td><td>${summary.turnCount}</td><td>${summary.errorCount}</td><td><a href="${input.basePath}/html?sessionId=${encoded}&redact=true">HTML</a> \xB7 <a href="${input.basePath}/markdown?sessionId=${encoded}&redact=true">Markdown</a> \xB7 <a href="${input.basePath}/json?sessionId=${encoded}&redact=true">JSON</a></td></tr>`;
|
|
7560
|
+
}).join("");
|
|
7561
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml6(input.title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}table{width:100%;border-collapse:collapse;background:white}td,th{border-bottom:1px solid #eee;padding:.7rem;text-align:left}a{color:#9a3412}</style></head><body><main><h1>${escapeHtml6(input.title)}</h1><p>Recent voice trace diagnostics. Exports support filters: sessionId, traceId, turnId, scenarioId, type, provider, status, since, until, limit, redact.</p><table><thead><tr><th>Session</th><th>Events</th><th>Turns</th><th>Errors</th><th>Exports</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
|
|
7562
|
+
};
|
|
7563
|
+
var withRedaction = (events, query, defaultRedact) => {
|
|
7564
|
+
const shouldRedact = query.redact === undefined ? defaultRedact : getBoolean(query.redact);
|
|
7565
|
+
return shouldRedact ? redactVoiceTraceEvents(events, shouldRedact) : events;
|
|
7566
|
+
};
|
|
7567
|
+
var createVoiceDiagnosticsRoutes = (options) => {
|
|
7568
|
+
const path = options.path ?? "/diagnostics";
|
|
7569
|
+
const title = options.title ?? "AbsoluteJS Voice Diagnostics";
|
|
7570
|
+
const routes = new Elysia4({
|
|
7571
|
+
name: options.name ?? "absolutejs-voice-diagnostics"
|
|
7572
|
+
});
|
|
7573
|
+
routes.get(path, async () => {
|
|
7574
|
+
const events = await options.store.list();
|
|
7575
|
+
return new Response(renderDiagnosticsIndex({ basePath: path, events, title }), {
|
|
7576
|
+
headers: {
|
|
7577
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7578
|
+
...options.headers
|
|
7579
|
+
}
|
|
7580
|
+
});
|
|
7581
|
+
});
|
|
7582
|
+
routes.get(`${path}/json`, async ({ query }) => {
|
|
7583
|
+
const events = filterByDiagnosticsQuery(await options.store.list(), query);
|
|
7584
|
+
const redacted = withRedaction(events, query, options.redact);
|
|
7585
|
+
return Response.json({
|
|
7586
|
+
...await exportVoiceTrace({
|
|
7587
|
+
filter: resolveVoiceDiagnosticsTraceFilter(query),
|
|
7588
|
+
redact: false,
|
|
7589
|
+
store: {
|
|
7590
|
+
...options.store,
|
|
7591
|
+
list: async () => redacted
|
|
7592
|
+
}
|
|
7593
|
+
}),
|
|
7594
|
+
filteredCount: events.length,
|
|
7595
|
+
redacted: redacted !== events
|
|
7596
|
+
});
|
|
7597
|
+
});
|
|
7598
|
+
routes.get(`${path}/markdown`, async ({ query }) => {
|
|
7599
|
+
const events = withRedaction(filterByDiagnosticsQuery(await options.store.list(), query), query, options.redact ?? true);
|
|
7600
|
+
const body = buildVoiceDiagnosticsMarkdown(events, {
|
|
7601
|
+
evaluation: options.evaluation,
|
|
7602
|
+
title
|
|
7603
|
+
});
|
|
7604
|
+
return new Response(body, {
|
|
7605
|
+
headers: {
|
|
7606
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
7607
|
+
...options.headers
|
|
7608
|
+
}
|
|
7609
|
+
});
|
|
7610
|
+
});
|
|
7611
|
+
routes.get(`${path}/html`, async ({ query }) => {
|
|
7612
|
+
const events = withRedaction(filterByDiagnosticsQuery(await options.store.list(), query), query, options.redact ?? true);
|
|
7613
|
+
const body = renderVoiceTraceHTML(events, {
|
|
7614
|
+
evaluation: options.evaluation,
|
|
7615
|
+
title
|
|
7616
|
+
});
|
|
7617
|
+
return new Response(body, {
|
|
7618
|
+
headers: {
|
|
7619
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7620
|
+
...options.headers
|
|
7621
|
+
}
|
|
7622
|
+
});
|
|
7623
|
+
});
|
|
7624
|
+
return routes;
|
|
7625
|
+
};
|
|
7626
|
+
// src/sessionReplay.ts
|
|
7627
|
+
import { Elysia as Elysia5 } from "elysia";
|
|
7628
|
+
var getString4 = (value) => typeof value === "string" ? value : undefined;
|
|
7629
|
+
var escapeHtml7 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
7495
7630
|
var increment2 = (record, key) => {
|
|
7496
7631
|
record[key] = (record[key] ?? 0) + 1;
|
|
7497
7632
|
};
|
|
@@ -7520,14 +7655,14 @@ var buildReplayTurns = (events) => {
|
|
|
7520
7655
|
case "turn.transcript":
|
|
7521
7656
|
turn.transcripts.push({
|
|
7522
7657
|
isFinal: event.payload.isFinal === true,
|
|
7523
|
-
text:
|
|
7658
|
+
text: getString4(event.payload.text)
|
|
7524
7659
|
});
|
|
7525
7660
|
break;
|
|
7526
7661
|
case "turn.committed":
|
|
7527
|
-
turn.committedText =
|
|
7662
|
+
turn.committedText = getString4(event.payload.text);
|
|
7528
7663
|
break;
|
|
7529
7664
|
case "turn.assistant": {
|
|
7530
|
-
const text =
|
|
7665
|
+
const text = getString4(event.payload.text);
|
|
7531
7666
|
if (text) {
|
|
7532
7667
|
turn.assistantReplies.push(text);
|
|
7533
7668
|
}
|
|
@@ -7596,7 +7731,7 @@ var summarizeVoiceSessions = async (options = {}) => {
|
|
|
7596
7731
|
let latestOutcome;
|
|
7597
7732
|
let errorCount = 0;
|
|
7598
7733
|
for (const event of sorted) {
|
|
7599
|
-
const provider =
|
|
7734
|
+
const provider = getString4(event.payload.provider);
|
|
7600
7735
|
if (provider) {
|
|
7601
7736
|
providers.add(provider);
|
|
7602
7737
|
}
|
|
@@ -7604,7 +7739,7 @@ var summarizeVoiceSessions = async (options = {}) => {
|
|
|
7604
7739
|
errorCount += 1;
|
|
7605
7740
|
increment2(providerErrors, provider ?? "unknown");
|
|
7606
7741
|
}
|
|
7607
|
-
const outcome =
|
|
7742
|
+
const outcome = getString4(event.payload.outcome);
|
|
7608
7743
|
if (outcome) {
|
|
7609
7744
|
latestOutcome = outcome;
|
|
7610
7745
|
}
|
|
@@ -7650,10 +7785,10 @@ var summarizeVoiceSessions = async (options = {}) => {
|
|
|
7650
7785
|
var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
|
|
7651
7786
|
'<div class="voice-sessions-list">',
|
|
7652
7787
|
...sessions.map((session) => [
|
|
7653
|
-
`<article class="voice-session-card ${
|
|
7788
|
+
`<article class="voice-session-card ${escapeHtml7(session.status)}">`,
|
|
7654
7789
|
'<div class="voice-session-card-header">',
|
|
7655
|
-
`<strong>${
|
|
7656
|
-
`<span>${
|
|
7790
|
+
`<strong>${escapeHtml7(session.sessionId)}</strong>`,
|
|
7791
|
+
`<span>${escapeHtml7(session.status)}</span>`,
|
|
7657
7792
|
"</div>",
|
|
7658
7793
|
"<dl>",
|
|
7659
7794
|
`<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
|
|
@@ -7661,9 +7796,9 @@ var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="v
|
|
|
7661
7796
|
`<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
|
|
7662
7797
|
`<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
|
|
7663
7798
|
"</dl>",
|
|
7664
|
-
session.latestOutcome ? `<p>Outcome: ${
|
|
7665
|
-
session.providers.length ? `<p>Providers: ${session.providers.map(
|
|
7666
|
-
session.replayHref ? `<p><a href="${
|
|
7799
|
+
session.latestOutcome ? `<p>Outcome: ${escapeHtml7(session.latestOutcome)}</p>` : "",
|
|
7800
|
+
session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml7).join(", ")}</p>` : "",
|
|
7801
|
+
session.replayHref ? `<p><a href="${escapeHtml7(session.replayHref)}">Open replay</a></p>` : "",
|
|
7667
7802
|
"</article>"
|
|
7668
7803
|
].join("")),
|
|
7669
7804
|
"</div>"
|
|
@@ -7694,7 +7829,7 @@ var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
|
|
|
7694
7829
|
var createVoiceSessionListRoutes = (options = {}) => {
|
|
7695
7830
|
const path = options.path ?? "/api/voice-sessions";
|
|
7696
7831
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
7697
|
-
const routes = new
|
|
7832
|
+
const routes = new Elysia5({
|
|
7698
7833
|
name: options.name ?? "absolutejs-voice-session-list"
|
|
7699
7834
|
}).get(path, createVoiceSessionsJSONHandler(options));
|
|
7700
7835
|
if (htmlPath) {
|
|
@@ -7722,7 +7857,7 @@ var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
|
|
|
7722
7857
|
var createVoiceSessionReplayRoutes = (options) => {
|
|
7723
7858
|
const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
|
|
7724
7859
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
7725
|
-
const routes = new
|
|
7860
|
+
const routes = new Elysia5({
|
|
7726
7861
|
name: options.name ?? "absolutejs-voice-session-replay"
|
|
7727
7862
|
}).get(path, createVoiceSessionReplayJSONHandler(options));
|
|
7728
7863
|
if (htmlPath) {
|
|
@@ -8858,6 +8993,319 @@ var createGeminiVoiceAssistantModel = (options) => {
|
|
|
8858
8993
|
}
|
|
8859
8994
|
};
|
|
8860
8995
|
};
|
|
8996
|
+
// src/resilienceRoutes.ts
|
|
8997
|
+
import { Elysia as Elysia6 } from "elysia";
|
|
8998
|
+
var escapeHtml8 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
8999
|
+
var getString5 = (value) => typeof value === "string" ? value : undefined;
|
|
9000
|
+
var getNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
9001
|
+
var getBoolean2 = (value) => value === true;
|
|
9002
|
+
var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
|
|
9003
|
+
var listVoiceRoutingEvents = (events) => {
|
|
9004
|
+
const routingEvents = [];
|
|
9005
|
+
for (const event of events) {
|
|
9006
|
+
if (event.type !== "session.error") {
|
|
9007
|
+
continue;
|
|
9008
|
+
}
|
|
9009
|
+
const provider = getString5(event.payload.provider);
|
|
9010
|
+
const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
9011
|
+
if (!provider || !providerStatus) {
|
|
9012
|
+
continue;
|
|
9013
|
+
}
|
|
9014
|
+
const kind = getString5(event.payload.kind);
|
|
9015
|
+
routingEvents.push({
|
|
9016
|
+
at: event.at,
|
|
9017
|
+
attempt: getNumber3(event.payload.attempt),
|
|
9018
|
+
elapsedMs: getNumber3(event.payload.elapsedMs),
|
|
9019
|
+
error: getString5(event.payload.error),
|
|
9020
|
+
fallbackProvider: getString5(event.payload.fallbackProvider),
|
|
9021
|
+
kind: kind === "stt" || kind === "tts" ? kind : "llm",
|
|
9022
|
+
latencyBudgetMs: getNumber3(event.payload.latencyBudgetMs),
|
|
9023
|
+
operation: getString5(event.payload.operation),
|
|
9024
|
+
provider,
|
|
9025
|
+
selectedProvider: getString5(event.payload.selectedProvider),
|
|
9026
|
+
sessionId: event.sessionId,
|
|
9027
|
+
status: providerStatus,
|
|
9028
|
+
timedOut: getBoolean2(event.payload.timedOut),
|
|
9029
|
+
turnId: event.turnId
|
|
9030
|
+
});
|
|
9031
|
+
}
|
|
9032
|
+
return routingEvents.sort((left, right) => right.at - left.at);
|
|
9033
|
+
};
|
|
9034
|
+
var summarizeRoutingEvents = (events) => {
|
|
9035
|
+
const byKind = new Map;
|
|
9036
|
+
let errors = 0;
|
|
9037
|
+
let fallbacks = 0;
|
|
9038
|
+
let timeouts = 0;
|
|
9039
|
+
for (const event of events) {
|
|
9040
|
+
byKind.set(event.kind, (byKind.get(event.kind) ?? 0) + 1);
|
|
9041
|
+
if (event.status === "error") {
|
|
9042
|
+
errors += 1;
|
|
9043
|
+
}
|
|
9044
|
+
if (event.status === "fallback") {
|
|
9045
|
+
fallbacks += 1;
|
|
9046
|
+
}
|
|
9047
|
+
if (event.timedOut) {
|
|
9048
|
+
timeouts += 1;
|
|
9049
|
+
}
|
|
9050
|
+
}
|
|
9051
|
+
return {
|
|
9052
|
+
byKind,
|
|
9053
|
+
errors,
|
|
9054
|
+
fallbacks,
|
|
9055
|
+
timeouts,
|
|
9056
|
+
total: events.length
|
|
9057
|
+
};
|
|
9058
|
+
};
|
|
9059
|
+
var renderProviderCards = (title, providers) => {
|
|
9060
|
+
if (providers.length === 0) {
|
|
9061
|
+
return `<p class="muted">No ${escapeHtml8(title)} provider health yet.</p>`;
|
|
9062
|
+
}
|
|
9063
|
+
return `<div class="provider-grid">${providers.map((provider) => `
|
|
9064
|
+
<article class="card provider ${escapeHtml8(provider.status)}">
|
|
9065
|
+
<div class="card-header">
|
|
9066
|
+
<strong>${escapeHtml8(provider.provider)}</strong>
|
|
9067
|
+
<span>${escapeHtml8(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
|
|
9068
|
+
</div>
|
|
9069
|
+
<dl>
|
|
9070
|
+
<div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
|
|
9071
|
+
<div><dt>Avg latency</dt><dd>${provider.averageElapsedMs ?? 0}ms</dd></div>
|
|
9072
|
+
<div><dt>Errors</dt><dd>${provider.errorCount}</dd></div>
|
|
9073
|
+
<div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
|
|
9074
|
+
<div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
|
|
9075
|
+
</dl>
|
|
9076
|
+
${provider.lastError ? `<p class="muted">${escapeHtml8(provider.lastError)}</p>` : ""}
|
|
9077
|
+
</article>
|
|
9078
|
+
`).join("")}</div>`;
|
|
9079
|
+
};
|
|
9080
|
+
var renderTimeline2 = (events) => {
|
|
9081
|
+
if (events.length === 0) {
|
|
9082
|
+
return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
|
|
9083
|
+
}
|
|
9084
|
+
return `<div class="timeline">${events.slice(0, 40).map((event) => `
|
|
9085
|
+
<article class="card event ${escapeHtml8(event.status ?? "unknown")}">
|
|
9086
|
+
<div class="card-header">
|
|
9087
|
+
<strong>${escapeHtml8(event.kind.toUpperCase())} ${escapeHtml8(event.operation ?? "generate")}</strong>
|
|
9088
|
+
<span>${new Date(event.at).toLocaleString()}</span>
|
|
9089
|
+
</div>
|
|
9090
|
+
<p>
|
|
9091
|
+
<span class="pill">${escapeHtml8(event.status ?? "unknown")}</span>
|
|
9092
|
+
<span class="pill">provider: ${escapeHtml8(event.provider ?? "unknown")}</span>
|
|
9093
|
+
${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml8(event.fallbackProvider)}</span>` : ""}
|
|
9094
|
+
${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
|
|
9095
|
+
</p>
|
|
9096
|
+
<dl>
|
|
9097
|
+
<div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
|
|
9098
|
+
<div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
|
|
9099
|
+
<div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
|
|
9100
|
+
<div><dt>Session</dt><dd>${escapeHtml8(event.sessionId)}</dd></div>
|
|
9101
|
+
</dl>
|
|
9102
|
+
${event.error ? `<p class="muted">${escapeHtml8(event.error)}</p>` : ""}
|
|
9103
|
+
</article>
|
|
9104
|
+
`).join("")}</div>`;
|
|
9105
|
+
};
|
|
9106
|
+
var renderSimulationControls = (kind, simulation) => {
|
|
9107
|
+
if (!simulation) {
|
|
9108
|
+
return "";
|
|
9109
|
+
}
|
|
9110
|
+
const configuredProviders = simulation.providers.filter((provider) => provider.configured !== false);
|
|
9111
|
+
if (configuredProviders.length === 0) {
|
|
9112
|
+
return `<p class="muted">No ${kind.toUpperCase()} providers are configured for simulation.</p>`;
|
|
9113
|
+
}
|
|
9114
|
+
const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
|
|
9115
|
+
const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
|
|
9116
|
+
const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
|
|
9117
|
+
return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml8(pathPrefix)}">
|
|
9118
|
+
<p class="muted">${escapeHtml8(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
|
|
9119
|
+
<div class="simulate-actions">
|
|
9120
|
+
${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml8(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml8(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
|
|
9121
|
+
${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml8(provider.provider)}">Mark ${escapeHtml8(provider.provider)} recovered</button>`).join("")}
|
|
9122
|
+
</div>
|
|
9123
|
+
${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml8(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
|
|
9124
|
+
<pre class="simulate-output" hidden></pre>
|
|
9125
|
+
</div>`;
|
|
9126
|
+
};
|
|
9127
|
+
var renderVoiceResilienceHTML = (input) => {
|
|
9128
|
+
const summary = summarizeRoutingEvents(input.routingEvents);
|
|
9129
|
+
const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml8(kind)}: ${String(count)}</span>`).join("");
|
|
9130
|
+
const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml8(link.href)}">${escapeHtml8(link.label)}</a>`).join(" \xB7 ") : "";
|
|
9131
|
+
return `<!doctype html>
|
|
9132
|
+
<html lang="en">
|
|
9133
|
+
<head>
|
|
9134
|
+
<meta charset="utf-8" />
|
|
9135
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
9136
|
+
<title>${escapeHtml8(input.title ?? "AbsoluteJS Voice Resilience")}</title>
|
|
9137
|
+
<style>
|
|
9138
|
+
:root { color-scheme: dark; }
|
|
9139
|
+
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; }
|
|
9140
|
+
main { display: grid; gap: 16px; margin: 0 auto; max-width: 1180px; }
|
|
9141
|
+
section, .card { background: rgba(19, 22, 27, 0.92); border: 1px solid #27272a; border-radius: 20px; padding: 20px; }
|
|
9142
|
+
.hero { background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(245, 158, 11, 0.12)); }
|
|
9143
|
+
.grid, .provider-grid { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
9144
|
+
.timeline { display: grid; gap: 12px; }
|
|
9145
|
+
.card-header { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
|
|
9146
|
+
.card-header strong { font-size: 1.05rem; }
|
|
9147
|
+
.metric strong { display: block; font-size: 2rem; margin-top: 6px; }
|
|
9148
|
+
.muted, dt, span { color: #a1a1aa; }
|
|
9149
|
+
dl { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
9150
|
+
dl div { background: #0f1217; border: 1px solid #27272a; border-radius: 12px; padding: 10px; }
|
|
9151
|
+
dd { font-weight: 800; margin: 4px 0 0; }
|
|
9152
|
+
.pill { background: #0f1217; border: 1px solid #3f3f46; border-radius: 999px; color: #d4d4d8; display: inline-flex; margin: 3px 4px 3px 0; padding: 5px 9px; }
|
|
9153
|
+
.danger { border-color: rgba(239, 68, 68, 0.75); color: #fecaca; }
|
|
9154
|
+
.event.error { border-color: rgba(239, 68, 68, 0.7); }
|
|
9155
|
+
.event.fallback { border-color: rgba(245, 158, 11, 0.7); }
|
|
9156
|
+
.event.success, .provider.healthy { border-color: rgba(34, 197, 94, 0.5); }
|
|
9157
|
+
.provider.suppressed, .provider.degraded, .provider.rate-limited { border-color: rgba(239, 68, 68, 0.7); }
|
|
9158
|
+
.provider.recoverable { border-color: rgba(59, 130, 246, 0.7); }
|
|
9159
|
+
button { background: #f59e0b; border: 0; border-radius: 999px; color: #111827; cursor: pointer; font-weight: 800; padding: 10px 14px; }
|
|
9160
|
+
button:disabled { cursor: not-allowed; opacity: 0.45; }
|
|
9161
|
+
.simulate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
|
|
9162
|
+
.simulate-output { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #d4d4d8; overflow: auto; padding: 12px; white-space: pre-wrap; }
|
|
9163
|
+
a { color: #f59e0b; }
|
|
9164
|
+
@media (max-width: 850px) { .grid, .provider-grid, dl { grid-template-columns: 1fr; } }
|
|
9165
|
+
</style>
|
|
9166
|
+
</head>
|
|
9167
|
+
<body>
|
|
9168
|
+
<main>
|
|
9169
|
+
<section class="hero">
|
|
9170
|
+
<h1>Provider routing and resilience</h1>
|
|
9171
|
+
<p>One view for the production reliability story: LLM failover, STT/TTS routing, latency budgets, timeouts, and fallback decisions.</p>
|
|
9172
|
+
${links ? `<p>${links}</p>` : ""}
|
|
9173
|
+
<p>${kindCounts || '<span class="pill">No routing events yet</span>'}</p>
|
|
9174
|
+
</section>
|
|
9175
|
+
<section class="grid">
|
|
9176
|
+
<article class="card metric"><span>Total routing events</span><strong>${summary.total}</strong></article>
|
|
9177
|
+
<article class="card metric"><span>Fallbacks</span><strong>${summary.fallbacks}</strong></article>
|
|
9178
|
+
<article class="card metric"><span>Errors</span><strong>${summary.errors}</strong></article>
|
|
9179
|
+
<article class="card metric"><span>Timeouts</span><strong>${summary.timeouts}</strong></article>
|
|
9180
|
+
</section>
|
|
9181
|
+
<section>
|
|
9182
|
+
<h2>LLM provider health</h2>
|
|
9183
|
+
${renderProviderCards("LLM", input.llmProviderHealth)}
|
|
9184
|
+
</section>
|
|
9185
|
+
<section>
|
|
9186
|
+
<h2>STT provider health</h2>
|
|
9187
|
+
${renderSimulationControls("stt", input.sttSimulation)}
|
|
9188
|
+
${renderProviderCards("STT", input.sttProviderHealth)}
|
|
9189
|
+
</section>
|
|
9190
|
+
<section>
|
|
9191
|
+
<h2>TTS provider health</h2>
|
|
9192
|
+
${renderSimulationControls("tts", input.ttsSimulation)}
|
|
9193
|
+
${renderProviderCards("TTS", input.ttsProviderHealth)}
|
|
9194
|
+
</section>
|
|
9195
|
+
<section>
|
|
9196
|
+
<h2>Routing timeline</h2>
|
|
9197
|
+
${renderTimeline2(input.routingEvents)}
|
|
9198
|
+
</section>
|
|
9199
|
+
</main>
|
|
9200
|
+
<script>
|
|
9201
|
+
const showResult = (panel, result) => {
|
|
9202
|
+
const output = panel.querySelector(".simulate-output");
|
|
9203
|
+
if (!output) return;
|
|
9204
|
+
output.hidden = false;
|
|
9205
|
+
output.textContent = JSON.stringify(result, null, 2);
|
|
9206
|
+
};
|
|
9207
|
+
document.querySelectorAll("[data-sim-prefix]").forEach((panel) => {
|
|
9208
|
+
const prefix = panel.getAttribute("data-sim-prefix");
|
|
9209
|
+
panel.querySelectorAll("[data-provider-fail]").forEach((button) => {
|
|
9210
|
+
button.addEventListener("click", async () => {
|
|
9211
|
+
const provider = button.getAttribute("data-provider-fail");
|
|
9212
|
+
const response = await fetch(prefix + "/failure?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
|
|
9213
|
+
showResult(panel, await response.json());
|
|
9214
|
+
if (response.ok) window.setTimeout(() => window.location.reload(), 450);
|
|
9215
|
+
});
|
|
9216
|
+
});
|
|
9217
|
+
panel.querySelectorAll("[data-provider-recover]").forEach((button) => {
|
|
9218
|
+
button.addEventListener("click", async () => {
|
|
9219
|
+
const provider = button.getAttribute("data-provider-recover");
|
|
9220
|
+
const response = await fetch(prefix + "/recovery?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
|
|
9221
|
+
showResult(panel, await response.json());
|
|
9222
|
+
if (response.ok) window.setTimeout(() => window.location.reload(), 450);
|
|
9223
|
+
});
|
|
9224
|
+
});
|
|
9225
|
+
});
|
|
9226
|
+
</script>
|
|
9227
|
+
</body>
|
|
9228
|
+
</html>`;
|
|
9229
|
+
};
|
|
9230
|
+
var providerFromQuery = (value, providers) => typeof value === "string" && providers.some((provider) => provider.provider === value && provider.configured !== false) ? value : undefined;
|
|
9231
|
+
var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
|
|
9232
|
+
if (!simulation) {
|
|
9233
|
+
return routes;
|
|
9234
|
+
}
|
|
9235
|
+
const pathPrefix = simulation.pathPrefix ?? defaultPathPrefix;
|
|
9236
|
+
routes.post(`${pathPrefix}/failure`, async ({ query, set }) => {
|
|
9237
|
+
const provider = providerFromQuery(query.provider, simulation.providers);
|
|
9238
|
+
if (!provider) {
|
|
9239
|
+
set.status = 400;
|
|
9240
|
+
return {
|
|
9241
|
+
error: "Provider is not configured for simulation."
|
|
9242
|
+
};
|
|
9243
|
+
}
|
|
9244
|
+
if (simulation.failureProviders && !simulation.failureProviders.includes(provider)) {
|
|
9245
|
+
set.status = 400;
|
|
9246
|
+
return {
|
|
9247
|
+
error: `${provider} is not configured for failure simulation.`
|
|
9248
|
+
};
|
|
9249
|
+
}
|
|
9250
|
+
if (simulation.fallbackRequiredProvider && !simulation.providers.some((entry) => entry.provider === simulation.fallbackRequiredProvider && entry.configured !== false)) {
|
|
9251
|
+
set.status = 400;
|
|
9252
|
+
return {
|
|
9253
|
+
error: simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} before simulating fallback.`
|
|
9254
|
+
};
|
|
9255
|
+
}
|
|
9256
|
+
return simulation.run(provider, "failure");
|
|
9257
|
+
});
|
|
9258
|
+
routes.post(`${pathPrefix}/recovery`, async ({ query, set }) => {
|
|
9259
|
+
const provider = providerFromQuery(query.provider, simulation.providers);
|
|
9260
|
+
if (!provider) {
|
|
9261
|
+
set.status = 400;
|
|
9262
|
+
return {
|
|
9263
|
+
error: "Provider is not configured for simulation."
|
|
9264
|
+
};
|
|
9265
|
+
}
|
|
9266
|
+
return simulation.run(provider, "recovery");
|
|
9267
|
+
});
|
|
9268
|
+
return routes;
|
|
9269
|
+
};
|
|
9270
|
+
var createVoiceResilienceRoutes = (options) => {
|
|
9271
|
+
const path = options.path ?? "/resilience";
|
|
9272
|
+
const routes = new Elysia6({
|
|
9273
|
+
name: options.name ?? "absolutejs-voice-resilience"
|
|
9274
|
+
}).get(path, async () => {
|
|
9275
|
+
const events = await options.store.list();
|
|
9276
|
+
const sttEvents = events.filter((event) => event.payload.kind === "stt");
|
|
9277
|
+
const ttsEvents = events.filter((event) => event.payload.kind === "tts");
|
|
9278
|
+
const data = {
|
|
9279
|
+
links: options.links,
|
|
9280
|
+
llmProviderHealth: await summarizeVoiceProviderHealth({
|
|
9281
|
+
events,
|
|
9282
|
+
providers: options.llmProviders ?? []
|
|
9283
|
+
}),
|
|
9284
|
+
routingEvents: listVoiceRoutingEvents(events),
|
|
9285
|
+
sttProviderHealth: await summarizeVoiceProviderHealth({
|
|
9286
|
+
events: sttEvents,
|
|
9287
|
+
providers: options.sttProviders ?? []
|
|
9288
|
+
}),
|
|
9289
|
+
sttSimulation: options.sttSimulation,
|
|
9290
|
+
title: options.title,
|
|
9291
|
+
ttsProviderHealth: await summarizeVoiceProviderHealth({
|
|
9292
|
+
events: ttsEvents,
|
|
9293
|
+
providers: options.ttsProviders ?? []
|
|
9294
|
+
}),
|
|
9295
|
+
ttsSimulation: options.ttsSimulation
|
|
9296
|
+
};
|
|
9297
|
+
const body = await (options.render ?? renderVoiceResilienceHTML)(data);
|
|
9298
|
+
return new Response(body, {
|
|
9299
|
+
headers: {
|
|
9300
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9301
|
+
...options.headers
|
|
9302
|
+
}
|
|
9303
|
+
});
|
|
9304
|
+
});
|
|
9305
|
+
registerSimulationRoutes(routes, options.sttSimulation, "/api/stt-simulate");
|
|
9306
|
+
registerSimulationRoutes(routes, options.ttsSimulation, "/api/tts-simulate");
|
|
9307
|
+
return routes;
|
|
9308
|
+
};
|
|
8861
9309
|
// src/providerAdapters.ts
|
|
8862
9310
|
class VoiceIOProviderTimeoutError extends Error {
|
|
8863
9311
|
provider;
|
|
@@ -9702,7 +10150,7 @@ var createVoiceMemoryStore = () => {
|
|
|
9702
10150
|
return { get, getOrCreate, list, remove, set };
|
|
9703
10151
|
};
|
|
9704
10152
|
// src/opsWebhook.ts
|
|
9705
|
-
import { Elysia as
|
|
10153
|
+
import { Elysia as Elysia7 } from "elysia";
|
|
9706
10154
|
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
9707
10155
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
9708
10156
|
const encoder = new TextEncoder;
|
|
@@ -9832,7 +10280,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
|
9832
10280
|
};
|
|
9833
10281
|
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
9834
10282
|
const path = options.path ?? "/api/voice-ops/webhook";
|
|
9835
|
-
return new
|
|
10283
|
+
return new Elysia7().post(path, async ({ body, request, set }) => {
|
|
9836
10284
|
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
9837
10285
|
if (options.signingSecret) {
|
|
9838
10286
|
const verification = await verifyVoiceOpsWebhookSignature({
|
|
@@ -9865,9 +10313,9 @@ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
|
9865
10313
|
});
|
|
9866
10314
|
};
|
|
9867
10315
|
// src/handoffHealth.ts
|
|
9868
|
-
import { Elysia as
|
|
9869
|
-
var
|
|
9870
|
-
var
|
|
10316
|
+
import { Elysia as Elysia8 } from "elysia";
|
|
10317
|
+
var escapeHtml9 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10318
|
+
var getString6 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9871
10319
|
var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
|
|
9872
10320
|
var increment3 = (record, key) => {
|
|
9873
10321
|
record[key] = (record[key] ?? 0) + 1;
|
|
@@ -9875,11 +10323,11 @@ var increment3 = (record, key) => {
|
|
|
9875
10323
|
var normalizeDelivery = (adapterId, value) => {
|
|
9876
10324
|
const record = value && typeof value === "object" ? value : {};
|
|
9877
10325
|
return {
|
|
9878
|
-
adapterId:
|
|
9879
|
-
adapterKind:
|
|
10326
|
+
adapterId: getString6(record.adapterId) ?? adapterId,
|
|
10327
|
+
adapterKind: getString6(record.adapterKind),
|
|
9880
10328
|
deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
|
|
9881
|
-
deliveredTo:
|
|
9882
|
-
error:
|
|
10329
|
+
deliveredTo: getString6(record.deliveredTo),
|
|
10330
|
+
error: getString6(record.error),
|
|
9883
10331
|
status: isStatus(record.status) ? record.status : "failed"
|
|
9884
10332
|
};
|
|
9885
10333
|
};
|
|
@@ -9913,13 +10361,13 @@ var summarizeVoiceHandoffHealth = async (options = {}) => {
|
|
|
9913
10361
|
const status = isStatus(event.payload.status) ? event.payload.status : "failed";
|
|
9914
10362
|
const deliveries = normalizeDeliveries(event.payload);
|
|
9915
10363
|
const item = {
|
|
9916
|
-
action:
|
|
10364
|
+
action: getString6(event.payload.action),
|
|
9917
10365
|
at: event.at,
|
|
9918
10366
|
deliveries,
|
|
9919
|
-
reason:
|
|
10367
|
+
reason: getString6(event.payload.reason),
|
|
9920
10368
|
sessionId: event.sessionId,
|
|
9921
10369
|
status,
|
|
9922
|
-
target:
|
|
10370
|
+
target: getString6(event.payload.target)
|
|
9923
10371
|
};
|
|
9924
10372
|
return {
|
|
9925
10373
|
...item,
|
|
@@ -9984,10 +10432,10 @@ var renderActionSummary = (summary) => {
|
|
|
9984
10432
|
return [
|
|
9985
10433
|
'<section class="voice-handoff-health-columns">',
|
|
9986
10434
|
"<article><h3>Actions</h3>",
|
|
9987
|
-
actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${
|
|
10435
|
+
actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml9(action)}: ${String(count)}</li>`).join("")}</ul>`,
|
|
9988
10436
|
"</article>",
|
|
9989
10437
|
"<article><h3>Adapters</h3>",
|
|
9990
|
-
adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${
|
|
10438
|
+
adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml9(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
|
|
9991
10439
|
"</article>",
|
|
9992
10440
|
"</section>"
|
|
9993
10441
|
].join("");
|
|
@@ -10001,22 +10449,22 @@ var renderVoiceHandoffHealthHTML = (summary) => [
|
|
|
10001
10449
|
summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
|
|
10002
10450
|
'<div class="voice-handoff-health-events">',
|
|
10003
10451
|
...summary.events.map((event) => [
|
|
10004
|
-
`<article class="${
|
|
10452
|
+
`<article class="${escapeHtml9(event.status)}">`,
|
|
10005
10453
|
'<div class="voice-handoff-health-event-header">',
|
|
10006
|
-
`<strong>${
|
|
10007
|
-
`<span>${
|
|
10454
|
+
`<strong>${escapeHtml9(event.action ?? "handoff")}</strong>`,
|
|
10455
|
+
`<span>${escapeHtml9(event.status)}</span>`,
|
|
10008
10456
|
"</div>",
|
|
10009
|
-
`<p><small>${
|
|
10010
|
-
event.target ? `<p>Target: ${
|
|
10011
|
-
event.reason ? `<p>Reason: ${
|
|
10457
|
+
`<p><small>${escapeHtml9(event.sessionId)}</small></p>`,
|
|
10458
|
+
event.target ? `<p>Target: ${escapeHtml9(event.target)}</p>` : "",
|
|
10459
|
+
event.reason ? `<p>Reason: ${escapeHtml9(event.reason)}</p>` : "",
|
|
10012
10460
|
event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
|
|
10013
10461
|
"<li>",
|
|
10014
|
-
`${
|
|
10015
|
-
delivery.deliveredTo ? ` to ${
|
|
10016
|
-
delivery.error ? ` (${
|
|
10462
|
+
`${escapeHtml9(delivery.adapterId)}: ${escapeHtml9(delivery.status)}`,
|
|
10463
|
+
delivery.deliveredTo ? ` to ${escapeHtml9(delivery.deliveredTo)}` : "",
|
|
10464
|
+
delivery.error ? ` (${escapeHtml9(delivery.error)})` : "",
|
|
10017
10465
|
"</li>"
|
|
10018
10466
|
].join("")).join("")}</ul>` : "",
|
|
10019
|
-
event.replayHref ? `<p><a href="${
|
|
10467
|
+
event.replayHref ? `<p><a href="${escapeHtml9(event.replayHref)}">Open replay</a></p>` : "",
|
|
10020
10468
|
"</article>"
|
|
10021
10469
|
].join("")),
|
|
10022
10470
|
"</div>"
|
|
@@ -10048,7 +10496,7 @@ var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) =>
|
|
|
10048
10496
|
var createVoiceHandoffHealthRoutes = (options = {}) => {
|
|
10049
10497
|
const path = options.path ?? "/api/voice-handoffs";
|
|
10050
10498
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
10051
|
-
const routes = new
|
|
10499
|
+
const routes = new Elysia8({
|
|
10052
10500
|
name: options.name ?? "absolutejs-voice-handoff-health"
|
|
10053
10501
|
}).get(path, createVoiceHandoffHealthJSONHandler(options));
|
|
10054
10502
|
if (htmlPath) {
|
|
@@ -12194,6 +12642,7 @@ export {
|
|
|
12194
12642
|
resolveVoiceOpsTaskAssignment,
|
|
12195
12643
|
resolveVoiceOpsTaskAgeBucket,
|
|
12196
12644
|
resolveVoiceOpsPreset,
|
|
12645
|
+
resolveVoiceDiagnosticsTraceFilter,
|
|
12197
12646
|
resolveVoiceAssistantMemoryNamespace,
|
|
12198
12647
|
resolveTurnDetectionConfig,
|
|
12199
12648
|
resolveAudioConditioningConfig,
|
|
@@ -12202,6 +12651,7 @@ export {
|
|
|
12202
12651
|
renderVoiceTraceMarkdown,
|
|
12203
12652
|
renderVoiceTraceHTML,
|
|
12204
12653
|
renderVoiceSessionsHTML,
|
|
12654
|
+
renderVoiceResilienceHTML,
|
|
12205
12655
|
renderVoiceProviderHealthHTML,
|
|
12206
12656
|
renderVoiceHandoffHealthHTML,
|
|
12207
12657
|
renderVoiceCallReviewMarkdown,
|
|
@@ -12214,6 +12664,7 @@ export {
|
|
|
12214
12664
|
pruneVoiceTraceEvents,
|
|
12215
12665
|
matchesVoiceOpsTaskAssignmentRule,
|
|
12216
12666
|
markVoiceOpsTaskSLABreached,
|
|
12667
|
+
listVoiceRoutingEvents,
|
|
12217
12668
|
listVoiceOpsTasks,
|
|
12218
12669
|
isVoiceOpsTaskOverdue,
|
|
12219
12670
|
heartbeatVoiceOpsTask,
|
|
@@ -12269,6 +12720,7 @@ export {
|
|
|
12269
12720
|
createVoiceSQLiteExternalObjectMapStore,
|
|
12270
12721
|
createVoiceS3ReviewStore,
|
|
12271
12722
|
createVoiceReviewSavedEvent,
|
|
12723
|
+
createVoiceResilienceRoutes,
|
|
12272
12724
|
createVoiceRedisTaskLeaseCoordinator,
|
|
12273
12725
|
createVoiceRedisIdempotencyStore,
|
|
12274
12726
|
createVoiceProviderRouter,
|
|
@@ -12324,6 +12776,7 @@ export {
|
|
|
12324
12776
|
createVoiceExternalObjectMapId,
|
|
12325
12777
|
createVoiceExternalObjectMap,
|
|
12326
12778
|
createVoiceExperiment,
|
|
12779
|
+
createVoiceDiagnosticsRoutes,
|
|
12327
12780
|
createVoiceCallReviewRecorder,
|
|
12328
12781
|
createVoiceCallReviewFromSession,
|
|
12329
12782
|
createVoiceCallReviewFromLiveTelephonyReport,
|
|
@@ -12359,6 +12812,7 @@ export {
|
|
|
12359
12812
|
buildVoiceTraceReplay,
|
|
12360
12813
|
buildVoiceOpsTaskFromSLABreach,
|
|
12361
12814
|
buildVoiceOpsTaskFromReview,
|
|
12815
|
+
buildVoiceDiagnosticsMarkdown,
|
|
12362
12816
|
assignVoiceOpsTask,
|
|
12363
12817
|
applyVoiceOpsTaskPolicy,
|
|
12364
12818
|
applyVoiceOpsTaskAssignmentRule,
|
|
@@ -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
|
+
}>;
|