@absolutejs/voice 0.0.22-beta.42 → 0.0.22-beta.43

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.
@@ -28,11 +28,45 @@ export type VoiceEvalReport = {
28
28
  total: number;
29
29
  trend: VoiceEvalTrendBucket[];
30
30
  };
31
+ export type VoiceEvalBaselineSummary = {
32
+ failed: number;
33
+ failedSessionIds: string[];
34
+ passRate: number;
35
+ passed: number;
36
+ total: number;
37
+ };
38
+ export type VoiceEvalBaselineComparison = {
39
+ baseline: VoiceEvalBaselineSummary;
40
+ checkedAt: number;
41
+ current: VoiceEvalBaselineSummary;
42
+ deltas: {
43
+ failed: number;
44
+ passRate: number;
45
+ passed: number;
46
+ total: number;
47
+ };
48
+ newFailedSessionIds: string[];
49
+ recoveredSessionIds: string[];
50
+ reasons: string[];
51
+ status: VoiceEvalStatus;
52
+ };
53
+ export type VoiceEvalBaselineComparisonOptions = {
54
+ failOnNewFailedSessions?: boolean;
55
+ maxFailedDelta?: number;
56
+ maxPassRateDrop?: number;
57
+ };
58
+ export type VoiceEvalBaselineStore = {
59
+ get: () => Promise<VoiceEvalReport | undefined>;
60
+ set: (report: VoiceEvalReport) => Promise<void>;
61
+ };
31
62
  export type VoiceEvalLink = {
32
63
  href: string;
33
64
  label: string;
34
65
  };
35
66
  export type VoiceEvalRoutesOptions = {
67
+ baseline?: VoiceEvalReport | (() => Promise<VoiceEvalReport | undefined>);
68
+ baselineComparison?: VoiceEvalBaselineComparisonOptions;
69
+ baselineStore?: VoiceEvalBaselineStore;
36
70
  events?: StoredVoiceTraceEvent[];
37
71
  headers?: HeadersInit;
38
72
  links?: VoiceEvalLink[];
@@ -49,10 +83,16 @@ export declare const runVoiceSessionEvals: (options?: {
49
83
  store?: VoiceTraceEventStore;
50
84
  thresholds?: VoiceQualityThresholds;
51
85
  }) => Promise<VoiceEvalReport>;
86
+ export declare const compareVoiceEvalBaseline: (currentReport: VoiceEvalReport, baselineReport: VoiceEvalReport, options?: VoiceEvalBaselineComparisonOptions) => VoiceEvalBaselineComparison;
87
+ export declare const createVoiceFileEvalBaselineStore: (filePath: string) => VoiceEvalBaselineStore;
52
88
  export declare const renderVoiceEvalHTML: (report: VoiceEvalReport, options?: {
53
89
  links?: VoiceEvalLink[];
54
90
  title?: string;
55
91
  }) => string;
92
+ export declare const renderVoiceEvalBaselineHTML: (comparison: VoiceEvalBaselineComparison, options?: {
93
+ links?: VoiceEvalLink[];
94
+ title?: string;
95
+ }) => string;
56
96
  export declare const createVoiceEvalRoutes: (options: VoiceEvalRoutesOptions) => Elysia<"", {
57
97
  decorator: {};
58
98
  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 { createVoiceEvalRoutes, renderVoiceEvalHTML, runVoiceSessionEvals } from './evalRoutes';
5
+ export { compareVoiceEvalBaseline, createVoiceFileEvalBaselineStore, createVoiceEvalRoutes, renderVoiceEvalBaselineHTML, renderVoiceEvalHTML, 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 { VoiceEvalLink, VoiceEvalReport, VoiceEvalRoutesOptions, VoiceEvalSessionReport, VoiceEvalStatus, VoiceEvalTrendBucket } from './evalRoutes';
43
+ export type { VoiceEvalBaselineComparison, VoiceEvalBaselineComparisonOptions, VoiceEvalBaselineStore, VoiceEvalBaselineSummary, VoiceEvalLink, VoiceEvalReport, VoiceEvalRoutesOptions, VoiceEvalSessionReport, VoiceEvalStatus, VoiceEvalTrendBucket } 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
@@ -7625,6 +7625,8 @@ var createVoiceDiagnosticsRoutes = (options) => {
7625
7625
  };
7626
7626
  // src/evalRoutes.ts
7627
7627
  import { Elysia as Elysia7 } from "elysia";
7628
+ import { mkdir } from "fs/promises";
7629
+ import { dirname } from "path";
7628
7630
 
7629
7631
  // src/qualityRoutes.ts
7630
7632
  import { Elysia as Elysia6 } from "elysia";
@@ -7974,6 +7976,7 @@ var createVoiceQualityRoutes = (options) => {
7974
7976
 
7975
7977
  // src/evalRoutes.ts
7976
7978
  var escapeHtml9 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7979
+ var rate2 = (count, total) => count / Math.max(1, total);
7977
7980
  var sessionTime = (events) => {
7978
7981
  const sorted = filterVoiceTraceEvents(events);
7979
7982
  return {
@@ -8044,7 +8047,69 @@ var runVoiceSessionEvals = async (options = {}) => {
8044
8047
  trend: buildTrend(limitedSessions)
8045
8048
  };
8046
8049
  };
8050
+ var summarizeEvalBaseline = (report) => {
8051
+ const failedSessionIds = report.sessions.filter((session) => session.status === "fail").map((session) => session.sessionId).sort();
8052
+ return {
8053
+ failed: report.failed,
8054
+ failedSessionIds,
8055
+ passRate: rate2(report.passed, report.total),
8056
+ passed: report.passed,
8057
+ total: report.total
8058
+ };
8059
+ };
8060
+ var compareVoiceEvalBaseline = (currentReport, baselineReport, options = {}) => {
8061
+ const baseline = summarizeEvalBaseline(baselineReport);
8062
+ const current = summarizeEvalBaseline(currentReport);
8063
+ const maxFailedDelta = options.maxFailedDelta ?? 0;
8064
+ const maxPassRateDrop = options.maxPassRateDrop ?? 0;
8065
+ const failOnNewFailedSessions = options.failOnNewFailedSessions ?? true;
8066
+ const baselineFailed = new Set(baseline.failedSessionIds);
8067
+ const currentFailed = new Set(current.failedSessionIds);
8068
+ const newFailedSessionIds = current.failedSessionIds.filter((sessionId) => !baselineFailed.has(sessionId));
8069
+ const recoveredSessionIds = baseline.failedSessionIds.filter((sessionId) => !currentFailed.has(sessionId));
8070
+ const deltas = {
8071
+ failed: current.failed - baseline.failed,
8072
+ passRate: current.passRate - baseline.passRate,
8073
+ passed: current.passed - baseline.passed,
8074
+ total: current.total - baseline.total
8075
+ };
8076
+ const reasons = [];
8077
+ if (deltas.failed > maxFailedDelta) {
8078
+ reasons.push(`Failed sessions increased by ${deltas.failed}, above allowed delta ${maxFailedDelta}.`);
8079
+ }
8080
+ if (deltas.passRate < -maxPassRateDrop) {
8081
+ reasons.push(`Pass rate dropped by ${Math.abs(deltas.passRate).toFixed(4)}, above allowed drop ${maxPassRateDrop}.`);
8082
+ }
8083
+ if (failOnNewFailedSessions && newFailedSessionIds.length > 0) {
8084
+ reasons.push(`${newFailedSessionIds.length} session(s) failed that were not failing in the baseline.`);
8085
+ }
8086
+ return {
8087
+ baseline,
8088
+ checkedAt: Date.now(),
8089
+ current,
8090
+ deltas,
8091
+ newFailedSessionIds,
8092
+ recoveredSessionIds,
8093
+ reasons,
8094
+ status: reasons.length > 0 ? "fail" : "pass"
8095
+ };
8096
+ };
8097
+ var createVoiceFileEvalBaselineStore = (filePath) => ({
8098
+ get: async () => {
8099
+ const file = Bun.file(filePath);
8100
+ if (!await file.exists()) {
8101
+ return;
8102
+ }
8103
+ const text = await file.text();
8104
+ return text.trim() ? JSON.parse(text) : undefined;
8105
+ },
8106
+ set: async (report) => {
8107
+ await mkdir(dirname(filePath), { recursive: true });
8108
+ await Bun.write(filePath, JSON.stringify(report, null, 2));
8109
+ }
8110
+ });
8047
8111
  var formatTime = (value) => value === undefined ? "unknown" : new Date(value).toLocaleString();
8112
+ var formatPercent = (value) => `${(value * 100).toFixed(2)}%`;
8048
8113
  var renderVoiceEvalHTML = (report, options = {}) => {
8049
8114
  const title = options.title ?? "AbsoluteJS Voice Evals";
8050
8115
  const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
@@ -8055,6 +8120,14 @@ var renderVoiceEvalHTML = (report, options = {}) => {
8055
8120
  }).join("") : '<tr><td colspan="7">No sessions found.</td></tr>';
8056
8121
  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}.pass{color:#166534}.fail{color:#991b1b}.status.pass{background:#dcfce7}.status.fail{background:#fee2e2}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:1rem 0}.card{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.card strong{display:block;font-size:2rem}table{border-collapse:collapse;background:white;width:100%;margin:1rem 0 2rem}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><h2>Trend</h2><table><thead><tr><th>Day</th><th>Total</th><th>Passed</th><th>Failed</th></tr></thead><tbody>${trend}</tbody></table><h2>Session Eval Results</h2><table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Turns</th><th>Errors</th><th>Last event</th><th>Failed metrics</th></tr></thead><tbody>${sessions}</tbody></table></main></body></html>`;
8057
8122
  };
8123
+ var renderVoiceEvalBaselineHTML = (comparison, options = {}) => {
8124
+ const title = options.title ?? "AbsoluteJS Voice Eval Baseline";
8125
+ const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
8126
+ const reasons = comparison.reasons.length ? comparison.reasons.map((reason) => `<li>${escapeHtml9(reason)}</li>`).join("") : "<li>No baseline regressions detected.</li>";
8127
+ const newFailures = comparison.newFailedSessionIds.length ? comparison.newFailedSessionIds.map((id) => `<li>${escapeHtml9(id)}</li>`).join("") : "<li>none</li>";
8128
+ const recovered = comparison.recoveredSessionIds.length ? comparison.recoveredSessionIds.map((id) => `<li>${escapeHtml9(id)}</li>`).join("") : "<li>none</li>";
8129
+ 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
+ };
8058
8131
  var createVoiceEvalRoutes = (options) => {
8059
8132
  const path = options.path ?? "/evals";
8060
8133
  const routes = new Elysia7({
@@ -8066,6 +8139,11 @@ var createVoiceEvalRoutes = (options) => {
8066
8139
  store: options.store,
8067
8140
  thresholds: options.thresholds
8068
8141
  });
8142
+ const getBaseline = async () => typeof options.baseline === "function" ? options.baseline() : options.baseline ?? await options.baselineStore?.get();
8143
+ const getBaselineComparison = async () => {
8144
+ const [current, baseline] = await Promise.all([getReport(), getBaseline()]);
8145
+ return baseline ? compareVoiceEvalBaseline(current, baseline, options.baselineComparison) : undefined;
8146
+ };
8069
8147
  routes.get(path, async () => {
8070
8148
  const report = await getReport();
8071
8149
  return new Response(renderVoiceEvalHTML(report, {
@@ -8086,6 +8164,53 @@ var createVoiceEvalRoutes = (options) => {
8086
8164
  }
8087
8165
  return report;
8088
8166
  });
8167
+ routes.get(`${path}/baseline`, async ({ set }) => {
8168
+ const comparison = await getBaselineComparison();
8169
+ if (!comparison) {
8170
+ set.status = 404;
8171
+ return Response.json({ error: "No voice eval baseline found." });
8172
+ }
8173
+ return new Response(renderVoiceEvalBaselineHTML(comparison, {
8174
+ links: options.links,
8175
+ title: `${options.title ?? "AbsoluteJS Voice Evals"} Baseline`
8176
+ }), {
8177
+ headers: {
8178
+ "Content-Type": "text/html; charset=utf-8",
8179
+ ...options.headers
8180
+ }
8181
+ });
8182
+ });
8183
+ routes.get(`${path}/baseline/json`, async ({ set }) => {
8184
+ const comparison = await getBaselineComparison();
8185
+ if (!comparison) {
8186
+ set.status = 404;
8187
+ return { error: "No voice eval baseline found." };
8188
+ }
8189
+ return comparison;
8190
+ });
8191
+ routes.get(`${path}/baseline/status`, async ({ set }) => {
8192
+ const comparison = await getBaselineComparison();
8193
+ if (!comparison) {
8194
+ set.status = 404;
8195
+ return { error: "No voice eval baseline found." };
8196
+ }
8197
+ if (comparison.status === "fail") {
8198
+ set.status = 503;
8199
+ }
8200
+ return comparison;
8201
+ });
8202
+ routes.post(`${path}/baseline`, async ({ set }) => {
8203
+ if (!options.baselineStore) {
8204
+ set.status = 501;
8205
+ return { error: "No voice eval baseline store configured." };
8206
+ }
8207
+ const report = await getReport();
8208
+ await options.baselineStore.set(report);
8209
+ return {
8210
+ baseline: report,
8211
+ status: "saved"
8212
+ };
8213
+ });
8089
8214
  return routes;
8090
8215
  };
8091
8216
  // src/sessionReplay.ts
@@ -8331,7 +8456,7 @@ var createVoiceSessionReplayRoutes = (options) => {
8331
8456
  return routes;
8332
8457
  };
8333
8458
  // src/fileStore.ts
8334
- import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
8459
+ import { mkdir as mkdir2, readFile, readdir, rename, rm, writeFile } from "fs/promises";
8335
8460
  import { join } from "path";
8336
8461
  var listJsonFiles = async (directory) => {
8337
8462
  try {
@@ -8351,7 +8476,7 @@ var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
8351
8476
  var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
8352
8477
  var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
8353
8478
  var writeJsonFile = async (path, value, options) => {
8354
- await mkdir(options.directory, {
8479
+ await mkdir2(options.directory, {
8355
8480
  recursive: true
8356
8481
  });
8357
8482
  const tempPath = `${path}.${crypto.randomUUID()}.tmp`;
@@ -13055,6 +13180,7 @@ export {
13055
13180
  renderVoiceOpsConsoleHTML,
13056
13181
  renderVoiceHandoffHealthHTML,
13057
13182
  renderVoiceEvalHTML,
13183
+ renderVoiceEvalBaselineHTML,
13058
13184
  renderVoiceCallReviewMarkdown,
13059
13185
  renderVoiceCallReviewHTML,
13060
13186
  renderVoiceAssistantHealthHTML,
@@ -13176,6 +13302,7 @@ export {
13176
13302
  createVoiceFileReviewStore,
13177
13303
  createVoiceFileIntegrationEventStore,
13178
13304
  createVoiceFileExternalObjectMapStore,
13305
+ createVoiceFileEvalBaselineStore,
13179
13306
  createVoiceFileAssistantMemoryStore,
13180
13307
  createVoiceExternalObjectMapId,
13181
13308
  createVoiceExternalObjectMap,
@@ -13213,6 +13340,7 @@ export {
13213
13340
  createAnthropicVoiceAssistantModel,
13214
13341
  conditionAudioChunk,
13215
13342
  completeVoiceOpsTask,
13343
+ compareVoiceEvalBaseline,
13216
13344
  claimVoiceOpsTask,
13217
13345
  buildVoiceTraceReplay,
13218
13346
  buildVoiceOpsTaskFromSLABreach,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.42",
3
+ "version": "0.0.22-beta.43",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",