@absolutejs/voice 0.0.22-beta.43 → 0.0.22-beta.45
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 +89 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +248 -13
- package/package.json +1 -1
package/dist/evalRoutes.d.ts
CHANGED
|
@@ -59,6 +59,73 @@ 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
|
+
};
|
|
105
|
+
export type VoiceScenarioFixture = {
|
|
106
|
+
description?: string;
|
|
107
|
+
events: StoredVoiceTraceEvent[];
|
|
108
|
+
id: string;
|
|
109
|
+
label?: string;
|
|
110
|
+
};
|
|
111
|
+
export type VoiceScenarioFixtureStore = {
|
|
112
|
+
list: () => Promise<VoiceScenarioFixture[]>;
|
|
113
|
+
};
|
|
114
|
+
export type VoiceScenarioFixtureEvalResult = {
|
|
115
|
+
description?: string;
|
|
116
|
+
fixtureId: string;
|
|
117
|
+
label: string;
|
|
118
|
+
report: VoiceScenarioEvalReport;
|
|
119
|
+
status: VoiceEvalStatus;
|
|
120
|
+
};
|
|
121
|
+
export type VoiceScenarioFixtureEvalReport = {
|
|
122
|
+
checkedAt: number;
|
|
123
|
+
failed: number;
|
|
124
|
+
fixtures: VoiceScenarioFixtureEvalResult[];
|
|
125
|
+
passed: number;
|
|
126
|
+
status: VoiceEvalStatus;
|
|
127
|
+
total: number;
|
|
128
|
+
};
|
|
62
129
|
export type VoiceEvalLink = {
|
|
63
130
|
href: string;
|
|
64
131
|
label: string;
|
|
@@ -68,11 +135,14 @@ export type VoiceEvalRoutesOptions = {
|
|
|
68
135
|
baselineComparison?: VoiceEvalBaselineComparisonOptions;
|
|
69
136
|
baselineStore?: VoiceEvalBaselineStore;
|
|
70
137
|
events?: StoredVoiceTraceEvent[];
|
|
138
|
+
fixtures?: VoiceScenarioFixture[];
|
|
139
|
+
fixtureStore?: VoiceScenarioFixtureStore;
|
|
71
140
|
headers?: HeadersInit;
|
|
72
141
|
links?: VoiceEvalLink[];
|
|
73
142
|
limit?: number;
|
|
74
143
|
name?: string;
|
|
75
144
|
path?: string;
|
|
145
|
+
scenarios?: VoiceScenarioEvalDefinition[];
|
|
76
146
|
store?: VoiceTraceEventStore;
|
|
77
147
|
thresholds?: VoiceQualityThresholds;
|
|
78
148
|
title?: string;
|
|
@@ -83,8 +153,19 @@ export declare const runVoiceSessionEvals: (options?: {
|
|
|
83
153
|
store?: VoiceTraceEventStore;
|
|
84
154
|
thresholds?: VoiceQualityThresholds;
|
|
85
155
|
}) => Promise<VoiceEvalReport>;
|
|
156
|
+
export declare const runVoiceScenarioEvals: (options?: {
|
|
157
|
+
events?: StoredVoiceTraceEvent[];
|
|
158
|
+
scenarios?: VoiceScenarioEvalDefinition[];
|
|
159
|
+
store?: VoiceTraceEventStore;
|
|
160
|
+
}) => Promise<VoiceScenarioEvalReport>;
|
|
161
|
+
export declare const runVoiceScenarioFixtureEvals: (options?: {
|
|
162
|
+
fixtures?: VoiceScenarioFixture[];
|
|
163
|
+
fixtureStore?: VoiceScenarioFixtureStore;
|
|
164
|
+
scenarios?: VoiceScenarioEvalDefinition[];
|
|
165
|
+
}) => Promise<VoiceScenarioFixtureEvalReport>;
|
|
86
166
|
export declare const compareVoiceEvalBaseline: (currentReport: VoiceEvalReport, baselineReport: VoiceEvalReport, options?: VoiceEvalBaselineComparisonOptions) => VoiceEvalBaselineComparison;
|
|
87
167
|
export declare const createVoiceFileEvalBaselineStore: (filePath: string) => VoiceEvalBaselineStore;
|
|
168
|
+
export declare const createVoiceFileScenarioFixtureStore: (filePath: string) => VoiceScenarioFixtureStore;
|
|
88
169
|
export declare const renderVoiceEvalHTML: (report: VoiceEvalReport, options?: {
|
|
89
170
|
links?: VoiceEvalLink[];
|
|
90
171
|
title?: string;
|
|
@@ -93,6 +174,14 @@ export declare const renderVoiceEvalBaselineHTML: (comparison: VoiceEvalBaseline
|
|
|
93
174
|
links?: VoiceEvalLink[];
|
|
94
175
|
title?: string;
|
|
95
176
|
}) => string;
|
|
177
|
+
export declare const renderVoiceScenarioEvalHTML: (report: VoiceScenarioEvalReport, options?: {
|
|
178
|
+
links?: VoiceEvalLink[];
|
|
179
|
+
title?: string;
|
|
180
|
+
}) => string;
|
|
181
|
+
export declare const renderVoiceScenarioFixtureEvalHTML: (report: VoiceScenarioFixtureEvalReport, options?: {
|
|
182
|
+
links?: VoiceEvalLink[];
|
|
183
|
+
title?: string;
|
|
184
|
+
}) => string;
|
|
96
185
|
export declare const createVoiceEvalRoutes: (options: VoiceEvalRoutesOptions) => Elysia<"", {
|
|
97
186
|
decorator: {};
|
|
98
187
|
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, createVoiceFileScenarioFixtureStore, createVoiceEvalRoutes, renderVoiceEvalBaselineHTML, renderVoiceEvalHTML, renderVoiceScenarioEvalHTML, renderVoiceScenarioFixtureEvalHTML, runVoiceScenarioEvals, runVoiceScenarioFixtureEvals, 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, VoiceScenarioFixture, VoiceScenarioFixtureEvalReport, VoiceScenarioFixtureEvalResult, VoiceScenarioFixtureStore } 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,135 @@ 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
|
+
};
|
|
8170
|
+
var resolveScenarioFixtures = async (options) => [...options.fixtures ?? [], ...await options.fixtureStore?.list() ?? []];
|
|
8171
|
+
var runVoiceScenarioFixtureEvals = async (options = {}) => {
|
|
8172
|
+
const fixtures = await resolveScenarioFixtures(options);
|
|
8173
|
+
const results = await Promise.all(fixtures.map(async (fixture) => {
|
|
8174
|
+
const report = await runVoiceScenarioEvals({
|
|
8175
|
+
events: fixture.events,
|
|
8176
|
+
scenarios: options.scenarios
|
|
8177
|
+
});
|
|
8178
|
+
return {
|
|
8179
|
+
description: fixture.description,
|
|
8180
|
+
fixtureId: fixture.id,
|
|
8181
|
+
label: fixture.label ?? fixture.id,
|
|
8182
|
+
report,
|
|
8183
|
+
status: report.status
|
|
8184
|
+
};
|
|
8185
|
+
}));
|
|
8186
|
+
const failed = results.filter((fixture) => fixture.status === "fail").length;
|
|
8187
|
+
const passed = results.length - failed;
|
|
8188
|
+
return {
|
|
8189
|
+
checkedAt: Date.now(),
|
|
8190
|
+
failed,
|
|
8191
|
+
fixtures: results,
|
|
8192
|
+
passed,
|
|
8193
|
+
status: failed > 0 ? "fail" : "pass",
|
|
8194
|
+
total: results.length
|
|
8195
|
+
};
|
|
8196
|
+
};
|
|
8050
8197
|
var summarizeEvalBaseline = (report) => {
|
|
8051
8198
|
const failedSessionIds = report.sessions.filter((session) => session.status === "fail").map((session) => session.sessionId).sort();
|
|
8052
8199
|
return {
|
|
@@ -8108,6 +8255,20 @@ var createVoiceFileEvalBaselineStore = (filePath) => ({
|
|
|
8108
8255
|
await Bun.write(filePath, JSON.stringify(report, null, 2));
|
|
8109
8256
|
}
|
|
8110
8257
|
});
|
|
8258
|
+
var createVoiceFileScenarioFixtureStore = (filePath) => ({
|
|
8259
|
+
list: async () => {
|
|
8260
|
+
const file = Bun.file(filePath);
|
|
8261
|
+
if (!await file.exists()) {
|
|
8262
|
+
return [];
|
|
8263
|
+
}
|
|
8264
|
+
const text = await file.text();
|
|
8265
|
+
if (!text.trim()) {
|
|
8266
|
+
return [];
|
|
8267
|
+
}
|
|
8268
|
+
const parsed = JSON.parse(text);
|
|
8269
|
+
return Array.isArray(parsed) ? parsed : parsed.fixtures ?? [];
|
|
8270
|
+
}
|
|
8271
|
+
});
|
|
8111
8272
|
var formatTime = (value) => value === undefined ? "unknown" : new Date(value).toLocaleString();
|
|
8112
8273
|
var formatPercent = (value) => `${(value * 100).toFixed(2)}%`;
|
|
8113
8274
|
var renderVoiceEvalHTML = (report, options = {}) => {
|
|
@@ -8128,6 +8289,25 @@ var renderVoiceEvalBaselineHTML = (comparison, options = {}) => {
|
|
|
8128
8289
|
const recovered = comparison.recoveredSessionIds.length ? comparison.recoveredSessionIds.map((id) => `<li>${escapeHtml9(id)}</li>`).join("") : "<li>none</li>";
|
|
8129
8290
|
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
8291
|
};
|
|
8292
|
+
var renderVoiceScenarioEvalHTML = (report, options = {}) => {
|
|
8293
|
+
const title = options.title ?? "AbsoluteJS Voice Scenario Evals";
|
|
8294
|
+
const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
|
|
8295
|
+
const scenarios = report.scenarios.length ? report.scenarios.map((scenario) => {
|
|
8296
|
+
const scenarioIssues = scenario.issues.length ? `<ul>${scenario.issues.map((issue) => `<li>${escapeHtml9(issue)}</li>`).join("")}</ul>` : "";
|
|
8297
|
+
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>';
|
|
8298
|
+
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>`;
|
|
8299
|
+
}).join("") : "<section><p>No scenarios configured.</p></section>";
|
|
8300
|
+
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>`;
|
|
8301
|
+
};
|
|
8302
|
+
var renderVoiceScenarioFixtureEvalHTML = (report, options = {}) => {
|
|
8303
|
+
const title = options.title ?? "AbsoluteJS Voice Fixture Evals";
|
|
8304
|
+
const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
|
|
8305
|
+
const fixtures = report.fixtures.length ? report.fixtures.map((fixture) => {
|
|
8306
|
+
const scenarios = fixture.report.scenarios.map((scenario) => `<tr class="${scenario.status}"><td>${escapeHtml9(scenario.label)}</td><td>${escapeHtml9(scenario.status)}</td><td>${scenario.matchedSessions}</td><td>${escapeHtml9([...scenario.issues, ...scenario.sessions.flatMap((session) => session.issues)].join(", ") || "none")}</td></tr>`).join("");
|
|
8307
|
+
return `<section class="${fixture.status}"><h2>${escapeHtml9(fixture.label)}</h2>${fixture.description ? `<p>${escapeHtml9(fixture.description)}</p>` : ""}<p class="status ${fixture.status}">${fixture.status}</p><table><thead><tr><th>Scenario</th><th>Status</th><th>Sessions</th><th>Issues</th></tr></thead><tbody>${scenarios}</tbody></table></section>`;
|
|
8308
|
+
}).join("") : "<section><p>No scenario fixtures configured.</p></section>";
|
|
8309
|
+
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>${fixtures}</main></body></html>`;
|
|
8310
|
+
};
|
|
8131
8311
|
var createVoiceEvalRoutes = (options) => {
|
|
8132
8312
|
const path = options.path ?? "/evals";
|
|
8133
8313
|
const routes = new Elysia7({
|
|
@@ -8144,6 +8324,16 @@ var createVoiceEvalRoutes = (options) => {
|
|
|
8144
8324
|
const [current, baseline] = await Promise.all([getReport(), getBaseline()]);
|
|
8145
8325
|
return baseline ? compareVoiceEvalBaseline(current, baseline, options.baselineComparison) : undefined;
|
|
8146
8326
|
};
|
|
8327
|
+
const getScenarioReport = () => runVoiceScenarioEvals({
|
|
8328
|
+
events: options.events,
|
|
8329
|
+
scenarios: options.scenarios,
|
|
8330
|
+
store: options.store
|
|
8331
|
+
});
|
|
8332
|
+
const getFixtureReport = () => runVoiceScenarioFixtureEvals({
|
|
8333
|
+
fixtures: options.fixtures,
|
|
8334
|
+
fixtureStore: options.fixtureStore,
|
|
8335
|
+
scenarios: options.scenarios
|
|
8336
|
+
});
|
|
8147
8337
|
routes.get(path, async () => {
|
|
8148
8338
|
const report = await getReport();
|
|
8149
8339
|
return new Response(renderVoiceEvalHTML(report, {
|
|
@@ -8211,11 +8401,51 @@ var createVoiceEvalRoutes = (options) => {
|
|
|
8211
8401
|
status: "saved"
|
|
8212
8402
|
};
|
|
8213
8403
|
});
|
|
8404
|
+
routes.get(`${path}/scenarios`, async () => {
|
|
8405
|
+
const report = await getScenarioReport();
|
|
8406
|
+
return new Response(renderVoiceScenarioEvalHTML(report, {
|
|
8407
|
+
links: options.links,
|
|
8408
|
+
title: `${options.title ?? "AbsoluteJS Voice Evals"} Scenarios`
|
|
8409
|
+
}), {
|
|
8410
|
+
headers: {
|
|
8411
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
8412
|
+
...options.headers
|
|
8413
|
+
}
|
|
8414
|
+
});
|
|
8415
|
+
});
|
|
8416
|
+
routes.get(`${path}/scenarios/json`, async () => getScenarioReport());
|
|
8417
|
+
routes.get(`${path}/scenarios/status`, async ({ set }) => {
|
|
8418
|
+
const report = await getScenarioReport();
|
|
8419
|
+
if (report.status === "fail") {
|
|
8420
|
+
set.status = 503;
|
|
8421
|
+
}
|
|
8422
|
+
return report;
|
|
8423
|
+
});
|
|
8424
|
+
routes.get(`${path}/fixtures`, async () => {
|
|
8425
|
+
const report = await getFixtureReport();
|
|
8426
|
+
return new Response(renderVoiceScenarioFixtureEvalHTML(report, {
|
|
8427
|
+
links: options.links,
|
|
8428
|
+
title: `${options.title ?? "AbsoluteJS Voice Evals"} Fixtures`
|
|
8429
|
+
}), {
|
|
8430
|
+
headers: {
|
|
8431
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
8432
|
+
...options.headers
|
|
8433
|
+
}
|
|
8434
|
+
});
|
|
8435
|
+
});
|
|
8436
|
+
routes.get(`${path}/fixtures/json`, async () => getFixtureReport());
|
|
8437
|
+
routes.get(`${path}/fixtures/status`, async ({ set }) => {
|
|
8438
|
+
const report = await getFixtureReport();
|
|
8439
|
+
if (report.status === "fail") {
|
|
8440
|
+
set.status = 503;
|
|
8441
|
+
}
|
|
8442
|
+
return report;
|
|
8443
|
+
});
|
|
8214
8444
|
return routes;
|
|
8215
8445
|
};
|
|
8216
8446
|
// src/sessionReplay.ts
|
|
8217
8447
|
import { Elysia as Elysia8 } from "elysia";
|
|
8218
|
-
var
|
|
8448
|
+
var getString7 = (value) => typeof value === "string" ? value : undefined;
|
|
8219
8449
|
var escapeHtml10 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
8220
8450
|
var increment3 = (record, key) => {
|
|
8221
8451
|
record[key] = (record[key] ?? 0) + 1;
|
|
@@ -8245,14 +8475,14 @@ var buildReplayTurns = (events) => {
|
|
|
8245
8475
|
case "turn.transcript":
|
|
8246
8476
|
turn.transcripts.push({
|
|
8247
8477
|
isFinal: event.payload.isFinal === true,
|
|
8248
|
-
text:
|
|
8478
|
+
text: getString7(event.payload.text)
|
|
8249
8479
|
});
|
|
8250
8480
|
break;
|
|
8251
8481
|
case "turn.committed":
|
|
8252
|
-
turn.committedText =
|
|
8482
|
+
turn.committedText = getString7(event.payload.text);
|
|
8253
8483
|
break;
|
|
8254
8484
|
case "turn.assistant": {
|
|
8255
|
-
const text =
|
|
8485
|
+
const text = getString7(event.payload.text);
|
|
8256
8486
|
if (text) {
|
|
8257
8487
|
turn.assistantReplies.push(text);
|
|
8258
8488
|
}
|
|
@@ -8321,7 +8551,7 @@ var summarizeVoiceSessions = async (options = {}) => {
|
|
|
8321
8551
|
let latestOutcome;
|
|
8322
8552
|
let errorCount = 0;
|
|
8323
8553
|
for (const event of sorted) {
|
|
8324
|
-
const provider =
|
|
8554
|
+
const provider = getString7(event.payload.provider);
|
|
8325
8555
|
if (provider) {
|
|
8326
8556
|
providers.add(provider);
|
|
8327
8557
|
}
|
|
@@ -8329,7 +8559,7 @@ var summarizeVoiceSessions = async (options = {}) => {
|
|
|
8329
8559
|
errorCount += 1;
|
|
8330
8560
|
increment3(providerErrors, provider ?? "unknown");
|
|
8331
8561
|
}
|
|
8332
|
-
const outcome =
|
|
8562
|
+
const outcome = getString7(event.payload.outcome);
|
|
8333
8563
|
if (outcome) {
|
|
8334
8564
|
latestOutcome = outcome;
|
|
8335
8565
|
}
|
|
@@ -9589,7 +9819,7 @@ import { Elysia as Elysia10 } from "elysia";
|
|
|
9589
9819
|
// src/resilienceRoutes.ts
|
|
9590
9820
|
import { Elysia as Elysia9 } from "elysia";
|
|
9591
9821
|
var escapeHtml11 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9592
|
-
var
|
|
9822
|
+
var getString8 = (value) => typeof value === "string" ? value : undefined;
|
|
9593
9823
|
var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
9594
9824
|
var getBoolean2 = (value) => value === true;
|
|
9595
9825
|
var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
|
|
@@ -9599,23 +9829,23 @@ var listVoiceRoutingEvents = (events) => {
|
|
|
9599
9829
|
if (event.type !== "session.error") {
|
|
9600
9830
|
continue;
|
|
9601
9831
|
}
|
|
9602
|
-
const provider =
|
|
9832
|
+
const provider = getString8(event.payload.provider);
|
|
9603
9833
|
const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
9604
9834
|
if (!provider || !providerStatus) {
|
|
9605
9835
|
continue;
|
|
9606
9836
|
}
|
|
9607
|
-
const kind =
|
|
9837
|
+
const kind = getString8(event.payload.kind);
|
|
9608
9838
|
routingEvents.push({
|
|
9609
9839
|
at: event.at,
|
|
9610
9840
|
attempt: getNumber4(event.payload.attempt),
|
|
9611
9841
|
elapsedMs: getNumber4(event.payload.elapsedMs),
|
|
9612
|
-
error:
|
|
9613
|
-
fallbackProvider:
|
|
9842
|
+
error: getString8(event.payload.error),
|
|
9843
|
+
fallbackProvider: getString8(event.payload.fallbackProvider),
|
|
9614
9844
|
kind: kind === "stt" || kind === "tts" ? kind : "llm",
|
|
9615
9845
|
latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
|
|
9616
|
-
operation:
|
|
9846
|
+
operation: getString8(event.payload.operation),
|
|
9617
9847
|
provider,
|
|
9618
|
-
selectedProvider:
|
|
9848
|
+
selectedProvider: getString8(event.payload.selectedProvider),
|
|
9619
9849
|
sessionId: event.sessionId,
|
|
9620
9850
|
status: providerStatus,
|
|
9621
9851
|
timedOut: getBoolean2(event.payload.timedOut),
|
|
@@ -13157,6 +13387,8 @@ export {
|
|
|
13157
13387
|
shapeTelephonyAssistantText,
|
|
13158
13388
|
selectVoiceTraceEventsForPrune,
|
|
13159
13389
|
runVoiceSessionEvals,
|
|
13390
|
+
runVoiceScenarioFixtureEvals,
|
|
13391
|
+
runVoiceScenarioEvals,
|
|
13160
13392
|
resolveVoiceTraceRedactionOptions,
|
|
13161
13393
|
resolveVoiceSTTRoutingStrategy,
|
|
13162
13394
|
resolveVoiceRuntimePreset,
|
|
@@ -13174,6 +13406,8 @@ export {
|
|
|
13174
13406
|
renderVoiceTraceMarkdown,
|
|
13175
13407
|
renderVoiceTraceHTML,
|
|
13176
13408
|
renderVoiceSessionsHTML,
|
|
13409
|
+
renderVoiceScenarioFixtureEvalHTML,
|
|
13410
|
+
renderVoiceScenarioEvalHTML,
|
|
13177
13411
|
renderVoiceResilienceHTML,
|
|
13178
13412
|
renderVoiceQualityHTML,
|
|
13179
13413
|
renderVoiceProviderHealthHTML,
|
|
@@ -13298,6 +13532,7 @@ export {
|
|
|
13298
13532
|
createVoiceFileTraceEventStore,
|
|
13299
13533
|
createVoiceFileTaskStore,
|
|
13300
13534
|
createVoiceFileSessionStore,
|
|
13535
|
+
createVoiceFileScenarioFixtureStore,
|
|
13301
13536
|
createVoiceFileRuntimeStorage,
|
|
13302
13537
|
createVoiceFileReviewStore,
|
|
13303
13538
|
createVoiceFileIntegrationEventStore,
|