@absolutejs/voice 0.0.22-beta.37 → 0.0.22-beta.39
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 +539 -246
- package/dist/qualityRoutes.d.ts +69 -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 { createVoiceQualityRoutes, evaluateVoiceQuality, renderVoiceQualityHTML } from './qualityRoutes';
|
|
10
12
|
export { createVoiceResilienceRoutes, listVoiceRoutingEvents, renderVoiceResilienceHTML } from './resilienceRoutes';
|
|
11
13
|
export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
|
|
12
14
|
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';
|
|
@@ -35,9 +37,11 @@ export { createVoiceCallReviewFromLiveTelephonyReport, createVoiceCallReviewReco
|
|
|
35
37
|
export type { VoiceAssistant, VoiceAssistantArtifactPlan, VoiceAssistantExperiment, VoiceAssistantExperimentOptions, VoiceAssistantGuardrailInput, VoiceAssistantGuardrails, VoiceAssistantMemoryLifecycle, VoiceAssistantMemoryLifecycleInput, VoiceAssistantOptions, VoiceAssistantOutputGuardrailInput, VoiceAssistantPreset, VoiceAssistantRunsSummary, VoiceAssistantRunSummary, VoiceAssistantVariant } from './assistant';
|
|
36
38
|
export type { VoiceAssistantHealthFailure, VoiceAssistantHealthHTMLHandlerOptions, VoiceAssistantHealthRoutesOptions, VoiceAssistantHealthSummary, VoiceAssistantHealthSummaryOptions } from './assistantHealth';
|
|
37
39
|
export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssistantMemoryOptions, VoiceAssistantMemoryRecord, VoiceAssistantMemoryStore } from './assistantMemory';
|
|
40
|
+
export type { VoiceDiagnosticsRoutesOptions } from './diagnosticsRoutes';
|
|
38
41
|
export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
|
|
39
42
|
export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
|
|
40
43
|
export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
|
|
44
|
+
export type { VoiceQualityMetric, VoiceQualityReport, VoiceQualityRoutesOptions, VoiceQualityStatus, VoiceQualityThresholds } from './qualityRoutes';
|
|
41
45
|
export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingEvent, VoiceRoutingEventKind } from './resilienceRoutes';
|
|
42
46
|
export type { VoiceIOProviderRouterEvent, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
|
|
43
47
|
export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
|
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,12 +8993,356 @@ var createGeminiVoiceAssistantModel = (options) => {
|
|
|
8858
8993
|
}
|
|
8859
8994
|
};
|
|
8860
8995
|
};
|
|
8996
|
+
// src/qualityRoutes.ts
|
|
8997
|
+
import { Elysia as Elysia7 } from "elysia";
|
|
8998
|
+
|
|
8999
|
+
// src/handoffHealth.ts
|
|
9000
|
+
import { Elysia as Elysia6 } from "elysia";
|
|
9001
|
+
var escapeHtml8 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9002
|
+
var getString5 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9003
|
+
var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
|
|
9004
|
+
var increment3 = (record, key) => {
|
|
9005
|
+
record[key] = (record[key] ?? 0) + 1;
|
|
9006
|
+
};
|
|
9007
|
+
var normalizeDelivery = (adapterId, value) => {
|
|
9008
|
+
const record = value && typeof value === "object" ? value : {};
|
|
9009
|
+
return {
|
|
9010
|
+
adapterId: getString5(record.adapterId) ?? adapterId,
|
|
9011
|
+
adapterKind: getString5(record.adapterKind),
|
|
9012
|
+
deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
|
|
9013
|
+
deliveredTo: getString5(record.deliveredTo),
|
|
9014
|
+
error: getString5(record.error),
|
|
9015
|
+
status: isStatus(record.status) ? record.status : "failed"
|
|
9016
|
+
};
|
|
9017
|
+
};
|
|
9018
|
+
var normalizeDeliveries = (payload) => {
|
|
9019
|
+
const deliveries = payload.deliveries;
|
|
9020
|
+
if (!deliveries || typeof deliveries !== "object") {
|
|
9021
|
+
return [];
|
|
9022
|
+
}
|
|
9023
|
+
return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
|
|
9024
|
+
};
|
|
9025
|
+
var resolveReplayHref = (event, replayHref) => {
|
|
9026
|
+
if (replayHref === false) {
|
|
9027
|
+
return;
|
|
9028
|
+
}
|
|
9029
|
+
if (typeof replayHref === "function") {
|
|
9030
|
+
return replayHref(event);
|
|
9031
|
+
}
|
|
9032
|
+
return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
|
|
9033
|
+
};
|
|
9034
|
+
var summarizeVoiceHandoffHealth = async (options = {}) => {
|
|
9035
|
+
const sourceEvents = options.events ?? await options.store?.list() ?? [];
|
|
9036
|
+
const search = options.q?.trim().toLowerCase();
|
|
9037
|
+
const byAction = {};
|
|
9038
|
+
const byAdapter = {};
|
|
9039
|
+
const byStatus = {
|
|
9040
|
+
delivered: 0,
|
|
9041
|
+
failed: 0,
|
|
9042
|
+
skipped: 0
|
|
9043
|
+
};
|
|
9044
|
+
const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
|
|
9045
|
+
const status = isStatus(event.payload.status) ? event.payload.status : "failed";
|
|
9046
|
+
const deliveries = normalizeDeliveries(event.payload);
|
|
9047
|
+
const item = {
|
|
9048
|
+
action: getString5(event.payload.action),
|
|
9049
|
+
at: event.at,
|
|
9050
|
+
deliveries,
|
|
9051
|
+
reason: getString5(event.payload.reason),
|
|
9052
|
+
sessionId: event.sessionId,
|
|
9053
|
+
status,
|
|
9054
|
+
target: getString5(event.payload.target)
|
|
9055
|
+
};
|
|
9056
|
+
return {
|
|
9057
|
+
...item,
|
|
9058
|
+
replayHref: resolveReplayHref(item, options.replayHref)
|
|
9059
|
+
};
|
|
9060
|
+
}).filter((event) => {
|
|
9061
|
+
if (options.status && options.status !== "all" && event.status !== options.status) {
|
|
9062
|
+
return false;
|
|
9063
|
+
}
|
|
9064
|
+
if (!search) {
|
|
9065
|
+
return true;
|
|
9066
|
+
}
|
|
9067
|
+
return [
|
|
9068
|
+
event.action,
|
|
9069
|
+
event.reason,
|
|
9070
|
+
event.sessionId,
|
|
9071
|
+
event.status,
|
|
9072
|
+
event.target,
|
|
9073
|
+
...event.deliveries.flatMap((delivery) => [
|
|
9074
|
+
delivery.adapterId,
|
|
9075
|
+
delivery.adapterKind,
|
|
9076
|
+
delivery.deliveredTo,
|
|
9077
|
+
delivery.error,
|
|
9078
|
+
delivery.status
|
|
9079
|
+
])
|
|
9080
|
+
].some((value) => value?.toLowerCase().includes(search));
|
|
9081
|
+
}).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
|
|
9082
|
+
for (const event of events) {
|
|
9083
|
+
byStatus[event.status] += 1;
|
|
9084
|
+
if (event.action) {
|
|
9085
|
+
increment3(byAction, event.action);
|
|
9086
|
+
}
|
|
9087
|
+
for (const delivery of event.deliveries) {
|
|
9088
|
+
byAdapter[delivery.adapterId] ??= {
|
|
9089
|
+
delivered: 0,
|
|
9090
|
+
failed: 0,
|
|
9091
|
+
skipped: 0
|
|
9092
|
+
};
|
|
9093
|
+
byAdapter[delivery.adapterId][delivery.status] += 1;
|
|
9094
|
+
}
|
|
9095
|
+
}
|
|
9096
|
+
return {
|
|
9097
|
+
byAction,
|
|
9098
|
+
byAdapter,
|
|
9099
|
+
byStatus,
|
|
9100
|
+
events,
|
|
9101
|
+
failed: byStatus.failed,
|
|
9102
|
+
total: events.length
|
|
9103
|
+
};
|
|
9104
|
+
};
|
|
9105
|
+
var renderMetricGrid = (summary) => [
|
|
9106
|
+
'<section class="voice-handoff-health-grid">',
|
|
9107
|
+
`<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
|
|
9108
|
+
`<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
|
|
9109
|
+
`<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
|
|
9110
|
+
`<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
|
|
9111
|
+
"</section>"
|
|
9112
|
+
].join("");
|
|
9113
|
+
var renderActionSummary = (summary) => {
|
|
9114
|
+
const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
|
|
9115
|
+
const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
|
|
9116
|
+
return [
|
|
9117
|
+
'<section class="voice-handoff-health-columns">',
|
|
9118
|
+
"<article><h3>Actions</h3>",
|
|
9119
|
+
actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml8(action)}: ${String(count)}</li>`).join("")}</ul>`,
|
|
9120
|
+
"</article>",
|
|
9121
|
+
"<article><h3>Adapters</h3>",
|
|
9122
|
+
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>`,
|
|
9123
|
+
"</article>",
|
|
9124
|
+
"</section>"
|
|
9125
|
+
].join("");
|
|
9126
|
+
};
|
|
9127
|
+
var renderVoiceHandoffHealthHTML = (summary) => [
|
|
9128
|
+
'<div class="voice-handoff-health">',
|
|
9129
|
+
renderMetricGrid(summary),
|
|
9130
|
+
renderActionSummary(summary),
|
|
9131
|
+
"<section>",
|
|
9132
|
+
"<h3>Recent Handoffs</h3>",
|
|
9133
|
+
summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
|
|
9134
|
+
'<div class="voice-handoff-health-events">',
|
|
9135
|
+
...summary.events.map((event) => [
|
|
9136
|
+
`<article class="${escapeHtml8(event.status)}">`,
|
|
9137
|
+
'<div class="voice-handoff-health-event-header">',
|
|
9138
|
+
`<strong>${escapeHtml8(event.action ?? "handoff")}</strong>`,
|
|
9139
|
+
`<span>${escapeHtml8(event.status)}</span>`,
|
|
9140
|
+
"</div>",
|
|
9141
|
+
`<p><small>${escapeHtml8(event.sessionId)}</small></p>`,
|
|
9142
|
+
event.target ? `<p>Target: ${escapeHtml8(event.target)}</p>` : "",
|
|
9143
|
+
event.reason ? `<p>Reason: ${escapeHtml8(event.reason)}</p>` : "",
|
|
9144
|
+
event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
|
|
9145
|
+
"<li>",
|
|
9146
|
+
`${escapeHtml8(delivery.adapterId)}: ${escapeHtml8(delivery.status)}`,
|
|
9147
|
+
delivery.deliveredTo ? ` to ${escapeHtml8(delivery.deliveredTo)}` : "",
|
|
9148
|
+
delivery.error ? ` (${escapeHtml8(delivery.error)})` : "",
|
|
9149
|
+
"</li>"
|
|
9150
|
+
].join("")).join("")}</ul>` : "",
|
|
9151
|
+
event.replayHref ? `<p><a href="${escapeHtml8(event.replayHref)}">Open replay</a></p>` : "",
|
|
9152
|
+
"</article>"
|
|
9153
|
+
].join("")),
|
|
9154
|
+
"</div>"
|
|
9155
|
+
].join(""),
|
|
9156
|
+
"</section>",
|
|
9157
|
+
"</div>"
|
|
9158
|
+
].join("");
|
|
9159
|
+
var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
|
|
9160
|
+
...options,
|
|
9161
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
9162
|
+
q: query?.q ?? options.q,
|
|
9163
|
+
status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
|
|
9164
|
+
});
|
|
9165
|
+
var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
|
|
9166
|
+
const summary = await summarizeVoiceHandoffHealth({
|
|
9167
|
+
...options,
|
|
9168
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
9169
|
+
q: query?.q ?? options.q,
|
|
9170
|
+
status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
|
|
9171
|
+
});
|
|
9172
|
+
const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
|
|
9173
|
+
return new Response(body, {
|
|
9174
|
+
headers: {
|
|
9175
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9176
|
+
...options.headers
|
|
9177
|
+
}
|
|
9178
|
+
});
|
|
9179
|
+
};
|
|
9180
|
+
var createVoiceHandoffHealthRoutes = (options = {}) => {
|
|
9181
|
+
const path = options.path ?? "/api/voice-handoffs";
|
|
9182
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
9183
|
+
const routes = new Elysia6({
|
|
9184
|
+
name: options.name ?? "absolutejs-voice-handoff-health"
|
|
9185
|
+
}).get(path, createVoiceHandoffHealthJSONHandler(options));
|
|
9186
|
+
if (htmlPath) {
|
|
9187
|
+
routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
|
|
9188
|
+
}
|
|
9189
|
+
return routes;
|
|
9190
|
+
};
|
|
9191
|
+
|
|
9192
|
+
// src/qualityRoutes.ts
|
|
9193
|
+
var DEFAULT_THRESHOLDS = {
|
|
9194
|
+
maxDuplicateTurnRate: 0,
|
|
9195
|
+
maxEmptyTurnRate: 0.02,
|
|
9196
|
+
maxHandoffFailureRate: 0,
|
|
9197
|
+
maxMissingAssistantReplyRate: 0.05,
|
|
9198
|
+
maxProviderAverageLatencyMs: 3000,
|
|
9199
|
+
maxProviderErrorRate: 0.05,
|
|
9200
|
+
maxProviderFallbackRate: 0.25,
|
|
9201
|
+
maxProviderTimeoutRate: 0.03
|
|
9202
|
+
};
|
|
9203
|
+
var getString6 = (value) => typeof value === "string" ? value : undefined;
|
|
9204
|
+
var getNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
9205
|
+
var rate = (count, total) => count / Math.max(1, total);
|
|
9206
|
+
var roundMetric2 = (value) => Math.round(value * 1e4) / 1e4;
|
|
9207
|
+
var createMetric = (input) => ({
|
|
9208
|
+
...input,
|
|
9209
|
+
actual: roundMetric2(input.actual),
|
|
9210
|
+
pass: input.actual <= input.threshold
|
|
9211
|
+
});
|
|
9212
|
+
var evaluateVoiceQuality = async (input) => {
|
|
9213
|
+
const events = filterVoiceTraceEvents(input.events ?? await input.store?.list() ?? []);
|
|
9214
|
+
const thresholds = {
|
|
9215
|
+
...DEFAULT_THRESHOLDS,
|
|
9216
|
+
...input.thresholds
|
|
9217
|
+
};
|
|
9218
|
+
const committedTurns = events.filter((event) => event.type === "turn.committed");
|
|
9219
|
+
const assistantReplies = events.filter((event) => event.type === "turn.assistant");
|
|
9220
|
+
const sessionIdsWithAssistantReply = new Set(assistantReplies.map((event) => event.sessionId));
|
|
9221
|
+
const sessionsWithTurns = new Set(committedTurns.map((event) => event.sessionId));
|
|
9222
|
+
const emptyTurns = committedTurns.filter((event) => !getString6(event.payload.text)?.trim());
|
|
9223
|
+
const turnTextsBySession = new Map;
|
|
9224
|
+
let duplicateTurns = 0;
|
|
9225
|
+
for (const turn of committedTurns) {
|
|
9226
|
+
const normalized = getString6(turn.payload.text)?.trim().toLowerCase();
|
|
9227
|
+
if (!normalized) {
|
|
9228
|
+
continue;
|
|
9229
|
+
}
|
|
9230
|
+
const seen = turnTextsBySession.get(turn.sessionId) ?? new Set;
|
|
9231
|
+
if (seen.has(normalized)) {
|
|
9232
|
+
duplicateTurns += 1;
|
|
9233
|
+
}
|
|
9234
|
+
seen.add(normalized);
|
|
9235
|
+
turnTextsBySession.set(turn.sessionId, seen);
|
|
9236
|
+
}
|
|
9237
|
+
const missingAssistantReplySessions = [...sessionsWithTurns].filter((sessionId) => !sessionIdsWithAssistantReply.has(sessionId)).length;
|
|
9238
|
+
const providerEvents = events.filter((event) => event.type === "session.error" && typeof event.payload.provider === "string" && typeof event.payload.providerStatus === "string");
|
|
9239
|
+
const providerErrors = providerEvents.filter((event) => event.payload.providerStatus === "error");
|
|
9240
|
+
const providerFallbacks = providerEvents.filter((event) => event.payload.providerStatus === "fallback");
|
|
9241
|
+
const providerTimeouts = providerEvents.filter((event) => event.payload.timedOut === true);
|
|
9242
|
+
const providerLatencies = providerEvents.map((event) => getNumber3(event.payload.elapsedMs)).filter((value) => value !== undefined);
|
|
9243
|
+
const averageProviderLatencyMs = providerLatencies.length > 0 ? providerLatencies.reduce((sum, value) => sum + value, 0) / providerLatencies.length : 0;
|
|
9244
|
+
const handoffHealth = await summarizeVoiceHandoffHealth({ events });
|
|
9245
|
+
const metrics = {
|
|
9246
|
+
duplicateTurnRate: createMetric({
|
|
9247
|
+
actual: rate(duplicateTurns, committedTurns.length),
|
|
9248
|
+
label: "Duplicate turn rate",
|
|
9249
|
+
threshold: thresholds.maxDuplicateTurnRate,
|
|
9250
|
+
unit: "rate"
|
|
9251
|
+
}),
|
|
9252
|
+
emptyTurnRate: createMetric({
|
|
9253
|
+
actual: rate(emptyTurns.length, committedTurns.length),
|
|
9254
|
+
label: "Empty turn rate",
|
|
9255
|
+
threshold: thresholds.maxEmptyTurnRate,
|
|
9256
|
+
unit: "rate"
|
|
9257
|
+
}),
|
|
9258
|
+
handoffFailureRate: createMetric({
|
|
9259
|
+
actual: rate(handoffHealth.failed, handoffHealth.total),
|
|
9260
|
+
label: "Handoff failure rate",
|
|
9261
|
+
threshold: thresholds.maxHandoffFailureRate,
|
|
9262
|
+
unit: "rate"
|
|
9263
|
+
}),
|
|
9264
|
+
missingAssistantReplyRate: createMetric({
|
|
9265
|
+
actual: rate(missingAssistantReplySessions, sessionsWithTurns.size),
|
|
9266
|
+
label: "Missing assistant reply rate",
|
|
9267
|
+
threshold: thresholds.maxMissingAssistantReplyRate,
|
|
9268
|
+
unit: "rate"
|
|
9269
|
+
}),
|
|
9270
|
+
providerAverageLatencyMs: createMetric({
|
|
9271
|
+
actual: averageProviderLatencyMs,
|
|
9272
|
+
label: "Average provider latency",
|
|
9273
|
+
threshold: thresholds.maxProviderAverageLatencyMs,
|
|
9274
|
+
unit: "ms"
|
|
9275
|
+
}),
|
|
9276
|
+
providerErrorRate: createMetric({
|
|
9277
|
+
actual: rate(providerErrors.length, providerEvents.length),
|
|
9278
|
+
label: "Provider error rate",
|
|
9279
|
+
threshold: thresholds.maxProviderErrorRate,
|
|
9280
|
+
unit: "rate"
|
|
9281
|
+
}),
|
|
9282
|
+
providerFallbackRate: createMetric({
|
|
9283
|
+
actual: rate(providerFallbacks.length, providerEvents.length),
|
|
9284
|
+
label: "Provider fallback rate",
|
|
9285
|
+
threshold: thresholds.maxProviderFallbackRate,
|
|
9286
|
+
unit: "rate"
|
|
9287
|
+
}),
|
|
9288
|
+
providerTimeoutRate: createMetric({
|
|
9289
|
+
actual: rate(providerTimeouts.length, providerEvents.length),
|
|
9290
|
+
label: "Provider timeout rate",
|
|
9291
|
+
threshold: thresholds.maxProviderTimeoutRate,
|
|
9292
|
+
unit: "rate"
|
|
9293
|
+
})
|
|
9294
|
+
};
|
|
9295
|
+
const status = Object.values(metrics).every((metric) => metric.pass) ? "pass" : "fail";
|
|
9296
|
+
return {
|
|
9297
|
+
checkedAt: Date.now(),
|
|
9298
|
+
eventCount: events.length,
|
|
9299
|
+
metrics,
|
|
9300
|
+
status,
|
|
9301
|
+
thresholds
|
|
9302
|
+
};
|
|
9303
|
+
};
|
|
9304
|
+
var escapeHtml9 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9305
|
+
var formatMetricValue = (metric) => metric.unit === "rate" ? `${(metric.actual * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.actual)}ms` : String(metric.actual);
|
|
9306
|
+
var formatThreshold = (metric) => metric.unit === "rate" ? `${(metric.threshold * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.threshold)}ms` : String(metric.threshold);
|
|
9307
|
+
var renderVoiceQualityHTML = (report) => {
|
|
9308
|
+
const rows = Object.entries(report.metrics).map(([key, metric]) => `<tr class="${metric.pass ? "pass" : "fail"}"><td>${escapeHtml9(metric.label)}</td><td>${escapeHtml9(formatMetricValue(metric))}</td><td>${escapeHtml9(formatThreshold(metric))}</td><td>${metric.pass ? "pass" : "fail"}</td><td><code>${escapeHtml9(key)}</code></td></tr>`).join("");
|
|
9309
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>AbsoluteJS Voice Quality</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}.status{border-radius:999px;display:inline-flex;padding:.35rem .75rem;font-weight:800}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}table{border-collapse:collapse;width:100%;background:white;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}.pass td{border-left:4px solid #16a34a}.fail td{border-left:4px solid #dc2626}code{background:#f3f4f6;padding:.15rem .3rem;border-radius:.3rem}</style></head><body><main><h1>Voice quality gates</h1><p class="status ${report.status}">${report.status}</p><p>${report.eventCount} event(s) checked.</p><table><thead><tr><th>Metric</th><th>Actual</th><th>Threshold</th><th>Status</th><th>Key</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
|
|
9310
|
+
};
|
|
9311
|
+
var createVoiceQualityRoutes = (options) => {
|
|
9312
|
+
const path = options.path ?? "/quality";
|
|
9313
|
+
const routes = new Elysia7({
|
|
9314
|
+
name: options.name ?? "absolutejs-voice-quality"
|
|
9315
|
+
});
|
|
9316
|
+
const getReport = () => evaluateVoiceQuality({
|
|
9317
|
+
events: options.events,
|
|
9318
|
+
store: options.store,
|
|
9319
|
+
thresholds: options.thresholds
|
|
9320
|
+
});
|
|
9321
|
+
routes.get(path, async () => {
|
|
9322
|
+
const report = await getReport();
|
|
9323
|
+
return new Response(renderVoiceQualityHTML(report), {
|
|
9324
|
+
headers: {
|
|
9325
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9326
|
+
...options.headers
|
|
9327
|
+
}
|
|
9328
|
+
});
|
|
9329
|
+
});
|
|
9330
|
+
routes.get(`${path}/json`, async () => getReport());
|
|
9331
|
+
routes.get(`${path}/status`, async ({ set }) => {
|
|
9332
|
+
const report = await getReport();
|
|
9333
|
+
if (report.status === "fail") {
|
|
9334
|
+
set.status = 503;
|
|
9335
|
+
}
|
|
9336
|
+
return report;
|
|
9337
|
+
});
|
|
9338
|
+
return routes;
|
|
9339
|
+
};
|
|
8861
9340
|
// src/resilienceRoutes.ts
|
|
8862
|
-
import { Elysia as
|
|
8863
|
-
var
|
|
8864
|
-
var
|
|
8865
|
-
var
|
|
8866
|
-
var
|
|
9341
|
+
import { Elysia as Elysia8 } from "elysia";
|
|
9342
|
+
var escapeHtml10 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9343
|
+
var getString7 = (value) => typeof value === "string" ? value : undefined;
|
|
9344
|
+
var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
9345
|
+
var getBoolean2 = (value) => value === true;
|
|
8867
9346
|
var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
|
|
8868
9347
|
var listVoiceRoutingEvents = (events) => {
|
|
8869
9348
|
const routingEvents = [];
|
|
@@ -8871,26 +9350,26 @@ var listVoiceRoutingEvents = (events) => {
|
|
|
8871
9350
|
if (event.type !== "session.error") {
|
|
8872
9351
|
continue;
|
|
8873
9352
|
}
|
|
8874
|
-
const provider =
|
|
9353
|
+
const provider = getString7(event.payload.provider);
|
|
8875
9354
|
const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
8876
9355
|
if (!provider || !providerStatus) {
|
|
8877
9356
|
continue;
|
|
8878
9357
|
}
|
|
8879
|
-
const kind =
|
|
9358
|
+
const kind = getString7(event.payload.kind);
|
|
8880
9359
|
routingEvents.push({
|
|
8881
9360
|
at: event.at,
|
|
8882
|
-
attempt:
|
|
8883
|
-
elapsedMs:
|
|
8884
|
-
error:
|
|
8885
|
-
fallbackProvider:
|
|
9361
|
+
attempt: getNumber4(event.payload.attempt),
|
|
9362
|
+
elapsedMs: getNumber4(event.payload.elapsedMs),
|
|
9363
|
+
error: getString7(event.payload.error),
|
|
9364
|
+
fallbackProvider: getString7(event.payload.fallbackProvider),
|
|
8886
9365
|
kind: kind === "stt" || kind === "tts" ? kind : "llm",
|
|
8887
|
-
latencyBudgetMs:
|
|
8888
|
-
operation:
|
|
9366
|
+
latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
|
|
9367
|
+
operation: getString7(event.payload.operation),
|
|
8889
9368
|
provider,
|
|
8890
|
-
selectedProvider:
|
|
9369
|
+
selectedProvider: getString7(event.payload.selectedProvider),
|
|
8891
9370
|
sessionId: event.sessionId,
|
|
8892
9371
|
status: providerStatus,
|
|
8893
|
-
timedOut:
|
|
9372
|
+
timedOut: getBoolean2(event.payload.timedOut),
|
|
8894
9373
|
turnId: event.turnId
|
|
8895
9374
|
});
|
|
8896
9375
|
}
|
|
@@ -8923,13 +9402,13 @@ var summarizeRoutingEvents = (events) => {
|
|
|
8923
9402
|
};
|
|
8924
9403
|
var renderProviderCards = (title, providers) => {
|
|
8925
9404
|
if (providers.length === 0) {
|
|
8926
|
-
return `<p class="muted">No ${
|
|
9405
|
+
return `<p class="muted">No ${escapeHtml10(title)} provider health yet.</p>`;
|
|
8927
9406
|
}
|
|
8928
9407
|
return `<div class="provider-grid">${providers.map((provider) => `
|
|
8929
|
-
<article class="card provider ${
|
|
9408
|
+
<article class="card provider ${escapeHtml10(provider.status)}">
|
|
8930
9409
|
<div class="card-header">
|
|
8931
|
-
<strong>${
|
|
8932
|
-
<span>${
|
|
9410
|
+
<strong>${escapeHtml10(provider.provider)}</strong>
|
|
9411
|
+
<span>${escapeHtml10(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
|
|
8933
9412
|
</div>
|
|
8934
9413
|
<dl>
|
|
8935
9414
|
<div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
|
|
@@ -8938,7 +9417,7 @@ var renderProviderCards = (title, providers) => {
|
|
|
8938
9417
|
<div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
|
|
8939
9418
|
<div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
|
|
8940
9419
|
</dl>
|
|
8941
|
-
${provider.lastError ? `<p class="muted">${
|
|
9420
|
+
${provider.lastError ? `<p class="muted">${escapeHtml10(provider.lastError)}</p>` : ""}
|
|
8942
9421
|
</article>
|
|
8943
9422
|
`).join("")}</div>`;
|
|
8944
9423
|
};
|
|
@@ -8947,24 +9426,24 @@ var renderTimeline2 = (events) => {
|
|
|
8947
9426
|
return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
|
|
8948
9427
|
}
|
|
8949
9428
|
return `<div class="timeline">${events.slice(0, 40).map((event) => `
|
|
8950
|
-
<article class="card event ${
|
|
9429
|
+
<article class="card event ${escapeHtml10(event.status ?? "unknown")}">
|
|
8951
9430
|
<div class="card-header">
|
|
8952
|
-
<strong>${
|
|
9431
|
+
<strong>${escapeHtml10(event.kind.toUpperCase())} ${escapeHtml10(event.operation ?? "generate")}</strong>
|
|
8953
9432
|
<span>${new Date(event.at).toLocaleString()}</span>
|
|
8954
9433
|
</div>
|
|
8955
9434
|
<p>
|
|
8956
|
-
<span class="pill">${
|
|
8957
|
-
<span class="pill">provider: ${
|
|
8958
|
-
${event.fallbackProvider ? `<span class="pill">fallback: ${
|
|
9435
|
+
<span class="pill">${escapeHtml10(event.status ?? "unknown")}</span>
|
|
9436
|
+
<span class="pill">provider: ${escapeHtml10(event.provider ?? "unknown")}</span>
|
|
9437
|
+
${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml10(event.fallbackProvider)}</span>` : ""}
|
|
8959
9438
|
${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
|
|
8960
9439
|
</p>
|
|
8961
9440
|
<dl>
|
|
8962
9441
|
<div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
|
|
8963
9442
|
<div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
|
|
8964
9443
|
<div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
|
|
8965
|
-
<div><dt>Session</dt><dd>${
|
|
9444
|
+
<div><dt>Session</dt><dd>${escapeHtml10(event.sessionId)}</dd></div>
|
|
8966
9445
|
</dl>
|
|
8967
|
-
${event.error ? `<p class="muted">${
|
|
9446
|
+
${event.error ? `<p class="muted">${escapeHtml10(event.error)}</p>` : ""}
|
|
8968
9447
|
</article>
|
|
8969
9448
|
`).join("")}</div>`;
|
|
8970
9449
|
};
|
|
@@ -8979,26 +9458,26 @@ var renderSimulationControls = (kind, simulation) => {
|
|
|
8979
9458
|
const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
|
|
8980
9459
|
const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
|
|
8981
9460
|
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="${
|
|
8983
|
-
<p class="muted">${
|
|
9461
|
+
return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml10(pathPrefix)}">
|
|
9462
|
+
<p class="muted">${escapeHtml10(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
|
|
8984
9463
|
<div class="simulate-actions">
|
|
8985
|
-
${failureProviders.map((provider) => `<button type="button" data-provider-fail="${
|
|
8986
|
-
${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${
|
|
9464
|
+
${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml10(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml10(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
|
|
9465
|
+
${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml10(provider.provider)}">Mark ${escapeHtml10(provider.provider)} recovered</button>`).join("")}
|
|
8987
9466
|
</div>
|
|
8988
|
-
${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${
|
|
9467
|
+
${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml10(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
|
|
8989
9468
|
<pre class="simulate-output" hidden></pre>
|
|
8990
9469
|
</div>`;
|
|
8991
9470
|
};
|
|
8992
9471
|
var renderVoiceResilienceHTML = (input) => {
|
|
8993
9472
|
const summary = summarizeRoutingEvents(input.routingEvents);
|
|
8994
|
-
const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${
|
|
8995
|
-
const links = input.links?.length ? input.links.map((link) => `<a href="${
|
|
9473
|
+
const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml10(kind)}: ${String(count)}</span>`).join("");
|
|
9474
|
+
const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml10(link.href)}">${escapeHtml10(link.label)}</a>`).join(" \xB7 ") : "";
|
|
8996
9475
|
return `<!doctype html>
|
|
8997
9476
|
<html lang="en">
|
|
8998
9477
|
<head>
|
|
8999
9478
|
<meta charset="utf-8" />
|
|
9000
9479
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
9001
|
-
<title>${
|
|
9480
|
+
<title>${escapeHtml10(input.title ?? "AbsoluteJS Voice Resilience")}</title>
|
|
9002
9481
|
<style>
|
|
9003
9482
|
:root { color-scheme: dark; }
|
|
9004
9483
|
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; }
|
|
@@ -9134,7 +9613,7 @@ var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
|
|
|
9134
9613
|
};
|
|
9135
9614
|
var createVoiceResilienceRoutes = (options) => {
|
|
9136
9615
|
const path = options.path ?? "/resilience";
|
|
9137
|
-
const routes = new
|
|
9616
|
+
const routes = new Elysia8({
|
|
9138
9617
|
name: options.name ?? "absolutejs-voice-resilience"
|
|
9139
9618
|
}).get(path, async () => {
|
|
9140
9619
|
const events = await options.store.list();
|
|
@@ -10015,7 +10494,7 @@ var createVoiceMemoryStore = () => {
|
|
|
10015
10494
|
return { get, getOrCreate, list, remove, set };
|
|
10016
10495
|
};
|
|
10017
10496
|
// src/opsWebhook.ts
|
|
10018
|
-
import { Elysia as
|
|
10497
|
+
import { Elysia as Elysia9 } from "elysia";
|
|
10019
10498
|
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
10020
10499
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
10021
10500
|
const encoder = new TextEncoder;
|
|
@@ -10145,7 +10624,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
|
10145
10624
|
};
|
|
10146
10625
|
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
10147
10626
|
const path = options.path ?? "/api/voice-ops/webhook";
|
|
10148
|
-
return new
|
|
10627
|
+
return new Elysia9().post(path, async ({ body, request, set }) => {
|
|
10149
10628
|
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
10150
10629
|
if (options.signingSecret) {
|
|
10151
10630
|
const verification = await verifyVoiceOpsWebhookSignature({
|
|
@@ -10177,198 +10656,6 @@ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
|
10177
10656
|
parse: "text"
|
|
10178
10657
|
});
|
|
10179
10658
|
};
|
|
10180
|
-
// src/handoffHealth.ts
|
|
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;
|
|
10184
|
-
var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
|
|
10185
|
-
var increment3 = (record, key) => {
|
|
10186
|
-
record[key] = (record[key] ?? 0) + 1;
|
|
10187
|
-
};
|
|
10188
|
-
var normalizeDelivery = (adapterId, value) => {
|
|
10189
|
-
const record = value && typeof value === "object" ? value : {};
|
|
10190
|
-
return {
|
|
10191
|
-
adapterId: getString5(record.adapterId) ?? adapterId,
|
|
10192
|
-
adapterKind: getString5(record.adapterKind),
|
|
10193
|
-
deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
|
|
10194
|
-
deliveredTo: getString5(record.deliveredTo),
|
|
10195
|
-
error: getString5(record.error),
|
|
10196
|
-
status: isStatus(record.status) ? record.status : "failed"
|
|
10197
|
-
};
|
|
10198
|
-
};
|
|
10199
|
-
var normalizeDeliveries = (payload) => {
|
|
10200
|
-
const deliveries = payload.deliveries;
|
|
10201
|
-
if (!deliveries || typeof deliveries !== "object") {
|
|
10202
|
-
return [];
|
|
10203
|
-
}
|
|
10204
|
-
return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
|
|
10205
|
-
};
|
|
10206
|
-
var resolveReplayHref = (event, replayHref) => {
|
|
10207
|
-
if (replayHref === false) {
|
|
10208
|
-
return;
|
|
10209
|
-
}
|
|
10210
|
-
if (typeof replayHref === "function") {
|
|
10211
|
-
return replayHref(event);
|
|
10212
|
-
}
|
|
10213
|
-
return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
|
|
10214
|
-
};
|
|
10215
|
-
var summarizeVoiceHandoffHealth = async (options = {}) => {
|
|
10216
|
-
const sourceEvents = options.events ?? await options.store?.list() ?? [];
|
|
10217
|
-
const search = options.q?.trim().toLowerCase();
|
|
10218
|
-
const byAction = {};
|
|
10219
|
-
const byAdapter = {};
|
|
10220
|
-
const byStatus = {
|
|
10221
|
-
delivered: 0,
|
|
10222
|
-
failed: 0,
|
|
10223
|
-
skipped: 0
|
|
10224
|
-
};
|
|
10225
|
-
const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
|
|
10226
|
-
const status = isStatus(event.payload.status) ? event.payload.status : "failed";
|
|
10227
|
-
const deliveries = normalizeDeliveries(event.payload);
|
|
10228
|
-
const item = {
|
|
10229
|
-
action: getString5(event.payload.action),
|
|
10230
|
-
at: event.at,
|
|
10231
|
-
deliveries,
|
|
10232
|
-
reason: getString5(event.payload.reason),
|
|
10233
|
-
sessionId: event.sessionId,
|
|
10234
|
-
status,
|
|
10235
|
-
target: getString5(event.payload.target)
|
|
10236
|
-
};
|
|
10237
|
-
return {
|
|
10238
|
-
...item,
|
|
10239
|
-
replayHref: resolveReplayHref(item, options.replayHref)
|
|
10240
|
-
};
|
|
10241
|
-
}).filter((event) => {
|
|
10242
|
-
if (options.status && options.status !== "all" && event.status !== options.status) {
|
|
10243
|
-
return false;
|
|
10244
|
-
}
|
|
10245
|
-
if (!search) {
|
|
10246
|
-
return true;
|
|
10247
|
-
}
|
|
10248
|
-
return [
|
|
10249
|
-
event.action,
|
|
10250
|
-
event.reason,
|
|
10251
|
-
event.sessionId,
|
|
10252
|
-
event.status,
|
|
10253
|
-
event.target,
|
|
10254
|
-
...event.deliveries.flatMap((delivery) => [
|
|
10255
|
-
delivery.adapterId,
|
|
10256
|
-
delivery.adapterKind,
|
|
10257
|
-
delivery.deliveredTo,
|
|
10258
|
-
delivery.error,
|
|
10259
|
-
delivery.status
|
|
10260
|
-
])
|
|
10261
|
-
].some((value) => value?.toLowerCase().includes(search));
|
|
10262
|
-
}).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
|
|
10263
|
-
for (const event of events) {
|
|
10264
|
-
byStatus[event.status] += 1;
|
|
10265
|
-
if (event.action) {
|
|
10266
|
-
increment3(byAction, event.action);
|
|
10267
|
-
}
|
|
10268
|
-
for (const delivery of event.deliveries) {
|
|
10269
|
-
byAdapter[delivery.adapterId] ??= {
|
|
10270
|
-
delivered: 0,
|
|
10271
|
-
failed: 0,
|
|
10272
|
-
skipped: 0
|
|
10273
|
-
};
|
|
10274
|
-
byAdapter[delivery.adapterId][delivery.status] += 1;
|
|
10275
|
-
}
|
|
10276
|
-
}
|
|
10277
|
-
return {
|
|
10278
|
-
byAction,
|
|
10279
|
-
byAdapter,
|
|
10280
|
-
byStatus,
|
|
10281
|
-
events,
|
|
10282
|
-
failed: byStatus.failed,
|
|
10283
|
-
total: events.length
|
|
10284
|
-
};
|
|
10285
|
-
};
|
|
10286
|
-
var renderMetricGrid = (summary) => [
|
|
10287
|
-
'<section class="voice-handoff-health-grid">',
|
|
10288
|
-
`<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
|
|
10289
|
-
`<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
|
|
10290
|
-
`<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
|
|
10291
|
-
`<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
|
|
10292
|
-
"</section>"
|
|
10293
|
-
].join("");
|
|
10294
|
-
var renderActionSummary = (summary) => {
|
|
10295
|
-
const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
|
|
10296
|
-
const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
|
|
10297
|
-
return [
|
|
10298
|
-
'<section class="voice-handoff-health-columns">',
|
|
10299
|
-
"<article><h3>Actions</h3>",
|
|
10300
|
-
actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml8(action)}: ${String(count)}</li>`).join("")}</ul>`,
|
|
10301
|
-
"</article>",
|
|
10302
|
-
"<article><h3>Adapters</h3>",
|
|
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>`,
|
|
10304
|
-
"</article>",
|
|
10305
|
-
"</section>"
|
|
10306
|
-
].join("");
|
|
10307
|
-
};
|
|
10308
|
-
var renderVoiceHandoffHealthHTML = (summary) => [
|
|
10309
|
-
'<div class="voice-handoff-health">',
|
|
10310
|
-
renderMetricGrid(summary),
|
|
10311
|
-
renderActionSummary(summary),
|
|
10312
|
-
"<section>",
|
|
10313
|
-
"<h3>Recent Handoffs</h3>",
|
|
10314
|
-
summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
|
|
10315
|
-
'<div class="voice-handoff-health-events">',
|
|
10316
|
-
...summary.events.map((event) => [
|
|
10317
|
-
`<article class="${escapeHtml8(event.status)}">`,
|
|
10318
|
-
'<div class="voice-handoff-health-event-header">',
|
|
10319
|
-
`<strong>${escapeHtml8(event.action ?? "handoff")}</strong>`,
|
|
10320
|
-
`<span>${escapeHtml8(event.status)}</span>`,
|
|
10321
|
-
"</div>",
|
|
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>` : "",
|
|
10325
|
-
event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
|
|
10326
|
-
"<li>",
|
|
10327
|
-
`${escapeHtml8(delivery.adapterId)}: ${escapeHtml8(delivery.status)}`,
|
|
10328
|
-
delivery.deliveredTo ? ` to ${escapeHtml8(delivery.deliveredTo)}` : "",
|
|
10329
|
-
delivery.error ? ` (${escapeHtml8(delivery.error)})` : "",
|
|
10330
|
-
"</li>"
|
|
10331
|
-
].join("")).join("")}</ul>` : "",
|
|
10332
|
-
event.replayHref ? `<p><a href="${escapeHtml8(event.replayHref)}">Open replay</a></p>` : "",
|
|
10333
|
-
"</article>"
|
|
10334
|
-
].join("")),
|
|
10335
|
-
"</div>"
|
|
10336
|
-
].join(""),
|
|
10337
|
-
"</section>",
|
|
10338
|
-
"</div>"
|
|
10339
|
-
].join("");
|
|
10340
|
-
var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
|
|
10341
|
-
...options,
|
|
10342
|
-
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
10343
|
-
q: query?.q ?? options.q,
|
|
10344
|
-
status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
|
|
10345
|
-
});
|
|
10346
|
-
var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
|
|
10347
|
-
const summary = await summarizeVoiceHandoffHealth({
|
|
10348
|
-
...options,
|
|
10349
|
-
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
10350
|
-
q: query?.q ?? options.q,
|
|
10351
|
-
status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
|
|
10352
|
-
});
|
|
10353
|
-
const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
|
|
10354
|
-
return new Response(body, {
|
|
10355
|
-
headers: {
|
|
10356
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
10357
|
-
...options.headers
|
|
10358
|
-
}
|
|
10359
|
-
});
|
|
10360
|
-
};
|
|
10361
|
-
var createVoiceHandoffHealthRoutes = (options = {}) => {
|
|
10362
|
-
const path = options.path ?? "/api/voice-handoffs";
|
|
10363
|
-
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
10364
|
-
const routes = new Elysia7({
|
|
10365
|
-
name: options.name ?? "absolutejs-voice-handoff-health"
|
|
10366
|
-
}).get(path, createVoiceHandoffHealthJSONHandler(options));
|
|
10367
|
-
if (htmlPath) {
|
|
10368
|
-
routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
|
|
10369
|
-
}
|
|
10370
|
-
return routes;
|
|
10371
|
-
};
|
|
10372
10659
|
// src/queue.ts
|
|
10373
10660
|
var releaseLeaseScript = `
|
|
10374
10661
|
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
|
@@ -12507,6 +12794,7 @@ export {
|
|
|
12507
12794
|
resolveVoiceOpsTaskAssignment,
|
|
12508
12795
|
resolveVoiceOpsTaskAgeBucket,
|
|
12509
12796
|
resolveVoiceOpsPreset,
|
|
12797
|
+
resolveVoiceDiagnosticsTraceFilter,
|
|
12510
12798
|
resolveVoiceAssistantMemoryNamespace,
|
|
12511
12799
|
resolveTurnDetectionConfig,
|
|
12512
12800
|
resolveAudioConditioningConfig,
|
|
@@ -12516,6 +12804,7 @@ export {
|
|
|
12516
12804
|
renderVoiceTraceHTML,
|
|
12517
12805
|
renderVoiceSessionsHTML,
|
|
12518
12806
|
renderVoiceResilienceHTML,
|
|
12807
|
+
renderVoiceQualityHTML,
|
|
12519
12808
|
renderVoiceProviderHealthHTML,
|
|
12520
12809
|
renderVoiceHandoffHealthHTML,
|
|
12521
12810
|
renderVoiceCallReviewMarkdown,
|
|
@@ -12537,6 +12826,7 @@ export {
|
|
|
12537
12826
|
failVoiceOpsTask,
|
|
12538
12827
|
exportVoiceTrace,
|
|
12539
12828
|
evaluateVoiceTrace,
|
|
12829
|
+
evaluateVoiceQuality,
|
|
12540
12830
|
encodeTwilioMulawBase64,
|
|
12541
12831
|
deliverVoiceTraceEventsToSinks,
|
|
12542
12832
|
deliverVoiceIntegrationEventToSinks,
|
|
@@ -12587,6 +12877,7 @@ export {
|
|
|
12587
12877
|
createVoiceResilienceRoutes,
|
|
12588
12878
|
createVoiceRedisTaskLeaseCoordinator,
|
|
12589
12879
|
createVoiceRedisIdempotencyStore,
|
|
12880
|
+
createVoiceQualityRoutes,
|
|
12590
12881
|
createVoiceProviderRouter,
|
|
12591
12882
|
createVoiceProviderHealthRoutes,
|
|
12592
12883
|
createVoiceProviderHealthJSONHandler,
|
|
@@ -12640,6 +12931,7 @@ export {
|
|
|
12640
12931
|
createVoiceExternalObjectMapId,
|
|
12641
12932
|
createVoiceExternalObjectMap,
|
|
12642
12933
|
createVoiceExperiment,
|
|
12934
|
+
createVoiceDiagnosticsRoutes,
|
|
12643
12935
|
createVoiceCallReviewRecorder,
|
|
12644
12936
|
createVoiceCallReviewFromSession,
|
|
12645
12937
|
createVoiceCallReviewFromLiveTelephonyReport,
|
|
@@ -12675,6 +12967,7 @@ export {
|
|
|
12675
12967
|
buildVoiceTraceReplay,
|
|
12676
12968
|
buildVoiceOpsTaskFromSLABreach,
|
|
12677
12969
|
buildVoiceOpsTaskFromReview,
|
|
12970
|
+
buildVoiceDiagnosticsMarkdown,
|
|
12678
12971
|
assignVoiceOpsTask,
|
|
12679
12972
|
applyVoiceOpsTaskPolicy,
|
|
12680
12973
|
applyVoiceOpsTaskAssignmentRule,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { type StoredVoiceTraceEvent, type VoiceTraceEventStore } from './trace';
|
|
3
|
+
export type VoiceQualityStatus = 'pass' | 'fail';
|
|
4
|
+
export type VoiceQualityThresholds = {
|
|
5
|
+
maxDuplicateTurnRate?: number;
|
|
6
|
+
maxEmptyTurnRate?: number;
|
|
7
|
+
maxHandoffFailureRate?: number;
|
|
8
|
+
maxMissingAssistantReplyRate?: number;
|
|
9
|
+
maxProviderAverageLatencyMs?: number;
|
|
10
|
+
maxProviderErrorRate?: number;
|
|
11
|
+
maxProviderFallbackRate?: number;
|
|
12
|
+
maxProviderTimeoutRate?: number;
|
|
13
|
+
};
|
|
14
|
+
export type VoiceQualityMetric = {
|
|
15
|
+
actual: number;
|
|
16
|
+
label: string;
|
|
17
|
+
pass: boolean;
|
|
18
|
+
threshold: number;
|
|
19
|
+
unit: 'count' | 'ms' | 'rate';
|
|
20
|
+
};
|
|
21
|
+
export type VoiceQualityReport = {
|
|
22
|
+
checkedAt: number;
|
|
23
|
+
eventCount: number;
|
|
24
|
+
metrics: Record<string, VoiceQualityMetric>;
|
|
25
|
+
status: VoiceQualityStatus;
|
|
26
|
+
thresholds: Required<VoiceQualityThresholds>;
|
|
27
|
+
};
|
|
28
|
+
export type VoiceQualityRoutesOptions = {
|
|
29
|
+
events?: StoredVoiceTraceEvent[];
|
|
30
|
+
headers?: HeadersInit;
|
|
31
|
+
name?: string;
|
|
32
|
+
path?: string;
|
|
33
|
+
store?: VoiceTraceEventStore;
|
|
34
|
+
thresholds?: VoiceQualityThresholds;
|
|
35
|
+
};
|
|
36
|
+
export declare const evaluateVoiceQuality: (input: {
|
|
37
|
+
events?: StoredVoiceTraceEvent[];
|
|
38
|
+
store?: VoiceTraceEventStore;
|
|
39
|
+
thresholds?: VoiceQualityThresholds;
|
|
40
|
+
}) => Promise<VoiceQualityReport>;
|
|
41
|
+
export declare const renderVoiceQualityHTML: (report: VoiceQualityReport) => string;
|
|
42
|
+
export declare const createVoiceQualityRoutes: (options: VoiceQualityRoutesOptions) => Elysia<"", {
|
|
43
|
+
decorator: {};
|
|
44
|
+
store: {};
|
|
45
|
+
derive: {};
|
|
46
|
+
resolve: {};
|
|
47
|
+
}, {
|
|
48
|
+
typebox: {};
|
|
49
|
+
error: {};
|
|
50
|
+
}, {
|
|
51
|
+
schema: {};
|
|
52
|
+
standaloneSchema: {};
|
|
53
|
+
macro: {};
|
|
54
|
+
macroFn: {};
|
|
55
|
+
parser: {};
|
|
56
|
+
response: {};
|
|
57
|
+
}, {}, {
|
|
58
|
+
derive: {};
|
|
59
|
+
resolve: {};
|
|
60
|
+
schema: {};
|
|
61
|
+
standaloneSchema: {};
|
|
62
|
+
response: {};
|
|
63
|
+
}, {
|
|
64
|
+
derive: {};
|
|
65
|
+
resolve: {};
|
|
66
|
+
schema: {};
|
|
67
|
+
standaloneSchema: {};
|
|
68
|
+
response: {};
|
|
69
|
+
}>;
|