@absolutejs/voice 0.0.22-beta.38 → 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/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap
8
8
  export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
9
9
  export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, createVoiceProviderRouter } from './modelAdapters';
10
10
  export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
11
+ export { createVoiceQualityRoutes, evaluateVoiceQuality, renderVoiceQualityHTML } from './qualityRoutes';
11
12
  export { createVoiceResilienceRoutes, listVoiceRoutingEvents, renderVoiceResilienceHTML } from './resilienceRoutes';
12
13
  export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
13
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';
@@ -40,6 +41,7 @@ export type { VoiceDiagnosticsRoutesOptions } from './diagnosticsRoutes';
40
41
  export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
41
42
  export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
42
43
  export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
44
+ export type { VoiceQualityMetric, VoiceQualityReport, VoiceQualityRoutesOptions, VoiceQualityStatus, VoiceQualityThresholds } from './qualityRoutes';
43
45
  export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingEvent, VoiceRoutingEventKind } from './resilienceRoutes';
44
46
  export type { VoiceIOProviderRouterEvent, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
45
47
  export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
package/dist/index.js CHANGED
@@ -8993,11 +8993,355 @@ var createGeminiVoiceAssistantModel = (options) => {
8993
8993
  }
8994
8994
  };
8995
8995
  };
8996
- // src/resilienceRoutes.ts
8996
+ // src/qualityRoutes.ts
8997
+ import { Elysia as Elysia7 } from "elysia";
8998
+
8999
+ // src/handoffHealth.ts
8997
9000
  import { Elysia as Elysia6 } from "elysia";
8998
9001
  var escapeHtml8 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
8999
- var getString5 = (value) => typeof value === "string" ? value : undefined;
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;
9000
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
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
+ };
9340
+ // src/resilienceRoutes.ts
9341
+ import { Elysia as Elysia8 } from "elysia";
9342
+ var escapeHtml10 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9343
+ var getString7 = (value) => typeof value === "string" ? value : undefined;
9344
+ var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
9001
9345
  var getBoolean2 = (value) => value === true;
9002
9346
  var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
9003
9347
  var listVoiceRoutingEvents = (events) => {
@@ -9006,23 +9350,23 @@ var listVoiceRoutingEvents = (events) => {
9006
9350
  if (event.type !== "session.error") {
9007
9351
  continue;
9008
9352
  }
9009
- const provider = getString5(event.payload.provider);
9353
+ const provider = getString7(event.payload.provider);
9010
9354
  const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
9011
9355
  if (!provider || !providerStatus) {
9012
9356
  continue;
9013
9357
  }
9014
- const kind = getString5(event.payload.kind);
9358
+ const kind = getString7(event.payload.kind);
9015
9359
  routingEvents.push({
9016
9360
  at: event.at,
9017
- attempt: getNumber3(event.payload.attempt),
9018
- elapsedMs: getNumber3(event.payload.elapsedMs),
9019
- error: getString5(event.payload.error),
9020
- fallbackProvider: getString5(event.payload.fallbackProvider),
9361
+ attempt: getNumber4(event.payload.attempt),
9362
+ elapsedMs: getNumber4(event.payload.elapsedMs),
9363
+ error: getString7(event.payload.error),
9364
+ fallbackProvider: getString7(event.payload.fallbackProvider),
9021
9365
  kind: kind === "stt" || kind === "tts" ? kind : "llm",
9022
- latencyBudgetMs: getNumber3(event.payload.latencyBudgetMs),
9023
- operation: getString5(event.payload.operation),
9366
+ latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
9367
+ operation: getString7(event.payload.operation),
9024
9368
  provider,
9025
- selectedProvider: getString5(event.payload.selectedProvider),
9369
+ selectedProvider: getString7(event.payload.selectedProvider),
9026
9370
  sessionId: event.sessionId,
9027
9371
  status: providerStatus,
9028
9372
  timedOut: getBoolean2(event.payload.timedOut),
@@ -9058,13 +9402,13 @@ var summarizeRoutingEvents = (events) => {
9058
9402
  };
9059
9403
  var renderProviderCards = (title, providers) => {
9060
9404
  if (providers.length === 0) {
9061
- return `<p class="muted">No ${escapeHtml8(title)} provider health yet.</p>`;
9405
+ return `<p class="muted">No ${escapeHtml10(title)} provider health yet.</p>`;
9062
9406
  }
9063
9407
  return `<div class="provider-grid">${providers.map((provider) => `
9064
- <article class="card provider ${escapeHtml8(provider.status)}">
9408
+ <article class="card provider ${escapeHtml10(provider.status)}">
9065
9409
  <div class="card-header">
9066
- <strong>${escapeHtml8(provider.provider)}</strong>
9067
- <span>${escapeHtml8(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
9410
+ <strong>${escapeHtml10(provider.provider)}</strong>
9411
+ <span>${escapeHtml10(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
9068
9412
  </div>
9069
9413
  <dl>
9070
9414
  <div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
@@ -9073,7 +9417,7 @@ var renderProviderCards = (title, providers) => {
9073
9417
  <div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
9074
9418
  <div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
9075
9419
  </dl>
9076
- ${provider.lastError ? `<p class="muted">${escapeHtml8(provider.lastError)}</p>` : ""}
9420
+ ${provider.lastError ? `<p class="muted">${escapeHtml10(provider.lastError)}</p>` : ""}
9077
9421
  </article>
9078
9422
  `).join("")}</div>`;
9079
9423
  };
@@ -9082,24 +9426,24 @@ var renderTimeline2 = (events) => {
9082
9426
  return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
9083
9427
  }
9084
9428
  return `<div class="timeline">${events.slice(0, 40).map((event) => `
9085
- <article class="card event ${escapeHtml8(event.status ?? "unknown")}">
9429
+ <article class="card event ${escapeHtml10(event.status ?? "unknown")}">
9086
9430
  <div class="card-header">
9087
- <strong>${escapeHtml8(event.kind.toUpperCase())} ${escapeHtml8(event.operation ?? "generate")}</strong>
9431
+ <strong>${escapeHtml10(event.kind.toUpperCase())} ${escapeHtml10(event.operation ?? "generate")}</strong>
9088
9432
  <span>${new Date(event.at).toLocaleString()}</span>
9089
9433
  </div>
9090
9434
  <p>
9091
- <span class="pill">${escapeHtml8(event.status ?? "unknown")}</span>
9092
- <span class="pill">provider: ${escapeHtml8(event.provider ?? "unknown")}</span>
9093
- ${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml8(event.fallbackProvider)}</span>` : ""}
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>` : ""}
9094
9438
  ${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
9095
9439
  </p>
9096
9440
  <dl>
9097
9441
  <div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
9098
9442
  <div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
9099
9443
  <div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
9100
- <div><dt>Session</dt><dd>${escapeHtml8(event.sessionId)}</dd></div>
9444
+ <div><dt>Session</dt><dd>${escapeHtml10(event.sessionId)}</dd></div>
9101
9445
  </dl>
9102
- ${event.error ? `<p class="muted">${escapeHtml8(event.error)}</p>` : ""}
9446
+ ${event.error ? `<p class="muted">${escapeHtml10(event.error)}</p>` : ""}
9103
9447
  </article>
9104
9448
  `).join("")}</div>`;
9105
9449
  };
@@ -9114,26 +9458,26 @@ var renderSimulationControls = (kind, simulation) => {
9114
9458
  const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
9115
9459
  const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
9116
9460
  const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
9117
- return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml8(pathPrefix)}">
9118
- <p class="muted">${escapeHtml8(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
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>
9119
9463
  <div class="simulate-actions">
9120
- ${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml8(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml8(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
9121
- ${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml8(provider.provider)}">Mark ${escapeHtml8(provider.provider)} recovered</button>`).join("")}
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("")}
9122
9466
  </div>
9123
- ${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml8(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
9467
+ ${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml10(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
9124
9468
  <pre class="simulate-output" hidden></pre>
9125
9469
  </div>`;
9126
9470
  };
9127
9471
  var renderVoiceResilienceHTML = (input) => {
9128
9472
  const summary = summarizeRoutingEvents(input.routingEvents);
9129
- const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml8(kind)}: ${String(count)}</span>`).join("");
9130
- const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml8(link.href)}">${escapeHtml8(link.label)}</a>`).join(" \xB7 ") : "";
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 ") : "";
9131
9475
  return `<!doctype html>
9132
9476
  <html lang="en">
9133
9477
  <head>
9134
9478
  <meta charset="utf-8" />
9135
9479
  <meta name="viewport" content="width=device-width, initial-scale=1" />
9136
- <title>${escapeHtml8(input.title ?? "AbsoluteJS Voice Resilience")}</title>
9480
+ <title>${escapeHtml10(input.title ?? "AbsoluteJS Voice Resilience")}</title>
9137
9481
  <style>
9138
9482
  :root { color-scheme: dark; }
9139
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; }
@@ -9269,7 +9613,7 @@ var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
9269
9613
  };
9270
9614
  var createVoiceResilienceRoutes = (options) => {
9271
9615
  const path = options.path ?? "/resilience";
9272
- const routes = new Elysia6({
9616
+ const routes = new Elysia8({
9273
9617
  name: options.name ?? "absolutejs-voice-resilience"
9274
9618
  }).get(path, async () => {
9275
9619
  const events = await options.store.list();
@@ -10150,7 +10494,7 @@ var createVoiceMemoryStore = () => {
10150
10494
  return { get, getOrCreate, list, remove, set };
10151
10495
  };
10152
10496
  // src/opsWebhook.ts
10153
- import { Elysia as Elysia7 } from "elysia";
10497
+ import { Elysia as Elysia9 } from "elysia";
10154
10498
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
10155
10499
  var signVoiceOpsWebhookBody = async (input) => {
10156
10500
  const encoder = new TextEncoder;
@@ -10280,7 +10624,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
10280
10624
  };
10281
10625
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
10282
10626
  const path = options.path ?? "/api/voice-ops/webhook";
10283
- return new Elysia7().post(path, async ({ body, request, set }) => {
10627
+ return new Elysia9().post(path, async ({ body, request, set }) => {
10284
10628
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
10285
10629
  if (options.signingSecret) {
10286
10630
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -10312,198 +10656,6 @@ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
10312
10656
  parse: "text"
10313
10657
  });
10314
10658
  };
10315
- // src/handoffHealth.ts
10316
- import { Elysia as Elysia8 } from "elysia";
10317
- var escapeHtml9 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10318
- var getString6 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
10319
- var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
10320
- var increment3 = (record, key) => {
10321
- record[key] = (record[key] ?? 0) + 1;
10322
- };
10323
- var normalizeDelivery = (adapterId, value) => {
10324
- const record = value && typeof value === "object" ? value : {};
10325
- return {
10326
- adapterId: getString6(record.adapterId) ?? adapterId,
10327
- adapterKind: getString6(record.adapterKind),
10328
- deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
10329
- deliveredTo: getString6(record.deliveredTo),
10330
- error: getString6(record.error),
10331
- status: isStatus(record.status) ? record.status : "failed"
10332
- };
10333
- };
10334
- var normalizeDeliveries = (payload) => {
10335
- const deliveries = payload.deliveries;
10336
- if (!deliveries || typeof deliveries !== "object") {
10337
- return [];
10338
- }
10339
- return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
10340
- };
10341
- var resolveReplayHref = (event, replayHref) => {
10342
- if (replayHref === false) {
10343
- return;
10344
- }
10345
- if (typeof replayHref === "function") {
10346
- return replayHref(event);
10347
- }
10348
- return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
10349
- };
10350
- var summarizeVoiceHandoffHealth = async (options = {}) => {
10351
- const sourceEvents = options.events ?? await options.store?.list() ?? [];
10352
- const search = options.q?.trim().toLowerCase();
10353
- const byAction = {};
10354
- const byAdapter = {};
10355
- const byStatus = {
10356
- delivered: 0,
10357
- failed: 0,
10358
- skipped: 0
10359
- };
10360
- const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
10361
- const status = isStatus(event.payload.status) ? event.payload.status : "failed";
10362
- const deliveries = normalizeDeliveries(event.payload);
10363
- const item = {
10364
- action: getString6(event.payload.action),
10365
- at: event.at,
10366
- deliveries,
10367
- reason: getString6(event.payload.reason),
10368
- sessionId: event.sessionId,
10369
- status,
10370
- target: getString6(event.payload.target)
10371
- };
10372
- return {
10373
- ...item,
10374
- replayHref: resolveReplayHref(item, options.replayHref)
10375
- };
10376
- }).filter((event) => {
10377
- if (options.status && options.status !== "all" && event.status !== options.status) {
10378
- return false;
10379
- }
10380
- if (!search) {
10381
- return true;
10382
- }
10383
- return [
10384
- event.action,
10385
- event.reason,
10386
- event.sessionId,
10387
- event.status,
10388
- event.target,
10389
- ...event.deliveries.flatMap((delivery) => [
10390
- delivery.adapterId,
10391
- delivery.adapterKind,
10392
- delivery.deliveredTo,
10393
- delivery.error,
10394
- delivery.status
10395
- ])
10396
- ].some((value) => value?.toLowerCase().includes(search));
10397
- }).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
10398
- for (const event of events) {
10399
- byStatus[event.status] += 1;
10400
- if (event.action) {
10401
- increment3(byAction, event.action);
10402
- }
10403
- for (const delivery of event.deliveries) {
10404
- byAdapter[delivery.adapterId] ??= {
10405
- delivered: 0,
10406
- failed: 0,
10407
- skipped: 0
10408
- };
10409
- byAdapter[delivery.adapterId][delivery.status] += 1;
10410
- }
10411
- }
10412
- return {
10413
- byAction,
10414
- byAdapter,
10415
- byStatus,
10416
- events,
10417
- failed: byStatus.failed,
10418
- total: events.length
10419
- };
10420
- };
10421
- var renderMetricGrid = (summary) => [
10422
- '<section class="voice-handoff-health-grid">',
10423
- `<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
10424
- `<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
10425
- `<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
10426
- `<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
10427
- "</section>"
10428
- ].join("");
10429
- var renderActionSummary = (summary) => {
10430
- const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
10431
- const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
10432
- return [
10433
- '<section class="voice-handoff-health-columns">',
10434
- "<article><h3>Actions</h3>",
10435
- actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml9(action)}: ${String(count)}</li>`).join("")}</ul>`,
10436
- "</article>",
10437
- "<article><h3>Adapters</h3>",
10438
- adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml9(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
10439
- "</article>",
10440
- "</section>"
10441
- ].join("");
10442
- };
10443
- var renderVoiceHandoffHealthHTML = (summary) => [
10444
- '<div class="voice-handoff-health">',
10445
- renderMetricGrid(summary),
10446
- renderActionSummary(summary),
10447
- "<section>",
10448
- "<h3>Recent Handoffs</h3>",
10449
- summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
10450
- '<div class="voice-handoff-health-events">',
10451
- ...summary.events.map((event) => [
10452
- `<article class="${escapeHtml9(event.status)}">`,
10453
- '<div class="voice-handoff-health-event-header">',
10454
- `<strong>${escapeHtml9(event.action ?? "handoff")}</strong>`,
10455
- `<span>${escapeHtml9(event.status)}</span>`,
10456
- "</div>",
10457
- `<p><small>${escapeHtml9(event.sessionId)}</small></p>`,
10458
- event.target ? `<p>Target: ${escapeHtml9(event.target)}</p>` : "",
10459
- event.reason ? `<p>Reason: ${escapeHtml9(event.reason)}</p>` : "",
10460
- event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
10461
- "<li>",
10462
- `${escapeHtml9(delivery.adapterId)}: ${escapeHtml9(delivery.status)}`,
10463
- delivery.deliveredTo ? ` to ${escapeHtml9(delivery.deliveredTo)}` : "",
10464
- delivery.error ? ` (${escapeHtml9(delivery.error)})` : "",
10465
- "</li>"
10466
- ].join("")).join("")}</ul>` : "",
10467
- event.replayHref ? `<p><a href="${escapeHtml9(event.replayHref)}">Open replay</a></p>` : "",
10468
- "</article>"
10469
- ].join("")),
10470
- "</div>"
10471
- ].join(""),
10472
- "</section>",
10473
- "</div>"
10474
- ].join("");
10475
- var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
10476
- ...options,
10477
- limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
10478
- q: query?.q ?? options.q,
10479
- status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
10480
- });
10481
- var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
10482
- const summary = await summarizeVoiceHandoffHealth({
10483
- ...options,
10484
- limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
10485
- q: query?.q ?? options.q,
10486
- status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
10487
- });
10488
- const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
10489
- return new Response(body, {
10490
- headers: {
10491
- "Content-Type": "text/html; charset=utf-8",
10492
- ...options.headers
10493
- }
10494
- });
10495
- };
10496
- var createVoiceHandoffHealthRoutes = (options = {}) => {
10497
- const path = options.path ?? "/api/voice-handoffs";
10498
- const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
10499
- const routes = new Elysia8({
10500
- name: options.name ?? "absolutejs-voice-handoff-health"
10501
- }).get(path, createVoiceHandoffHealthJSONHandler(options));
10502
- if (htmlPath) {
10503
- routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
10504
- }
10505
- return routes;
10506
- };
10507
10659
  // src/queue.ts
10508
10660
  var releaseLeaseScript = `
10509
10661
  if redis.call("GET", KEYS[1]) == ARGV[1] then
@@ -12652,6 +12804,7 @@ export {
12652
12804
  renderVoiceTraceHTML,
12653
12805
  renderVoiceSessionsHTML,
12654
12806
  renderVoiceResilienceHTML,
12807
+ renderVoiceQualityHTML,
12655
12808
  renderVoiceProviderHealthHTML,
12656
12809
  renderVoiceHandoffHealthHTML,
12657
12810
  renderVoiceCallReviewMarkdown,
@@ -12673,6 +12826,7 @@ export {
12673
12826
  failVoiceOpsTask,
12674
12827
  exportVoiceTrace,
12675
12828
  evaluateVoiceTrace,
12829
+ evaluateVoiceQuality,
12676
12830
  encodeTwilioMulawBase64,
12677
12831
  deliverVoiceTraceEventsToSinks,
12678
12832
  deliverVoiceIntegrationEventToSinks,
@@ -12723,6 +12877,7 @@ export {
12723
12877
  createVoiceResilienceRoutes,
12724
12878
  createVoiceRedisTaskLeaseCoordinator,
12725
12879
  createVoiceRedisIdempotencyStore,
12880
+ createVoiceQualityRoutes,
12726
12881
  createVoiceProviderRouter,
12727
12882
  createVoiceProviderHealthRoutes,
12728
12883
  createVoiceProviderHealthJSONHandler,
@@ -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
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.38",
3
+ "version": "0.0.22-beta.39",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",