@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 +2 -0
- package/dist/index.js +381 -226
- package/dist/qualityRoutes.d.ts +69 -0
- package/package.json +1 -1
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/
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9305
|
+
var formatMetricValue = (metric) => metric.unit === "rate" ? `${(metric.actual * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.actual)}ms` : String(metric.actual);
|
|
9306
|
+
var formatThreshold = (metric) => metric.unit === "rate" ? `${(metric.threshold * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.threshold)}ms` : String(metric.threshold);
|
|
9307
|
+
var renderVoiceQualityHTML = (report) => {
|
|
9308
|
+
const rows = Object.entries(report.metrics).map(([key, metric]) => `<tr class="${metric.pass ? "pass" : "fail"}"><td>${escapeHtml9(metric.label)}</td><td>${escapeHtml9(formatMetricValue(metric))}</td><td>${escapeHtml9(formatThreshold(metric))}</td><td>${metric.pass ? "pass" : "fail"}</td><td><code>${escapeHtml9(key)}</code></td></tr>`).join("");
|
|
9309
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>AbsoluteJS Voice Quality</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}.status{border-radius:999px;display:inline-flex;padding:.35rem .75rem;font-weight:800}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}table{border-collapse:collapse;width:100%;background:white;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}.pass td{border-left:4px solid #16a34a}.fail td{border-left:4px solid #dc2626}code{background:#f3f4f6;padding:.15rem .3rem;border-radius:.3rem}</style></head><body><main><h1>Voice quality gates</h1><p class="status ${report.status}">${report.status}</p><p>${report.eventCount} event(s) checked.</p><table><thead><tr><th>Metric</th><th>Actual</th><th>Threshold</th><th>Status</th><th>Key</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
|
|
9310
|
+
};
|
|
9311
|
+
var createVoiceQualityRoutes = (options) => {
|
|
9312
|
+
const path = options.path ?? "/quality";
|
|
9313
|
+
const routes = new Elysia7({
|
|
9314
|
+
name: options.name ?? "absolutejs-voice-quality"
|
|
9315
|
+
});
|
|
9316
|
+
const getReport = () => evaluateVoiceQuality({
|
|
9317
|
+
events: options.events,
|
|
9318
|
+
store: options.store,
|
|
9319
|
+
thresholds: options.thresholds
|
|
9320
|
+
});
|
|
9321
|
+
routes.get(path, async () => {
|
|
9322
|
+
const report = await getReport();
|
|
9323
|
+
return new Response(renderVoiceQualityHTML(report), {
|
|
9324
|
+
headers: {
|
|
9325
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9326
|
+
...options.headers
|
|
9327
|
+
}
|
|
9328
|
+
});
|
|
9329
|
+
});
|
|
9330
|
+
routes.get(`${path}/json`, async () => getReport());
|
|
9331
|
+
routes.get(`${path}/status`, async ({ set }) => {
|
|
9332
|
+
const report = await getReport();
|
|
9333
|
+
if (report.status === "fail") {
|
|
9334
|
+
set.status = 503;
|
|
9335
|
+
}
|
|
9336
|
+
return report;
|
|
9337
|
+
});
|
|
9338
|
+
return routes;
|
|
9339
|
+
};
|
|
9340
|
+
// src/resilienceRoutes.ts
|
|
9341
|
+
import { Elysia as Elysia8 } from "elysia";
|
|
9342
|
+
var escapeHtml10 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9343
|
+
var getString7 = (value) => typeof value === "string" ? value : undefined;
|
|
9344
|
+
var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
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 =
|
|
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 =
|
|
9358
|
+
const kind = getString7(event.payload.kind);
|
|
9015
9359
|
routingEvents.push({
|
|
9016
9360
|
at: event.at,
|
|
9017
|
-
attempt:
|
|
9018
|
-
elapsedMs:
|
|
9019
|
-
error:
|
|
9020
|
-
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:
|
|
9023
|
-
operation:
|
|
9366
|
+
latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
|
|
9367
|
+
operation: getString7(event.payload.operation),
|
|
9024
9368
|
provider,
|
|
9025
|
-
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 ${
|
|
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 ${
|
|
9408
|
+
<article class="card provider ${escapeHtml10(provider.status)}">
|
|
9065
9409
|
<div class="card-header">
|
|
9066
|
-
<strong>${
|
|
9067
|
-
<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">${
|
|
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 ${
|
|
9429
|
+
<article class="card event ${escapeHtml10(event.status ?? "unknown")}">
|
|
9086
9430
|
<div class="card-header">
|
|
9087
|
-
<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">${
|
|
9092
|
-
<span class="pill">provider: ${
|
|
9093
|
-
${event.fallbackProvider ? `<span class="pill">fallback: ${
|
|
9435
|
+
<span class="pill">${escapeHtml10(event.status ?? "unknown")}</span>
|
|
9436
|
+
<span class="pill">provider: ${escapeHtml10(event.provider ?? "unknown")}</span>
|
|
9437
|
+
${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml10(event.fallbackProvider)}</span>` : ""}
|
|
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>${
|
|
9444
|
+
<div><dt>Session</dt><dd>${escapeHtml10(event.sessionId)}</dd></div>
|
|
9101
9445
|
</dl>
|
|
9102
|
-
${event.error ? `<p class="muted">${
|
|
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="${
|
|
9118
|
-
<p class="muted">${
|
|
9461
|
+
return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml10(pathPrefix)}">
|
|
9462
|
+
<p class="muted">${escapeHtml10(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
|
|
9119
9463
|
<div class="simulate-actions">
|
|
9120
|
-
${failureProviders.map((provider) => `<button type="button" data-provider-fail="${
|
|
9121
|
-
${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${
|
|
9464
|
+
${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml10(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml10(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
|
|
9465
|
+
${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml10(provider.provider)}">Mark ${escapeHtml10(provider.provider)} recovered</button>`).join("")}
|
|
9122
9466
|
</div>
|
|
9123
|
-
${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${
|
|
9467
|
+
${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml10(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
|
|
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">${
|
|
9130
|
-
const links = input.links?.length ? input.links.map((link) => `<a href="${
|
|
9473
|
+
const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml10(kind)}: ${String(count)}</span>`).join("");
|
|
9474
|
+
const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml10(link.href)}">${escapeHtml10(link.label)}</a>`).join(" \xB7 ") : "";
|
|
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>${
|
|
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
|
|
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
|
|
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
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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
|
+
}>;
|