@absolutejs/voice 0.0.22-beta.43 → 0.0.22-beta.44
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/evalRoutes.d.ts +53 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +170 -13
- package/package.json +1 -1
package/dist/evalRoutes.d.ts
CHANGED
|
@@ -59,6 +59,49 @@ export type VoiceEvalBaselineStore = {
|
|
|
59
59
|
get: () => Promise<VoiceEvalReport | undefined>;
|
|
60
60
|
set: (report: VoiceEvalReport) => Promise<void>;
|
|
61
61
|
};
|
|
62
|
+
export type VoiceScenarioEvalDefinition = {
|
|
63
|
+
description?: string;
|
|
64
|
+
forbiddenHandoffActions?: string[];
|
|
65
|
+
forbiddenLifecycleTypes?: string[];
|
|
66
|
+
id: string;
|
|
67
|
+
label?: string;
|
|
68
|
+
maxProviderErrors?: number;
|
|
69
|
+
maxSessionErrors?: number;
|
|
70
|
+
minSessions?: number;
|
|
71
|
+
minTurns?: number;
|
|
72
|
+
requiredAssistantIncludes?: string[];
|
|
73
|
+
requiredDisposition?: string;
|
|
74
|
+
requiredHandoffActions?: string[];
|
|
75
|
+
requiredLifecycleTypes?: string[];
|
|
76
|
+
requiredPayloadPaths?: string[];
|
|
77
|
+
requiredTranscriptIncludes?: string[];
|
|
78
|
+
scenarioId?: string;
|
|
79
|
+
};
|
|
80
|
+
export type VoiceScenarioEvalSessionResult = {
|
|
81
|
+
eventCount: number;
|
|
82
|
+
issues: string[];
|
|
83
|
+
sessionId: string;
|
|
84
|
+
status: VoiceEvalStatus;
|
|
85
|
+
};
|
|
86
|
+
export type VoiceScenarioEvalResult = {
|
|
87
|
+
description?: string;
|
|
88
|
+
failed: number;
|
|
89
|
+
id: string;
|
|
90
|
+
issues: string[];
|
|
91
|
+
label: string;
|
|
92
|
+
matchedSessions: number;
|
|
93
|
+
passed: number;
|
|
94
|
+
sessions: VoiceScenarioEvalSessionResult[];
|
|
95
|
+
status: VoiceEvalStatus;
|
|
96
|
+
};
|
|
97
|
+
export type VoiceScenarioEvalReport = {
|
|
98
|
+
checkedAt: number;
|
|
99
|
+
failed: number;
|
|
100
|
+
passed: number;
|
|
101
|
+
scenarios: VoiceScenarioEvalResult[];
|
|
102
|
+
status: VoiceEvalStatus;
|
|
103
|
+
total: number;
|
|
104
|
+
};
|
|
62
105
|
export type VoiceEvalLink = {
|
|
63
106
|
href: string;
|
|
64
107
|
label: string;
|
|
@@ -73,6 +116,7 @@ export type VoiceEvalRoutesOptions = {
|
|
|
73
116
|
limit?: number;
|
|
74
117
|
name?: string;
|
|
75
118
|
path?: string;
|
|
119
|
+
scenarios?: VoiceScenarioEvalDefinition[];
|
|
76
120
|
store?: VoiceTraceEventStore;
|
|
77
121
|
thresholds?: VoiceQualityThresholds;
|
|
78
122
|
title?: string;
|
|
@@ -83,6 +127,11 @@ export declare const runVoiceSessionEvals: (options?: {
|
|
|
83
127
|
store?: VoiceTraceEventStore;
|
|
84
128
|
thresholds?: VoiceQualityThresholds;
|
|
85
129
|
}) => Promise<VoiceEvalReport>;
|
|
130
|
+
export declare const runVoiceScenarioEvals: (options?: {
|
|
131
|
+
events?: StoredVoiceTraceEvent[];
|
|
132
|
+
scenarios?: VoiceScenarioEvalDefinition[];
|
|
133
|
+
store?: VoiceTraceEventStore;
|
|
134
|
+
}) => Promise<VoiceScenarioEvalReport>;
|
|
86
135
|
export declare const compareVoiceEvalBaseline: (currentReport: VoiceEvalReport, baselineReport: VoiceEvalReport, options?: VoiceEvalBaselineComparisonOptions) => VoiceEvalBaselineComparison;
|
|
87
136
|
export declare const createVoiceFileEvalBaselineStore: (filePath: string) => VoiceEvalBaselineStore;
|
|
88
137
|
export declare const renderVoiceEvalHTML: (report: VoiceEvalReport, options?: {
|
|
@@ -93,6 +142,10 @@ export declare const renderVoiceEvalBaselineHTML: (comparison: VoiceEvalBaseline
|
|
|
93
142
|
links?: VoiceEvalLink[];
|
|
94
143
|
title?: string;
|
|
95
144
|
}) => string;
|
|
145
|
+
export declare const renderVoiceScenarioEvalHTML: (report: VoiceScenarioEvalReport, options?: {
|
|
146
|
+
links?: VoiceEvalLink[];
|
|
147
|
+
title?: string;
|
|
148
|
+
}) => string;
|
|
96
149
|
export declare const createVoiceEvalRoutes: (options: VoiceEvalRoutesOptions) => Elysia<"", {
|
|
97
150
|
decorator: {};
|
|
98
151
|
store: {};
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { voice } from './plugin';
|
|
|
2
2
|
export { createVoiceAssistant, createVoiceExperiment, summarizeVoiceAssistantRuns } from './assistant';
|
|
3
3
|
export { createVoiceAssistantHealthHTMLHandler, createVoiceAssistantHealthJSONHandler, createVoiceAssistantHealthRoutes, renderVoiceAssistantHealthHTML, summarizeVoiceAssistantHealth } from './assistantHealth';
|
|
4
4
|
export { buildVoiceDiagnosticsMarkdown, createVoiceDiagnosticsRoutes, resolveVoiceDiagnosticsTraceFilter } from './diagnosticsRoutes';
|
|
5
|
-
export { compareVoiceEvalBaseline, createVoiceFileEvalBaselineStore, createVoiceEvalRoutes, renderVoiceEvalBaselineHTML, renderVoiceEvalHTML, runVoiceSessionEvals } from './evalRoutes';
|
|
5
|
+
export { compareVoiceEvalBaseline, createVoiceFileEvalBaselineStore, createVoiceEvalRoutes, renderVoiceEvalBaselineHTML, renderVoiceEvalHTML, renderVoiceScenarioEvalHTML, runVoiceScenarioEvals, runVoiceSessionEvals } from './evalRoutes';
|
|
6
6
|
export { createVoiceSessionListRoutes, createVoiceSessionReplayHTMLHandler, createVoiceSessionReplayJSONHandler, createVoiceSessionReplayRoutes, createVoiceSessionsHTMLHandler, createVoiceSessionsJSONHandler, renderVoiceSessionsHTML, summarizeVoiceSessions, summarizeVoiceSessionReplay } from './sessionReplay';
|
|
7
7
|
export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from './agent';
|
|
8
8
|
export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore } from './fileStore';
|
|
@@ -40,7 +40,7 @@ export type { VoiceAssistant, VoiceAssistantArtifactPlan, VoiceAssistantExperime
|
|
|
40
40
|
export type { VoiceAssistantHealthFailure, VoiceAssistantHealthHTMLHandlerOptions, VoiceAssistantHealthRoutesOptions, VoiceAssistantHealthSummary, VoiceAssistantHealthSummaryOptions } from './assistantHealth';
|
|
41
41
|
export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssistantMemoryOptions, VoiceAssistantMemoryRecord, VoiceAssistantMemoryStore } from './assistantMemory';
|
|
42
42
|
export type { VoiceDiagnosticsRoutesOptions } from './diagnosticsRoutes';
|
|
43
|
-
export type { VoiceEvalBaselineComparison, VoiceEvalBaselineComparisonOptions, VoiceEvalBaselineStore, VoiceEvalBaselineSummary, VoiceEvalLink, VoiceEvalReport, VoiceEvalRoutesOptions, VoiceEvalSessionReport, VoiceEvalStatus, VoiceEvalTrendBucket } from './evalRoutes';
|
|
43
|
+
export type { VoiceEvalBaselineComparison, VoiceEvalBaselineComparisonOptions, VoiceEvalBaselineStore, VoiceEvalBaselineSummary, VoiceEvalLink, VoiceEvalReport, VoiceEvalRoutesOptions, VoiceEvalSessionReport, VoiceEvalStatus, VoiceEvalTrendBucket, VoiceScenarioEvalDefinition, VoiceScenarioEvalReport, VoiceScenarioEvalResult, VoiceScenarioEvalSessionResult } from './evalRoutes';
|
|
44
44
|
export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
|
|
45
45
|
export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
|
|
46
46
|
export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
|
package/dist/index.js
CHANGED
|
@@ -7977,6 +7977,24 @@ var createVoiceQualityRoutes = (options) => {
|
|
|
7977
7977
|
// src/evalRoutes.ts
|
|
7978
7978
|
var escapeHtml9 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
7979
7979
|
var rate2 = (count, total) => count / Math.max(1, total);
|
|
7980
|
+
var normalizeSearchText = (value) => value.trim().toLowerCase();
|
|
7981
|
+
var getString6 = (value) => typeof value === "string" ? value : undefined;
|
|
7982
|
+
var getObject = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
7983
|
+
var getPathValue = (value, path) => {
|
|
7984
|
+
let current = value;
|
|
7985
|
+
for (const part of path.split(".").filter(Boolean)) {
|
|
7986
|
+
const record = getObject(current);
|
|
7987
|
+
if (!record || !(part in record)) {
|
|
7988
|
+
return;
|
|
7989
|
+
}
|
|
7990
|
+
current = record[part];
|
|
7991
|
+
}
|
|
7992
|
+
return current;
|
|
7993
|
+
};
|
|
7994
|
+
var includesAll = (haystack, needles) => {
|
|
7995
|
+
const normalized = normalizeSearchText(haystack);
|
|
7996
|
+
return needles.filter((needle) => !normalized.includes(normalizeSearchText(needle)));
|
|
7997
|
+
};
|
|
7980
7998
|
var sessionTime = (events) => {
|
|
7981
7999
|
const sorted = filterVoiceTraceEvents(events);
|
|
7982
8000
|
return {
|
|
@@ -8047,6 +8065,108 @@ var runVoiceSessionEvals = async (options = {}) => {
|
|
|
8047
8065
|
trend: buildTrend(limitedSessions)
|
|
8048
8066
|
};
|
|
8049
8067
|
};
|
|
8068
|
+
var getSessionText = (events, type) => events.filter((event) => event.type === type).map((event) => getString6(event.payload.text)).filter((text) => Boolean(text?.trim())).join(`
|
|
8069
|
+
`);
|
|
8070
|
+
var countProviderErrors = (events) => events.filter((event) => event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.provider === "string")).length;
|
|
8071
|
+
var evaluateScenarioSession = (scenario, sessionId, events) => {
|
|
8072
|
+
const issues = [];
|
|
8073
|
+
const committedText = getSessionText(events, "turn.committed");
|
|
8074
|
+
const assistantText = getSessionText(events, "turn.assistant");
|
|
8075
|
+
const lifecycleTypes = events.filter((event) => event.type === "call.lifecycle").map((event) => getString6(event.payload.type)).filter((type) => Boolean(type));
|
|
8076
|
+
const dispositions = events.filter((event) => event.type === "call.lifecycle").map((event) => getString6(event.payload.disposition)).filter((disposition) => Boolean(disposition));
|
|
8077
|
+
const handoffActions = events.filter((event) => event.type === "call.handoff").map((event) => getString6(event.payload.action)).filter((action) => Boolean(action));
|
|
8078
|
+
const turnCount = events.filter((event) => event.type === "turn.committed").length;
|
|
8079
|
+
const sessionErrorCount = events.filter((event) => event.type === "session.error").length;
|
|
8080
|
+
const providerErrorCount = countProviderErrors(events);
|
|
8081
|
+
for (const missing of includesAll(committedText, scenario.requiredTranscriptIncludes ?? [])) {
|
|
8082
|
+
issues.push(`Missing transcript text: ${missing}`);
|
|
8083
|
+
}
|
|
8084
|
+
for (const missing of includesAll(assistantText, scenario.requiredAssistantIncludes ?? [])) {
|
|
8085
|
+
issues.push(`Missing assistant text: ${missing}`);
|
|
8086
|
+
}
|
|
8087
|
+
for (const type of scenario.requiredLifecycleTypes ?? []) {
|
|
8088
|
+
if (!lifecycleTypes.includes(type)) {
|
|
8089
|
+
issues.push(`Missing lifecycle event: ${type}`);
|
|
8090
|
+
}
|
|
8091
|
+
}
|
|
8092
|
+
for (const type of scenario.forbiddenLifecycleTypes ?? []) {
|
|
8093
|
+
if (lifecycleTypes.includes(type)) {
|
|
8094
|
+
issues.push(`Forbidden lifecycle event occurred: ${type}`);
|
|
8095
|
+
}
|
|
8096
|
+
}
|
|
8097
|
+
for (const action of scenario.requiredHandoffActions ?? []) {
|
|
8098
|
+
if (!handoffActions.includes(action)) {
|
|
8099
|
+
issues.push(`Missing handoff action: ${action}`);
|
|
8100
|
+
}
|
|
8101
|
+
}
|
|
8102
|
+
for (const action of scenario.forbiddenHandoffActions ?? []) {
|
|
8103
|
+
if (handoffActions.includes(action)) {
|
|
8104
|
+
issues.push(`Forbidden handoff action occurred: ${action}`);
|
|
8105
|
+
}
|
|
8106
|
+
}
|
|
8107
|
+
if (scenario.requiredDisposition && !dispositions.includes(scenario.requiredDisposition)) {
|
|
8108
|
+
issues.push(`Missing disposition: ${scenario.requiredDisposition}`);
|
|
8109
|
+
}
|
|
8110
|
+
if (scenario.minTurns !== undefined && turnCount < scenario.minTurns) {
|
|
8111
|
+
issues.push(`Expected at least ${scenario.minTurns} turn(s), saw ${turnCount}.`);
|
|
8112
|
+
}
|
|
8113
|
+
if (scenario.maxSessionErrors !== undefined && sessionErrorCount > scenario.maxSessionErrors) {
|
|
8114
|
+
issues.push(`Expected at most ${scenario.maxSessionErrors} session error(s), saw ${sessionErrorCount}.`);
|
|
8115
|
+
}
|
|
8116
|
+
if (scenario.maxProviderErrors !== undefined && providerErrorCount > scenario.maxProviderErrors) {
|
|
8117
|
+
issues.push(`Expected at most ${scenario.maxProviderErrors} provider error(s), saw ${providerErrorCount}.`);
|
|
8118
|
+
}
|
|
8119
|
+
for (const path of scenario.requiredPayloadPaths ?? []) {
|
|
8120
|
+
if (events.every((event) => getPathValue(event.payload, path) === undefined)) {
|
|
8121
|
+
issues.push(`Missing payload path: ${path}`);
|
|
8122
|
+
}
|
|
8123
|
+
}
|
|
8124
|
+
return {
|
|
8125
|
+
eventCount: events.length,
|
|
8126
|
+
issues,
|
|
8127
|
+
sessionId,
|
|
8128
|
+
status: issues.length > 0 ? "fail" : "pass"
|
|
8129
|
+
};
|
|
8130
|
+
};
|
|
8131
|
+
var runVoiceScenarioEvals = async (options = {}) => {
|
|
8132
|
+
const scenarios = options.scenarios ?? [];
|
|
8133
|
+
const events = filterVoiceTraceEvents(options.events ?? await options.store?.list() ?? []);
|
|
8134
|
+
const grouped = new Map;
|
|
8135
|
+
for (const event of events) {
|
|
8136
|
+
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
8137
|
+
}
|
|
8138
|
+
const results = scenarios.map((scenario) => {
|
|
8139
|
+
const sessions = [...grouped.entries()].filter(([, sessionEvents]) => scenario.scenarioId ? sessionEvents.some((event) => event.scenarioId === scenario.scenarioId) : true).map(([sessionId, sessionEvents]) => evaluateScenarioSession(scenario, sessionId, filterVoiceTraceEvents(sessionEvents))).sort((left, right) => left.sessionId.localeCompare(right.sessionId));
|
|
8140
|
+
const issues = [];
|
|
8141
|
+
const minSessions = scenario.minSessions ?? 1;
|
|
8142
|
+
if (sessions.length < minSessions) {
|
|
8143
|
+
issues.push(`Expected at least ${minSessions} matching session(s), saw ${sessions.length}.`);
|
|
8144
|
+
}
|
|
8145
|
+
const failed2 = sessions.filter((session) => session.status === "fail").length;
|
|
8146
|
+
const passed2 = sessions.length - failed2;
|
|
8147
|
+
return {
|
|
8148
|
+
description: scenario.description,
|
|
8149
|
+
failed: failed2,
|
|
8150
|
+
id: scenario.id,
|
|
8151
|
+
issues,
|
|
8152
|
+
label: scenario.label ?? scenario.id,
|
|
8153
|
+
matchedSessions: sessions.length,
|
|
8154
|
+
passed: passed2,
|
|
8155
|
+
sessions,
|
|
8156
|
+
status: issues.length > 0 || failed2 > 0 ? "fail" : "pass"
|
|
8157
|
+
};
|
|
8158
|
+
});
|
|
8159
|
+
const failed = results.filter((scenario) => scenario.status === "fail").length;
|
|
8160
|
+
const passed = results.length - failed;
|
|
8161
|
+
return {
|
|
8162
|
+
checkedAt: Date.now(),
|
|
8163
|
+
failed,
|
|
8164
|
+
passed,
|
|
8165
|
+
scenarios: results,
|
|
8166
|
+
status: failed > 0 ? "fail" : "pass",
|
|
8167
|
+
total: results.length
|
|
8168
|
+
};
|
|
8169
|
+
};
|
|
8050
8170
|
var summarizeEvalBaseline = (report) => {
|
|
8051
8171
|
const failedSessionIds = report.sessions.filter((session) => session.status === "fail").map((session) => session.sessionId).sort();
|
|
8052
8172
|
return {
|
|
@@ -8128,6 +8248,16 @@ var renderVoiceEvalBaselineHTML = (comparison, options = {}) => {
|
|
|
8128
8248
|
const recovered = comparison.recoveredSessionIds.length ? comparison.recoveredSessionIds.map((id) => `<li>${escapeHtml9(id)}</li>`).join("") : "<li>none</li>";
|
|
8129
8249
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml9(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1000px;margin:auto}nav{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;font-weight:800;padding:.35rem .75rem}.pass{background:#dcfce7;color:#166534}.fail{background:#fee2e2;color:#991b1b}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:1rem 0}.card{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.card strong{display:block;font-size:2rem}section{background:white;border:1px solid #e7e5e4;border-radius:1rem;margin:1rem 0;padding:1rem}</style></head><body><main>${links}<h1>${escapeHtml9(title)}</h1><p class="status ${comparison.status}">${comparison.status}</p><div class="grid"><article class="card"><span>Baseline pass rate</span><strong>${escapeHtml9(formatPercent(comparison.baseline.passRate))}</strong></article><article class="card"><span>Current pass rate</span><strong>${escapeHtml9(formatPercent(comparison.current.passRate))}</strong></article><article class="card"><span>Failed delta</span><strong>${comparison.deltas.failed}</strong></article><article class="card"><span>Pass rate delta</span><strong>${escapeHtml9(formatPercent(comparison.deltas.passRate))}</strong></article></div><section><h2>Regression Reasons</h2><ul>${reasons}</ul></section><section><h2>New Failed Sessions</h2><ul>${newFailures}</ul></section><section><h2>Recovered Sessions</h2><ul>${recovered}</ul></section></main></body></html>`;
|
|
8130
8250
|
};
|
|
8251
|
+
var renderVoiceScenarioEvalHTML = (report, options = {}) => {
|
|
8252
|
+
const title = options.title ?? "AbsoluteJS Voice Scenario Evals";
|
|
8253
|
+
const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
|
|
8254
|
+
const scenarios = report.scenarios.length ? report.scenarios.map((scenario) => {
|
|
8255
|
+
const scenarioIssues = scenario.issues.length ? `<ul>${scenario.issues.map((issue) => `<li>${escapeHtml9(issue)}</li>`).join("")}</ul>` : "";
|
|
8256
|
+
const sessions = scenario.sessions.length ? scenario.sessions.map((session) => `<tr class="${session.status}"><td>${escapeHtml9(session.sessionId)}</td><td>${escapeHtml9(session.status)}</td><td>${session.eventCount}</td><td>${escapeHtml9(session.issues.join(", ") || "none")}</td></tr>`).join("") : '<tr><td colspan="4">No matching sessions.</td></tr>';
|
|
8257
|
+
return `<section class="scenario ${scenario.status}"><h2>${escapeHtml9(scenario.label)}</h2>${scenario.description ? `<p>${escapeHtml9(scenario.description)}</p>` : ""}<p class="status ${scenario.status}">${scenario.status}</p><p>${scenario.passed} passed, ${scenario.failed} failed, ${scenario.matchedSessions} matched.</p>${scenarioIssues}<table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Issues</th></tr></thead><tbody>${sessions}</tbody></table></section>`;
|
|
8258
|
+
}).join("") : "<section><p>No scenarios configured.</p></section>";
|
|
8259
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml9(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1180px;margin:auto}nav{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;font-weight:800;padding:.35rem .75rem}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:1rem 0}.card,section{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.card strong{display:block;font-size:2rem}section{margin:1rem 0}table{border-collapse:collapse;width:100%;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}tr.fail td{border-left:4px solid #dc2626}tr.pass td{border-left:4px solid #16a34a}</style></head><body><main>${links}<h1>${escapeHtml9(title)}</h1><p class="status ${report.status}">${report.status}</p><div class="grid"><article class="card"><span>Total</span><strong>${report.total}</strong></article><article class="card"><span>Passed</span><strong>${report.passed}</strong></article><article class="card"><span>Failed</span><strong>${report.failed}</strong></article></div>${scenarios}</main></body></html>`;
|
|
8260
|
+
};
|
|
8131
8261
|
var createVoiceEvalRoutes = (options) => {
|
|
8132
8262
|
const path = options.path ?? "/evals";
|
|
8133
8263
|
const routes = new Elysia7({
|
|
@@ -8144,6 +8274,11 @@ var createVoiceEvalRoutes = (options) => {
|
|
|
8144
8274
|
const [current, baseline] = await Promise.all([getReport(), getBaseline()]);
|
|
8145
8275
|
return baseline ? compareVoiceEvalBaseline(current, baseline, options.baselineComparison) : undefined;
|
|
8146
8276
|
};
|
|
8277
|
+
const getScenarioReport = () => runVoiceScenarioEvals({
|
|
8278
|
+
events: options.events,
|
|
8279
|
+
scenarios: options.scenarios,
|
|
8280
|
+
store: options.store
|
|
8281
|
+
});
|
|
8147
8282
|
routes.get(path, async () => {
|
|
8148
8283
|
const report = await getReport();
|
|
8149
8284
|
return new Response(renderVoiceEvalHTML(report, {
|
|
@@ -8211,11 +8346,31 @@ var createVoiceEvalRoutes = (options) => {
|
|
|
8211
8346
|
status: "saved"
|
|
8212
8347
|
};
|
|
8213
8348
|
});
|
|
8349
|
+
routes.get(`${path}/scenarios`, async () => {
|
|
8350
|
+
const report = await getScenarioReport();
|
|
8351
|
+
return new Response(renderVoiceScenarioEvalHTML(report, {
|
|
8352
|
+
links: options.links,
|
|
8353
|
+
title: `${options.title ?? "AbsoluteJS Voice Evals"} Scenarios`
|
|
8354
|
+
}), {
|
|
8355
|
+
headers: {
|
|
8356
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
8357
|
+
...options.headers
|
|
8358
|
+
}
|
|
8359
|
+
});
|
|
8360
|
+
});
|
|
8361
|
+
routes.get(`${path}/scenarios/json`, async () => getScenarioReport());
|
|
8362
|
+
routes.get(`${path}/scenarios/status`, async ({ set }) => {
|
|
8363
|
+
const report = await getScenarioReport();
|
|
8364
|
+
if (report.status === "fail") {
|
|
8365
|
+
set.status = 503;
|
|
8366
|
+
}
|
|
8367
|
+
return report;
|
|
8368
|
+
});
|
|
8214
8369
|
return routes;
|
|
8215
8370
|
};
|
|
8216
8371
|
// src/sessionReplay.ts
|
|
8217
8372
|
import { Elysia as Elysia8 } from "elysia";
|
|
8218
|
-
var
|
|
8373
|
+
var getString7 = (value) => typeof value === "string" ? value : undefined;
|
|
8219
8374
|
var escapeHtml10 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
8220
8375
|
var increment3 = (record, key) => {
|
|
8221
8376
|
record[key] = (record[key] ?? 0) + 1;
|
|
@@ -8245,14 +8400,14 @@ var buildReplayTurns = (events) => {
|
|
|
8245
8400
|
case "turn.transcript":
|
|
8246
8401
|
turn.transcripts.push({
|
|
8247
8402
|
isFinal: event.payload.isFinal === true,
|
|
8248
|
-
text:
|
|
8403
|
+
text: getString7(event.payload.text)
|
|
8249
8404
|
});
|
|
8250
8405
|
break;
|
|
8251
8406
|
case "turn.committed":
|
|
8252
|
-
turn.committedText =
|
|
8407
|
+
turn.committedText = getString7(event.payload.text);
|
|
8253
8408
|
break;
|
|
8254
8409
|
case "turn.assistant": {
|
|
8255
|
-
const text =
|
|
8410
|
+
const text = getString7(event.payload.text);
|
|
8256
8411
|
if (text) {
|
|
8257
8412
|
turn.assistantReplies.push(text);
|
|
8258
8413
|
}
|
|
@@ -8321,7 +8476,7 @@ var summarizeVoiceSessions = async (options = {}) => {
|
|
|
8321
8476
|
let latestOutcome;
|
|
8322
8477
|
let errorCount = 0;
|
|
8323
8478
|
for (const event of sorted) {
|
|
8324
|
-
const provider =
|
|
8479
|
+
const provider = getString7(event.payload.provider);
|
|
8325
8480
|
if (provider) {
|
|
8326
8481
|
providers.add(provider);
|
|
8327
8482
|
}
|
|
@@ -8329,7 +8484,7 @@ var summarizeVoiceSessions = async (options = {}) => {
|
|
|
8329
8484
|
errorCount += 1;
|
|
8330
8485
|
increment3(providerErrors, provider ?? "unknown");
|
|
8331
8486
|
}
|
|
8332
|
-
const outcome =
|
|
8487
|
+
const outcome = getString7(event.payload.outcome);
|
|
8333
8488
|
if (outcome) {
|
|
8334
8489
|
latestOutcome = outcome;
|
|
8335
8490
|
}
|
|
@@ -9589,7 +9744,7 @@ import { Elysia as Elysia10 } from "elysia";
|
|
|
9589
9744
|
// src/resilienceRoutes.ts
|
|
9590
9745
|
import { Elysia as Elysia9 } from "elysia";
|
|
9591
9746
|
var escapeHtml11 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9592
|
-
var
|
|
9747
|
+
var getString8 = (value) => typeof value === "string" ? value : undefined;
|
|
9593
9748
|
var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
9594
9749
|
var getBoolean2 = (value) => value === true;
|
|
9595
9750
|
var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
|
|
@@ -9599,23 +9754,23 @@ var listVoiceRoutingEvents = (events) => {
|
|
|
9599
9754
|
if (event.type !== "session.error") {
|
|
9600
9755
|
continue;
|
|
9601
9756
|
}
|
|
9602
|
-
const provider =
|
|
9757
|
+
const provider = getString8(event.payload.provider);
|
|
9603
9758
|
const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
9604
9759
|
if (!provider || !providerStatus) {
|
|
9605
9760
|
continue;
|
|
9606
9761
|
}
|
|
9607
|
-
const kind =
|
|
9762
|
+
const kind = getString8(event.payload.kind);
|
|
9608
9763
|
routingEvents.push({
|
|
9609
9764
|
at: event.at,
|
|
9610
9765
|
attempt: getNumber4(event.payload.attempt),
|
|
9611
9766
|
elapsedMs: getNumber4(event.payload.elapsedMs),
|
|
9612
|
-
error:
|
|
9613
|
-
fallbackProvider:
|
|
9767
|
+
error: getString8(event.payload.error),
|
|
9768
|
+
fallbackProvider: getString8(event.payload.fallbackProvider),
|
|
9614
9769
|
kind: kind === "stt" || kind === "tts" ? kind : "llm",
|
|
9615
9770
|
latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
|
|
9616
|
-
operation:
|
|
9771
|
+
operation: getString8(event.payload.operation),
|
|
9617
9772
|
provider,
|
|
9618
|
-
selectedProvider:
|
|
9773
|
+
selectedProvider: getString8(event.payload.selectedProvider),
|
|
9619
9774
|
sessionId: event.sessionId,
|
|
9620
9775
|
status: providerStatus,
|
|
9621
9776
|
timedOut: getBoolean2(event.payload.timedOut),
|
|
@@ -13157,6 +13312,7 @@ export {
|
|
|
13157
13312
|
shapeTelephonyAssistantText,
|
|
13158
13313
|
selectVoiceTraceEventsForPrune,
|
|
13159
13314
|
runVoiceSessionEvals,
|
|
13315
|
+
runVoiceScenarioEvals,
|
|
13160
13316
|
resolveVoiceTraceRedactionOptions,
|
|
13161
13317
|
resolveVoiceSTTRoutingStrategy,
|
|
13162
13318
|
resolveVoiceRuntimePreset,
|
|
@@ -13174,6 +13330,7 @@ export {
|
|
|
13174
13330
|
renderVoiceTraceMarkdown,
|
|
13175
13331
|
renderVoiceTraceHTML,
|
|
13176
13332
|
renderVoiceSessionsHTML,
|
|
13333
|
+
renderVoiceScenarioEvalHTML,
|
|
13177
13334
|
renderVoiceResilienceHTML,
|
|
13178
13335
|
renderVoiceQualityHTML,
|
|
13179
13336
|
renderVoiceProviderHealthHTML,
|