@absolutejs/voice 0.0.22-beta.89 → 0.0.22-beta.90
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/appKit.d.ts +3 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +324 -84
- package/dist/traceTimeline.d.ts +93 -0
- package/package.json +1 -1
package/dist/appKit.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { type VoiceQualityRoutesOptions } from './qualityRoutes';
|
|
|
11
11
|
import { type VoiceResilienceRoutesOptions } from './resilienceRoutes';
|
|
12
12
|
import { type VoiceSessionListRoutesOptions, type VoiceSessionReplayRoutesOptions } from './sessionReplay';
|
|
13
13
|
import { type VoiceTraceEventStore } from './trace';
|
|
14
|
-
|
|
14
|
+
import { type VoiceTraceTimelineRoutesOptions } from './traceTimeline';
|
|
15
|
+
export type VoiceAppKitSurface = 'assistantHealth' | 'diagnostics' | 'evals' | 'handoffs' | 'opsConsole' | 'providerCapabilities' | 'providerHealth' | 'productionReadiness' | 'quality' | 'resilience' | 'sessionReplay' | 'sessions' | 'traceTimeline';
|
|
15
16
|
export type VoiceAppKitLink = VoiceEvalLink & {
|
|
16
17
|
description?: string;
|
|
17
18
|
statusHref?: string;
|
|
@@ -37,6 +38,7 @@ export type VoiceAppKitRoutesOptions<TProvider extends string = string> = {
|
|
|
37
38
|
store: VoiceTraceEventStore;
|
|
38
39
|
sttProviders?: readonly string[];
|
|
39
40
|
title?: string;
|
|
41
|
+
traceTimeline?: false | Partial<VoiceTraceTimelineRoutesOptions>;
|
|
40
42
|
ttsProviders?: readonly string[];
|
|
41
43
|
};
|
|
42
44
|
export type VoiceAppKitStatus = 'pass' | 'fail';
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export { createVoiceQualityRoutes, evaluateVoiceQuality, renderVoiceQualityHTML
|
|
|
24
24
|
export { createVoiceResilienceRoutes, createVoiceRoutingDecisionSummary, listVoiceRoutingEvents, renderVoiceResilienceHTML, summarizeVoiceRoutingDecision, summarizeVoiceRoutingSessions } from './resilienceRoutes';
|
|
25
25
|
export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
|
|
26
26
|
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';
|
|
27
|
+
export { createVoiceTraceTimelineRoutes, renderVoiceTraceTimelineHTML, renderVoiceTraceTimelineSessionHTML, summarizeVoiceTraceTimeline } from './traceTimeline';
|
|
27
28
|
export { createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTelephonyWebhookIdempotencyStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore } from './sqliteStore';
|
|
28
29
|
export { createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrationEventStore, createVoicePostgresReviewStore, createVoicePostgresRuntimeStorage, createVoicePostgresSessionStore, createVoicePostgresTaskStore, createVoicePostgresTelephonyWebhookIdempotencyStore, createVoicePostgresTraceSinkDeliveryStore, createVoicePostgresTraceEventStore } from './postgresStore';
|
|
29
30
|
export { createVoiceS3ReviewStore } from './s3Store';
|
|
@@ -79,6 +80,7 @@ export type { VoiceHandoffHealthDelivery, VoiceHandoffHealthEvent, VoiceHandoffH
|
|
|
79
80
|
export type { StoredVoiceCallReviewArtifact, VoiceCallReviewArtifact, VoiceCallReviewConfig, VoiceCallReviewPostCallSummary, VoiceCallReviewRecorder, VoiceCallReviewRecorderOptions, VoiceCallReviewStore, VoiceCallReviewSummary, VoiceCallReviewTimelineEvent } from './testing/review';
|
|
80
81
|
export type { VoiceFileRuntimeStorage, VoiceFileStoreOptions } from './fileStore';
|
|
81
82
|
export type { StoredVoiceTraceEvent, VoiceTraceEvaluation, VoiceTraceEvaluationOptions, VoiceTraceEvent, VoiceTraceEventFilter, VoiceTraceEventStore, VoiceTraceEventType, VoiceTraceIssue, VoiceTraceIssueSeverity, VoiceTraceHTTPSinkOptions, VoiceTracePruneFilter, VoiceTracePruneOptions, VoiceTracePruneResult, VoiceTraceRedactionConfig, VoiceTraceRedactionOptions, VoiceTraceRedactionReplacement, VoiceResolvedTraceRedactionOptions, VoiceTraceSink, VoiceTraceSinkDeliveryQueueStatus, VoiceTraceSinkDeliveryRecord, VoiceTraceSinkDeliveryResult, VoiceTraceSinkDeliveryStatus, VoiceTraceSinkDeliveryStore, VoiceTraceSinkFanoutResult, VoiceTraceSinkStoreOptions, VoiceTraceSummary } from './trace';
|
|
83
|
+
export type { VoiceTraceTimelineEvent, VoiceTraceTimelineProviderSummary, VoiceTraceTimelineReport, VoiceTraceTimelineRoutesOptions, VoiceTraceTimelineSession } from './traceTimeline';
|
|
82
84
|
export type { VoicePostgresClient, VoicePostgresRuntimeStorage, VoicePostgresStoreOptions } from './postgresStore';
|
|
83
85
|
export type { VoiceOpsTaskLease, VoiceOpsTaskWorker, VoiceOpsTaskWorkerOptions, VoiceHandoffDeliveryQueueSummary, VoiceHandoffDeliveryWorkerLoop, VoiceHandoffDeliveryWorkerLoopOptions, VoiceHandoffDeliveryWorkerOptions, VoiceHandoffDeliveryWorkerResult, VoiceIdempotencyStore, VoiceIntegrationEventQueueSummary, VoiceIntegrationSinkWorkerLoop, VoiceIntegrationSinkWorkerLoopOptions, VoiceIntegrationSinkWorkerOptions, VoiceIntegrationSinkWorkerResult, VoiceRedisIdempotencyClient, VoiceRedisIdempotencyStoreOptions, VoiceRedisTelephonyWebhookIdempotencyClient, VoiceRedisTelephonyWebhookIdempotencyStoreOptions, VoiceRedisTaskLeaseClient, VoiceRedisTaskLeaseCoordinator, VoiceRedisTaskLeaseCoordinatorOptions, VoiceTraceSinkDeliveryQueueSummary, VoiceTraceSinkDeliveryWorkerLoop, VoiceTraceSinkDeliveryWorkerLoopOptions, VoiceTraceSinkDeliveryWorkerOptions, VoiceTraceSinkDeliveryWorkerResult, VoiceOpsTaskClaimFilters, VoiceWebhookDeliveryWorkerLoop, VoiceWebhookDeliveryWorkerLoopOptions, VoiceWebhookDeliveryWorkerOptions, VoiceWebhookDeliveryWorkerResult, VoiceOpsTaskProcessorWorkerLoop, VoiceOpsTaskProcessorWorkerLoopOptions, VoiceOpsTaskProcessorWorkerOptions, VoiceOpsTaskProcessorWorkerResult, VoiceOpsTaskQueueSummary } from './queue';
|
|
84
86
|
export type { VoiceS3ReviewStoreClient, VoiceS3ReviewStoreFile, VoiceS3ReviewStoreOptions } from './s3Store';
|
package/dist/index.js
CHANGED
|
@@ -5474,7 +5474,7 @@ var voice = (config) => {
|
|
|
5474
5474
|
}).use(htmxRoutes());
|
|
5475
5475
|
};
|
|
5476
5476
|
// src/appKit.ts
|
|
5477
|
-
import { Elysia as
|
|
5477
|
+
import { Elysia as Elysia15 } from "elysia";
|
|
5478
5478
|
|
|
5479
5479
|
// src/assistantHealth.ts
|
|
5480
5480
|
import { Elysia as Elysia3 } from "elysia";
|
|
@@ -9821,6 +9821,232 @@ var createVoiceProductionReadinessRoutes = (options) => {
|
|
|
9821
9821
|
return routes;
|
|
9822
9822
|
};
|
|
9823
9823
|
|
|
9824
|
+
// src/traceTimeline.ts
|
|
9825
|
+
import { Elysia as Elysia14 } from "elysia";
|
|
9826
|
+
var escapeHtml16 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9827
|
+
var getString9 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
9828
|
+
var getNumber5 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
9829
|
+
var firstString = (payload, keys) => {
|
|
9830
|
+
for (const key of keys) {
|
|
9831
|
+
const value = getString9(payload[key]);
|
|
9832
|
+
if (value) {
|
|
9833
|
+
return value;
|
|
9834
|
+
}
|
|
9835
|
+
}
|
|
9836
|
+
return;
|
|
9837
|
+
};
|
|
9838
|
+
var firstNumber = (payload, keys) => {
|
|
9839
|
+
for (const key of keys) {
|
|
9840
|
+
const value = getNumber5(payload[key]);
|
|
9841
|
+
if (value !== undefined) {
|
|
9842
|
+
return value;
|
|
9843
|
+
}
|
|
9844
|
+
}
|
|
9845
|
+
return;
|
|
9846
|
+
};
|
|
9847
|
+
var eventProvider = (event) => firstString(event.payload, [
|
|
9848
|
+
"provider",
|
|
9849
|
+
"selectedProvider",
|
|
9850
|
+
"fallbackProvider",
|
|
9851
|
+
"variantId"
|
|
9852
|
+
]);
|
|
9853
|
+
var eventStatus = (event) => firstString(event.payload, [
|
|
9854
|
+
"providerStatus",
|
|
9855
|
+
"status",
|
|
9856
|
+
"disposition",
|
|
9857
|
+
"type",
|
|
9858
|
+
"reason"
|
|
9859
|
+
]);
|
|
9860
|
+
var eventElapsedMs = (event) => firstNumber(event.payload, ["elapsedMs", "latencyMs", "durationMs"]);
|
|
9861
|
+
var timelineLabel = (event) => {
|
|
9862
|
+
switch (event.type) {
|
|
9863
|
+
case "call.lifecycle":
|
|
9864
|
+
return `Call ${eventStatus(event) ?? "lifecycle"}`;
|
|
9865
|
+
case "turn.transcript":
|
|
9866
|
+
return event.payload.isFinal === true ? "Final transcript" : "Partial transcript";
|
|
9867
|
+
case "turn.committed":
|
|
9868
|
+
return `Committed turn${getString9(event.payload.reason) ? ` (${getString9(event.payload.reason)})` : ""}`;
|
|
9869
|
+
case "turn.assistant":
|
|
9870
|
+
return "Assistant reply";
|
|
9871
|
+
case "agent.model":
|
|
9872
|
+
return `Model call${eventProvider(event) ? ` via ${eventProvider(event)}` : ""}`;
|
|
9873
|
+
case "agent.tool":
|
|
9874
|
+
return `Tool ${getString9(event.payload.toolName) ?? "call"}`;
|
|
9875
|
+
case "agent.handoff":
|
|
9876
|
+
return `Agent handoff${getString9(event.payload.targetAgentId) ? ` to ${getString9(event.payload.targetAgentId)}` : ""}`;
|
|
9877
|
+
case "assistant.run":
|
|
9878
|
+
return `Assistant run${eventProvider(event) ? ` via ${eventProvider(event)}` : ""}`;
|
|
9879
|
+
case "assistant.guardrail":
|
|
9880
|
+
return `Guardrail ${eventStatus(event) ?? "check"}`;
|
|
9881
|
+
case "call.handoff":
|
|
9882
|
+
return `Call handoff ${eventStatus(event) ?? ""}`.trim();
|
|
9883
|
+
case "session.error":
|
|
9884
|
+
return `Error${getString9(event.payload.error) ? `: ${getString9(event.payload.error)}` : ""}`;
|
|
9885
|
+
case "turn.cost":
|
|
9886
|
+
return "Cost telemetry";
|
|
9887
|
+
case "workflow.contract":
|
|
9888
|
+
return `Workflow contract ${eventStatus(event) ?? ""}`.trim();
|
|
9889
|
+
default:
|
|
9890
|
+
return event.type;
|
|
9891
|
+
}
|
|
9892
|
+
};
|
|
9893
|
+
var summarizeProviders = (events) => {
|
|
9894
|
+
const entries = new Map;
|
|
9895
|
+
const getEntry = (provider) => {
|
|
9896
|
+
const existing = entries.get(provider);
|
|
9897
|
+
if (existing) {
|
|
9898
|
+
return existing;
|
|
9899
|
+
}
|
|
9900
|
+
const entry = {
|
|
9901
|
+
elapsed: [],
|
|
9902
|
+
errorCount: 0,
|
|
9903
|
+
eventCount: 0,
|
|
9904
|
+
fallbackCount: 0,
|
|
9905
|
+
successCount: 0,
|
|
9906
|
+
timeoutCount: 0
|
|
9907
|
+
};
|
|
9908
|
+
entries.set(provider, entry);
|
|
9909
|
+
return entry;
|
|
9910
|
+
};
|
|
9911
|
+
for (const event of events) {
|
|
9912
|
+
const provider = eventProvider(event);
|
|
9913
|
+
if (!provider) {
|
|
9914
|
+
continue;
|
|
9915
|
+
}
|
|
9916
|
+
const entry = getEntry(provider);
|
|
9917
|
+
const status = eventStatus(event);
|
|
9918
|
+
const elapsedMs = eventElapsedMs(event);
|
|
9919
|
+
entry.eventCount += 1;
|
|
9920
|
+
if (elapsedMs !== undefined) {
|
|
9921
|
+
entry.elapsed.push(elapsedMs);
|
|
9922
|
+
}
|
|
9923
|
+
if (status === "success") {
|
|
9924
|
+
entry.successCount += 1;
|
|
9925
|
+
}
|
|
9926
|
+
if (status === "fallback") {
|
|
9927
|
+
entry.fallbackCount += 1;
|
|
9928
|
+
}
|
|
9929
|
+
if (status === "error") {
|
|
9930
|
+
entry.errorCount += 1;
|
|
9931
|
+
}
|
|
9932
|
+
if (status === "timeout") {
|
|
9933
|
+
entry.timeoutCount += 1;
|
|
9934
|
+
}
|
|
9935
|
+
}
|
|
9936
|
+
return [...entries.entries()].map(([provider, entry]) => ({
|
|
9937
|
+
averageElapsedMs: entry.elapsed.length > 0 ? Math.round(entry.elapsed.reduce((total, value) => total + value, 0) / entry.elapsed.length) : undefined,
|
|
9938
|
+
errorCount: entry.errorCount,
|
|
9939
|
+
eventCount: entry.eventCount,
|
|
9940
|
+
fallbackCount: entry.fallbackCount,
|
|
9941
|
+
maxElapsedMs: entry.elapsed.length > 0 ? Math.max(...entry.elapsed) : undefined,
|
|
9942
|
+
provider,
|
|
9943
|
+
successCount: entry.successCount,
|
|
9944
|
+
timeoutCount: entry.timeoutCount
|
|
9945
|
+
})).sort((left, right) => right.eventCount - left.eventCount);
|
|
9946
|
+
};
|
|
9947
|
+
var summarizeVoiceTraceTimeline = (events, options = {}) => {
|
|
9948
|
+
const source = options.redact ? redactVoiceTraceEvents(events, options.redact) : events;
|
|
9949
|
+
const grouped = new Map;
|
|
9950
|
+
for (const event of filterVoiceTraceEvents(source)) {
|
|
9951
|
+
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
9952
|
+
}
|
|
9953
|
+
const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
|
|
9954
|
+
const sorted = filterVoiceTraceEvents(sessionEvents);
|
|
9955
|
+
const summary = summarizeVoiceTrace(sorted);
|
|
9956
|
+
const evaluation = evaluateVoiceTrace(sorted, options.evaluation);
|
|
9957
|
+
const startedAt = summary.startedAt ?? sorted[0]?.at ?? 0;
|
|
9958
|
+
const status = summary.failed ? "failed" : evaluation.issues.length > 0 ? "warning" : "healthy";
|
|
9959
|
+
return {
|
|
9960
|
+
endedAt: summary.endedAt,
|
|
9961
|
+
evaluation,
|
|
9962
|
+
events: sorted.map((event) => ({
|
|
9963
|
+
at: event.at,
|
|
9964
|
+
elapsedMs: eventElapsedMs(event),
|
|
9965
|
+
id: event.id,
|
|
9966
|
+
label: timelineLabel(event),
|
|
9967
|
+
offsetMs: Math.max(0, event.at - startedAt),
|
|
9968
|
+
provider: eventProvider(event),
|
|
9969
|
+
status: eventStatus(event),
|
|
9970
|
+
turnId: event.turnId,
|
|
9971
|
+
type: event.type
|
|
9972
|
+
})),
|
|
9973
|
+
lastEventAt: sorted.at(-1)?.at,
|
|
9974
|
+
providers: summarizeProviders(sorted),
|
|
9975
|
+
sessionId,
|
|
9976
|
+
startedAt: summary.startedAt,
|
|
9977
|
+
status,
|
|
9978
|
+
summary
|
|
9979
|
+
};
|
|
9980
|
+
}).sort((left, right) => (right.lastEventAt ?? 0) - (left.lastEventAt ?? 0)).slice(0, options.limit ?? 50);
|
|
9981
|
+
return {
|
|
9982
|
+
checkedAt: Date.now(),
|
|
9983
|
+
failed: sessions.filter((session) => session.status === "failed").length,
|
|
9984
|
+
sessions,
|
|
9985
|
+
total: sessions.length,
|
|
9986
|
+
warnings: sessions.filter((session) => session.status === "warning").length
|
|
9987
|
+
};
|
|
9988
|
+
};
|
|
9989
|
+
var formatMs = (value) => value === undefined ? "n/a" : `${String(value)}ms`;
|
|
9990
|
+
var renderProviderCards2 = (session) => session.providers.length === 0 ? '<p class="muted">No provider events recorded for this session.</p>' : `<div class="providers">${session.providers.map((provider) => `<article><strong>${escapeHtml16(provider.provider)}</strong><dl><div><dt>Events</dt><dd>${String(provider.eventCount)}</dd></div><div><dt>Avg</dt><dd>${formatMs(provider.averageElapsedMs)}</dd></div><div><dt>Max</dt><dd>${formatMs(provider.maxElapsedMs)}</dd></div><div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div><div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div><div><dt>Timeouts</dt><dd>${String(provider.timeoutCount)}</dd></div></dl></article>`).join("")}</div>`;
|
|
9991
|
+
var renderVoiceTraceTimelineSessionHTML = (session, options = {}) => {
|
|
9992
|
+
const events = session.events.map((event) => `<tr class="${escapeHtml16(event.status ?? "")}"><td>+${String(event.offsetMs)}ms</td><td>${escapeHtml16(event.type)}</td><td>${escapeHtml16(event.label)}</td><td>${escapeHtml16(event.provider ?? "")}</td><td>${escapeHtml16(event.status ?? "")}</td><td>${formatMs(event.elapsedMs)}</td></tr>`).join("");
|
|
9993
|
+
const issues = session.evaluation.issues.length ? session.evaluation.issues.map((issue) => `<li class="${escapeHtml16(issue.severity)}">${escapeHtml16(issue.code)}: ${escapeHtml16(issue.message)}</li>`).join("") : "<li>none</li>";
|
|
9994
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml16(options.title ?? "Voice Trace Timeline")}</title><style>${timelineCSS}</style></head><body><main><a href="/traces">Back to traces</a><header><p class="eyebrow">Call timeline</p><h1>${escapeHtml16(session.sessionId)}</h1><p class="status ${escapeHtml16(session.status)}">${escapeHtml16(session.status)}</p></header><section class="metrics"><article><span>Events</span><strong>${String(session.summary.eventCount)}</strong></article><article><span>Turns</span><strong>${String(session.summary.turnCount)}</strong></article><article><span>Errors</span><strong>${String(session.summary.errorCount)}</strong></article><article><span>Duration</span><strong>${formatMs(session.summary.callDurationMs)}</strong></article></section><section><h2>Providers</h2>${renderProviderCards2(session)}</section><section><h2>Issues</h2><ul>${issues}</ul></section><section><h2>Timeline</h2><table><thead><tr><th>Offset</th><th>Type</th><th>Event</th><th>Provider</th><th>Status</th><th>Latency</th></tr></thead><tbody>${events}</tbody></table></section></main></body></html>`;
|
|
9995
|
+
};
|
|
9996
|
+
var renderSessionRows = (report) => report.sessions.length === 0 ? '<tr><td colspan="7">No trace events recorded yet.</td></tr>' : report.sessions.map((session) => `<tr class="${escapeHtml16(session.status)}"><td><a href="/traces/${encodeURIComponent(session.sessionId)}">${escapeHtml16(session.sessionId)}</a></td><td>${escapeHtml16(session.status)}</td><td>${String(session.summary.eventCount)}</td><td>${String(session.summary.turnCount)}</td><td>${String(session.summary.errorCount)}</td><td>${formatMs(session.summary.callDurationMs)}</td><td>${session.providers.map((provider) => escapeHtml16(provider.provider)).join(", ")}</td></tr>`).join("");
|
|
9997
|
+
var timelineCSS = "body{background:#0f1318;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}a{color:#fbbf24}.eyebrow{color:#fbbf24;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.5rem);line-height:.92;margin:.2rem 0 1rem}.status{border:1px solid #475569;border-radius:999px;display:inline-flex;padding:8px 12px}.healthy{color:#86efac}.warning{color:#fbbf24}.failed,.error{color:#fca5a5}.metrics,.providers{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));margin:20px 0}.metrics article,.providers article{background:#181f27;border:1px solid #2b3642;border-radius:20px;padding:16px}.metrics span,dt,.muted{color:#a8b0b8}.metrics strong{display:block;font-size:2rem}dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:12px 0 0}dd{font-weight:800;margin:4px 0 0}table{background:#181f27;border-collapse:collapse;border-radius:18px;overflow:hidden;width:100%}td,th{border-bottom:1px solid #2b3642;padding:12px;text-align:left}section{margin-top:28px}@media(max-width:760px){main{padding:20px}table{font-size:.9rem}}";
|
|
9998
|
+
var renderVoiceTraceTimelineHTML = (report, options = {}) => `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml16(options.title ?? "Voice Trace Timelines")}</title><style>${timelineCSS}</style></head><body><main><header><p class="eyebrow">Self-hosted voice debugging</p><h1>${escapeHtml16(options.title ?? "Voice Trace Timelines")}</h1><p class="muted">Per-call event timelines with provider latency, fallback, timeout, handoff, and error context.</p></header><section class="metrics"><article><span>Sessions</span><strong>${String(report.total)}</strong></article><article><span>Failed</span><strong>${String(report.failed)}</strong></article><article><span>Warnings</span><strong>${String(report.warnings)}</strong></article></section><table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Turns</th><th>Errors</th><th>Duration</th><th>Providers</th></tr></thead><tbody>${renderSessionRows(report)}</tbody></table></main></body></html>`;
|
|
9999
|
+
var createVoiceTraceTimelineRoutes = (options) => {
|
|
10000
|
+
const path = options.path ?? "/api/voice-traces";
|
|
10001
|
+
const htmlPath = options.htmlPath ?? "/traces";
|
|
10002
|
+
const title = options.title ?? "AbsoluteJS Voice Trace Timelines";
|
|
10003
|
+
const routes = new Elysia14({
|
|
10004
|
+
name: options.name ?? "absolutejs-voice-trace-timelines"
|
|
10005
|
+
});
|
|
10006
|
+
const buildReport = async () => summarizeVoiceTraceTimeline(await options.store.list(), {
|
|
10007
|
+
evaluation: options.evaluation,
|
|
10008
|
+
limit: options.limit,
|
|
10009
|
+
redact: options.redact
|
|
10010
|
+
});
|
|
10011
|
+
const findSession = async (sessionId) => {
|
|
10012
|
+
const report = summarizeVoiceTraceTimeline(await options.store.list({ sessionId }), {
|
|
10013
|
+
evaluation: options.evaluation,
|
|
10014
|
+
limit: 1,
|
|
10015
|
+
redact: options.redact
|
|
10016
|
+
});
|
|
10017
|
+
return report.sessions[0];
|
|
10018
|
+
};
|
|
10019
|
+
routes.get(path, async () => Response.json(await buildReport()));
|
|
10020
|
+
routes.get(`${path}/:sessionId`, async ({ params }) => {
|
|
10021
|
+
const session = await findSession(params.sessionId);
|
|
10022
|
+
return session ? Response.json(session) : Response.json({ error: "Voice trace session not found." }, { status: 404 });
|
|
10023
|
+
});
|
|
10024
|
+
routes.get(htmlPath, async () => {
|
|
10025
|
+
const report = await buildReport();
|
|
10026
|
+
const body = await (options.render ?? ((input) => renderVoiceTraceTimelineHTML(input, { title })))(report);
|
|
10027
|
+
return new Response(body, {
|
|
10028
|
+
headers: {
|
|
10029
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10030
|
+
...options.headers
|
|
10031
|
+
}
|
|
10032
|
+
});
|
|
10033
|
+
});
|
|
10034
|
+
routes.get(`${htmlPath}/:sessionId`, async ({ params }) => {
|
|
10035
|
+
const session = await findSession(params.sessionId);
|
|
10036
|
+
if (!session) {
|
|
10037
|
+
return Response.json({ error: "Voice trace session not found." }, { status: 404 });
|
|
10038
|
+
}
|
|
10039
|
+
const body = await (options.renderSession ?? ((input) => renderVoiceTraceTimelineSessionHTML(input, { title })))(session);
|
|
10040
|
+
return new Response(body, {
|
|
10041
|
+
headers: {
|
|
10042
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10043
|
+
...options.headers
|
|
10044
|
+
}
|
|
10045
|
+
});
|
|
10046
|
+
});
|
|
10047
|
+
return routes;
|
|
10048
|
+
};
|
|
10049
|
+
|
|
9824
10050
|
// src/appKit.ts
|
|
9825
10051
|
var DEFAULT_LINKS2 = [
|
|
9826
10052
|
{
|
|
@@ -9983,7 +10209,7 @@ var summarizeVoiceAppKitStatus = async (options) => {
|
|
|
9983
10209
|
};
|
|
9984
10210
|
};
|
|
9985
10211
|
var createVoiceAppKitRoutes = (options) => {
|
|
9986
|
-
const routes = new
|
|
10212
|
+
const routes = new Elysia15({
|
|
9987
10213
|
name: options.name ?? "absolutejs-voice-app-kit"
|
|
9988
10214
|
});
|
|
9989
10215
|
const links = resolveLinks(options.links);
|
|
@@ -10078,6 +10304,16 @@ var createVoiceAppKitRoutes = (options) => {
|
|
|
10078
10304
|
...options.diagnostics
|
|
10079
10305
|
}));
|
|
10080
10306
|
}
|
|
10307
|
+
if (options.traceTimeline !== false) {
|
|
10308
|
+
surfaces.push("traceTimeline");
|
|
10309
|
+
routes.use(createVoiceTraceTimelineRoutes({
|
|
10310
|
+
...common,
|
|
10311
|
+
htmlPath: "/traces",
|
|
10312
|
+
path: "/api/voice-traces",
|
|
10313
|
+
title: options.title ? `${options.title} Trace Timelines` : undefined,
|
|
10314
|
+
...options.traceTimeline
|
|
10315
|
+
}));
|
|
10316
|
+
}
|
|
10081
10317
|
if (options.resilience !== false) {
|
|
10082
10318
|
surfaces.push("resilience");
|
|
10083
10319
|
routes.use(createVoiceResilienceRoutes({
|
|
@@ -10618,7 +10854,7 @@ var createVoiceToolIdempotencyKey = (input) => {
|
|
|
10618
10854
|
].join(":");
|
|
10619
10855
|
};
|
|
10620
10856
|
// src/toolContract.ts
|
|
10621
|
-
import { Elysia as
|
|
10857
|
+
import { Elysia as Elysia16 } from "elysia";
|
|
10622
10858
|
var createDefaultSession = (contractId, caseId) => createVoiceSessionRecord(`tool-contract-${contractId}-${caseId}`);
|
|
10623
10859
|
var createDefaultTurn = (caseId) => ({
|
|
10624
10860
|
committedAt: Date.now(),
|
|
@@ -10628,7 +10864,7 @@ var createDefaultTurn = (caseId) => ({
|
|
|
10628
10864
|
});
|
|
10629
10865
|
var defaultApi = {};
|
|
10630
10866
|
var sameJSON = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
10631
|
-
var
|
|
10867
|
+
var escapeHtml17 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10632
10868
|
var evaluateExpectation = (input) => {
|
|
10633
10869
|
const issues = [];
|
|
10634
10870
|
const expect = input.expect;
|
|
@@ -10794,19 +11030,19 @@ var renderVoiceToolContractHTML = (report, options = {}) => {
|
|
|
10794
11030
|
const title = options.title ?? "Voice Tool Contracts";
|
|
10795
11031
|
const contracts = report.contracts.map((contract) => {
|
|
10796
11032
|
const cases = contract.cases.map((testCase) => `<tr>
|
|
10797
|
-
<td>${
|
|
11033
|
+
<td>${escapeHtml17(testCase.label ?? testCase.caseId)}</td>
|
|
10798
11034
|
<td class="${testCase.pass ? "pass" : "fail"}">${testCase.pass ? "pass" : "fail"}</td>
|
|
10799
|
-
<td>${
|
|
11035
|
+
<td>${escapeHtml17(testCase.status)}</td>
|
|
10800
11036
|
<td>${String(testCase.attempts)}</td>
|
|
10801
11037
|
<td>${String(testCase.elapsedMs)}ms</td>
|
|
10802
11038
|
<td>${testCase.timedOut ? "yes" : "no"}</td>
|
|
10803
|
-
<td>${
|
|
11039
|
+
<td>${escapeHtml17(testCase.issues.map((issue) => issue.message).join(" ") || testCase.error || "")}</td>
|
|
10804
11040
|
</tr>`).join("");
|
|
10805
11041
|
return `<section class="contract ${contract.pass ? "pass" : "fail"}">
|
|
10806
11042
|
<div class="contract-header">
|
|
10807
11043
|
<div>
|
|
10808
|
-
<p class="eyebrow">${
|
|
10809
|
-
<h2>${
|
|
11044
|
+
<p class="eyebrow">${escapeHtml17(contract.toolName)}</p>
|
|
11045
|
+
<h2>${escapeHtml17(contract.label ?? contract.contractId)}</h2>
|
|
10810
11046
|
</div>
|
|
10811
11047
|
<strong class="${contract.pass ? "pass" : "fail"}">${contract.pass ? "Passing" : "Failing"}</strong>
|
|
10812
11048
|
</div>
|
|
@@ -10816,7 +11052,7 @@ var renderVoiceToolContractHTML = (report, options = {}) => {
|
|
|
10816
11052
|
</table>
|
|
10817
11053
|
</section>`;
|
|
10818
11054
|
}).join("");
|
|
10819
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
11055
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml17(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.contract{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.14),rgba(245,158,11,.12))}.eyebrow{color:#fbbf24;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}h2{margin:.2rem 0 1rem}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}table{border-collapse:collapse;width:100%}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left;vertical-align:top}th{color:#a8b0b8;font-size:.82rem}@media(max-width:800px){main{padding:18px}table{display:block;overflow:auto}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Tool Reliability</p><h1>${escapeHtml17(title)}</h1><div class="summary"><span class="pill ${report.status === "pass" ? "pass" : "fail"}">${escapeHtml17(report.status)}</span><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} contracts</span></div></section>${contracts || '<section class="contract"><p>No tool contracts configured.</p></section>'}</main></body></html>`;
|
|
10820
11056
|
};
|
|
10821
11057
|
var createVoiceToolContractJSONHandler = (options) => () => runVoiceToolContractSuite(options);
|
|
10822
11058
|
var createVoiceToolContractHTMLHandler = (options) => async () => {
|
|
@@ -10833,7 +11069,7 @@ var createVoiceToolContractHTMLHandler = (options) => async () => {
|
|
|
10833
11069
|
var createVoiceToolContractRoutes = (options) => {
|
|
10834
11070
|
const path = options.path ?? "/api/tool-contracts";
|
|
10835
11071
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
10836
|
-
const routes = new
|
|
11072
|
+
const routes = new Elysia16({
|
|
10837
11073
|
name: options.name ?? "absolutejs-voice-tool-contracts"
|
|
10838
11074
|
}).get(path, createVoiceToolContractJSONHandler(options));
|
|
10839
11075
|
if (htmlPath) {
|
|
@@ -10842,9 +11078,9 @@ var createVoiceToolContractRoutes = (options) => {
|
|
|
10842
11078
|
return routes;
|
|
10843
11079
|
};
|
|
10844
11080
|
// src/turnQuality.ts
|
|
10845
|
-
import { Elysia as
|
|
11081
|
+
import { Elysia as Elysia17 } from "elysia";
|
|
10846
11082
|
var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
|
|
10847
|
-
var
|
|
11083
|
+
var escapeHtml18 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10848
11084
|
var getTurnLatencyMs = (turn) => {
|
|
10849
11085
|
const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
|
|
10850
11086
|
if (firstTranscriptAt === undefined) {
|
|
@@ -10915,24 +11151,24 @@ var summarizeVoiceTurnQuality = async (options) => {
|
|
|
10915
11151
|
};
|
|
10916
11152
|
var renderVoiceTurnQualityHTML = (report, options = {}) => {
|
|
10917
11153
|
const title = options.title ?? "Voice Turn Quality";
|
|
10918
|
-
const turns = report.turns.map((turn) => `<article class="turn ${
|
|
11154
|
+
const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml18(turn.status)}">
|
|
10919
11155
|
<div class="turn-header">
|
|
10920
11156
|
<div>
|
|
10921
|
-
<p class="eyebrow">${
|
|
10922
|
-
<h2>${
|
|
11157
|
+
<p class="eyebrow">${escapeHtml18(turn.sessionId)} \xB7 ${escapeHtml18(turn.turnId)}</p>
|
|
11158
|
+
<h2>${escapeHtml18(turn.text || "Empty turn")}</h2>
|
|
10923
11159
|
</div>
|
|
10924
|
-
<strong>${
|
|
11160
|
+
<strong>${escapeHtml18(turn.status)}</strong>
|
|
10925
11161
|
</div>
|
|
10926
11162
|
<dl>
|
|
10927
|
-
<div><dt>Source</dt><dd>${
|
|
11163
|
+
<div><dt>Source</dt><dd>${escapeHtml18(turn.source ?? "unknown")}</dd></div>
|
|
10928
11164
|
<div><dt>Confidence</dt><dd>${turn.averageConfidence === undefined ? "n/a" : `${Math.round(turn.averageConfidence * 100)}%`}</dd></div>
|
|
10929
|
-
<div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${
|
|
10930
|
-
<div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${
|
|
11165
|
+
<div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml18(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
|
|
11166
|
+
<div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml18(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
|
|
10931
11167
|
<div><dt>Transcripts</dt><dd>${String(turn.selectedTranscriptCount)} selected \xB7 ${String(turn.finalTranscriptCount)} final \xB7 ${String(turn.partialTranscriptCount)} partial</dd></div>
|
|
10932
11168
|
<div><dt>Cost</dt><dd>${turn.costUnits === undefined ? "n/a" : String(turn.costUnits)}</dd></div>
|
|
10933
11169
|
</dl>
|
|
10934
11170
|
</article>`).join("");
|
|
10935
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
11171
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml18(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(251,191,36,.16),rgba(34,197,94,.1))}.eyebrow{color:#fbbf24;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.unknown{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{margin:0}@media(max-width:800px){main{padding:18px}.turn-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Realtime STT Debugging</p><h1>${escapeHtml18(title)}</h1><div class="summary"><span class="pill ${escapeHtml18(report.status)}">${escapeHtml18(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span><span class="pill">${String(report.sessions)} sessions</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
|
|
10936
11172
|
};
|
|
10937
11173
|
var createVoiceTurnQualityJSONHandler = (options) => async () => summarizeVoiceTurnQuality(options);
|
|
10938
11174
|
var createVoiceTurnQualityHTMLHandler = (options) => async () => {
|
|
@@ -10949,7 +11185,7 @@ var createVoiceTurnQualityHTMLHandler = (options) => async () => {
|
|
|
10949
11185
|
var createVoiceTurnQualityRoutes = (options) => {
|
|
10950
11186
|
const path = options.path ?? "/api/turn-quality";
|
|
10951
11187
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
10952
|
-
const routes = new
|
|
11188
|
+
const routes = new Elysia17({
|
|
10953
11189
|
name: options.name ?? "absolutejs-voice-turn-quality"
|
|
10954
11190
|
}).get(path, createVoiceTurnQualityJSONHandler(options));
|
|
10955
11191
|
if (htmlPath) {
|
|
@@ -10958,8 +11194,8 @@ var createVoiceTurnQualityRoutes = (options) => {
|
|
|
10958
11194
|
return routes;
|
|
10959
11195
|
};
|
|
10960
11196
|
// src/outcomeContract.ts
|
|
10961
|
-
import { Elysia as
|
|
10962
|
-
var
|
|
11197
|
+
import { Elysia as Elysia18 } from "elysia";
|
|
11198
|
+
var escapeHtml19 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10963
11199
|
var getPayloadString = (event, key) => typeof event.payload[key] === "string" ? event.payload[key] : undefined;
|
|
10964
11200
|
var toList = async (input) => Array.isArray(input) ? input : await input?.list() ?? [];
|
|
10965
11201
|
var hydrateSessions = async (input) => {
|
|
@@ -11067,9 +11303,9 @@ var renderVoiceOutcomeContractHTML = (report, options = {}) => {
|
|
|
11067
11303
|
const contracts = report.contracts.map((contract) => `<section class="contract ${contract.pass ? "pass" : "fail"}">
|
|
11068
11304
|
<div class="contract-header">
|
|
11069
11305
|
<div>
|
|
11070
|
-
<p class="eyebrow">${
|
|
11071
|
-
<h2>${
|
|
11072
|
-
${contract.description ? `<p>${
|
|
11306
|
+
<p class="eyebrow">${escapeHtml19(contract.contractId)}</p>
|
|
11307
|
+
<h2>${escapeHtml19(contract.label ?? contract.contractId)}</h2>
|
|
11308
|
+
${contract.description ? `<p>${escapeHtml19(contract.description)}</p>` : ""}
|
|
11073
11309
|
</div>
|
|
11074
11310
|
<strong>${contract.pass ? "pass" : "fail"}</strong>
|
|
11075
11311
|
</div>
|
|
@@ -11080,9 +11316,9 @@ var renderVoiceOutcomeContractHTML = (report, options = {}) => {
|
|
|
11080
11316
|
<span>handoffs ${String(contract.matched.handoffs)}</span>
|
|
11081
11317
|
<span>events ${String(contract.matched.integrationEvents)}</span>
|
|
11082
11318
|
</div>
|
|
11083
|
-
${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${
|
|
11319
|
+
${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml19(issue.message)}</li>`).join("")}</ul>` : ""}
|
|
11084
11320
|
</section>`).join("");
|
|
11085
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
11321
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml19(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.contract{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.14),rgba(14,165,233,.12))}.eyebrow{color:#7dd3fc;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0}.summary,.grid{display:flex;flex-wrap:wrap;gap:10px}.pill,.grid span{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}li{margin:8px 0}@media(max-width:800px){main{padding:18px}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Business Outcome Verification</p><h1>${escapeHtml19(title)}</h1><div class="summary"><span class="pill ${report.status}">${report.status}</span><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} contracts</span></div></section>${contracts || '<section class="contract"><p>No outcome contracts configured.</p></section>'}</main></body></html>`;
|
|
11086
11322
|
};
|
|
11087
11323
|
var createVoiceOutcomeContractJSONHandler = (options) => async () => runVoiceOutcomeContractSuite(options);
|
|
11088
11324
|
var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
|
|
@@ -11098,7 +11334,7 @@ var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
|
|
|
11098
11334
|
var createVoiceOutcomeContractRoutes = (options) => {
|
|
11099
11335
|
const path = options.path ?? "/api/outcome-contracts";
|
|
11100
11336
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
11101
|
-
const routes = new
|
|
11337
|
+
const routes = new Elysia18({
|
|
11102
11338
|
name: options.name ?? "absolutejs-voice-outcome-contracts"
|
|
11103
11339
|
}).get(path, createVoiceOutcomeContractJSONHandler(options));
|
|
11104
11340
|
if (htmlPath) {
|
|
@@ -11107,7 +11343,7 @@ var createVoiceOutcomeContractRoutes = (options) => {
|
|
|
11107
11343
|
return routes;
|
|
11108
11344
|
};
|
|
11109
11345
|
// src/telephonyOutcome.ts
|
|
11110
|
-
import { Elysia as
|
|
11346
|
+
import { Elysia as Elysia19 } from "elysia";
|
|
11111
11347
|
var DEFAULT_COMPLETED_STATUSES = [
|
|
11112
11348
|
"answered",
|
|
11113
11349
|
"completed",
|
|
@@ -11167,7 +11403,7 @@ var createMemoryVoiceTelephonyWebhookIdempotencyStore = () => {
|
|
|
11167
11403
|
};
|
|
11168
11404
|
};
|
|
11169
11405
|
var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
|
|
11170
|
-
var
|
|
11406
|
+
var firstString2 = (source, keys) => {
|
|
11171
11407
|
for (const key of keys) {
|
|
11172
11408
|
const value = source[key];
|
|
11173
11409
|
if (typeof value === "string" && value.trim()) {
|
|
@@ -11178,7 +11414,7 @@ var firstString = (source, keys) => {
|
|
|
11178
11414
|
}
|
|
11179
11415
|
}
|
|
11180
11416
|
};
|
|
11181
|
-
var
|
|
11417
|
+
var firstNumber2 = (source, keys) => {
|
|
11182
11418
|
for (const key of keys) {
|
|
11183
11419
|
const value = source[key];
|
|
11184
11420
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
@@ -11539,8 +11775,8 @@ var verifyVoiceTelephonyWebhook = async (input) => {
|
|
|
11539
11775
|
var durationMsFromSeconds = (value) => typeof value === "number" ? value * 1000 : undefined;
|
|
11540
11776
|
var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
11541
11777
|
const payload = flattenPayload(input.body);
|
|
11542
|
-
const provider =
|
|
11543
|
-
const status =
|
|
11778
|
+
const provider = firstString2(payload, ["provider", "Provider"]) ?? input.provider;
|
|
11779
|
+
const status = firstString2(payload, [
|
|
11544
11780
|
"CallStatus",
|
|
11545
11781
|
"call_status",
|
|
11546
11782
|
"callStatus",
|
|
@@ -11550,7 +11786,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
11550
11786
|
"event_type",
|
|
11551
11787
|
"type"
|
|
11552
11788
|
]);
|
|
11553
|
-
const durationMs =
|
|
11789
|
+
const durationMs = firstNumber2(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber2(payload, [
|
|
11554
11790
|
"CallDuration",
|
|
11555
11791
|
"call_duration",
|
|
11556
11792
|
"callDuration",
|
|
@@ -11558,16 +11794,16 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
11558
11794
|
"dial_call_duration",
|
|
11559
11795
|
"duration"
|
|
11560
11796
|
]));
|
|
11561
|
-
const sipCode =
|
|
11797
|
+
const sipCode = firstNumber2(payload, [
|
|
11562
11798
|
"SipResponseCode",
|
|
11563
11799
|
"sip_response_code",
|
|
11564
11800
|
"sipCode",
|
|
11565
11801
|
"sip_code",
|
|
11566
11802
|
"hangupCauseCode"
|
|
11567
11803
|
]);
|
|
11568
|
-
const from =
|
|
11569
|
-
const to =
|
|
11570
|
-
const target =
|
|
11804
|
+
const from = firstString2(payload, ["From", "from", "caller_id", "callerId"]);
|
|
11805
|
+
const to = firstString2(payload, ["To", "to", "called_number", "calledNumber"]);
|
|
11806
|
+
const target = firstString2(payload, [
|
|
11571
11807
|
"transferTarget",
|
|
11572
11808
|
"TransferTarget",
|
|
11573
11809
|
"target",
|
|
@@ -11575,7 +11811,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
11575
11811
|
"department"
|
|
11576
11812
|
]);
|
|
11577
11813
|
return {
|
|
11578
|
-
answeredBy:
|
|
11814
|
+
answeredBy: firstString2(payload, [
|
|
11579
11815
|
"AnsweredBy",
|
|
11580
11816
|
"answered_by",
|
|
11581
11817
|
"answeredBy",
|
|
@@ -11586,7 +11822,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
11586
11822
|
from,
|
|
11587
11823
|
metadata: payload,
|
|
11588
11824
|
provider,
|
|
11589
|
-
reason:
|
|
11825
|
+
reason: firstString2(payload, [
|
|
11590
11826
|
"Reason",
|
|
11591
11827
|
"reason",
|
|
11592
11828
|
"HangupCause",
|
|
@@ -11602,7 +11838,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
11602
11838
|
var defaultSessionId = (input) => {
|
|
11603
11839
|
const payload = flattenPayload(input.body);
|
|
11604
11840
|
const metadataSessionId = input.event.metadata?.sessionId;
|
|
11605
|
-
return
|
|
11841
|
+
return firstString2(input.query, ["sessionId", "session_id"]) ?? firstString2(payload, [
|
|
11606
11842
|
"sessionId",
|
|
11607
11843
|
"session_id",
|
|
11608
11844
|
"SessionId",
|
|
@@ -11617,7 +11853,7 @@ var defaultSessionId = (input) => {
|
|
|
11617
11853
|
};
|
|
11618
11854
|
var defaultIdempotencyKey = (input) => {
|
|
11619
11855
|
const payload = flattenPayload(input.body);
|
|
11620
|
-
const eventId =
|
|
11856
|
+
const eventId = firstString2(payload, [
|
|
11621
11857
|
"id",
|
|
11622
11858
|
"event_id",
|
|
11623
11859
|
"eventId",
|
|
@@ -11754,7 +11990,7 @@ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
|
|
|
11754
11990
|
var createVoiceTelephonyWebhookRoutes = (options = {}) => {
|
|
11755
11991
|
const path = options.path ?? "/api/voice/telephony/webhook";
|
|
11756
11992
|
const handler = createVoiceTelephonyWebhookHandler(options);
|
|
11757
|
-
return new
|
|
11993
|
+
return new Elysia19({
|
|
11758
11994
|
name: options.name ?? "absolutejs-voice-telephony-webhooks"
|
|
11759
11995
|
}).post(path, async ({ query, request }) => {
|
|
11760
11996
|
try {
|
|
@@ -14022,7 +14258,7 @@ var createVoiceMemoryStore = () => {
|
|
|
14022
14258
|
return { get, getOrCreate, list, remove, set };
|
|
14023
14259
|
};
|
|
14024
14260
|
// src/opsWebhook.ts
|
|
14025
|
-
import { Elysia as
|
|
14261
|
+
import { Elysia as Elysia20 } from "elysia";
|
|
14026
14262
|
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
14027
14263
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
14028
14264
|
const encoder = new TextEncoder;
|
|
@@ -14152,7 +14388,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
|
14152
14388
|
};
|
|
14153
14389
|
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
14154
14390
|
const path = options.path ?? "/api/voice-ops/webhook";
|
|
14155
|
-
return new
|
|
14391
|
+
return new Elysia20().post(path, async ({ body, request, set }) => {
|
|
14156
14392
|
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
14157
14393
|
if (options.signingSecret) {
|
|
14158
14394
|
const verification = await verifyVoiceOpsWebhookSignature({
|
|
@@ -15881,7 +16117,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
|
|
|
15881
16117
|
};
|
|
15882
16118
|
// src/telephony/twilio.ts
|
|
15883
16119
|
import { Buffer as Buffer3 } from "buffer";
|
|
15884
|
-
import { Elysia as
|
|
16120
|
+
import { Elysia as Elysia21 } from "elysia";
|
|
15885
16121
|
var TWILIO_MULAW_SAMPLE_RATE = 8000;
|
|
15886
16122
|
var VOICE_PCM_SAMPLE_RATE = 16000;
|
|
15887
16123
|
var escapeXml2 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
@@ -15911,7 +16147,7 @@ var resolveTwilioStreamParameters = async (parameters, input) => {
|
|
|
15911
16147
|
return parameters;
|
|
15912
16148
|
};
|
|
15913
16149
|
var joinUrlPath = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
15914
|
-
var
|
|
16150
|
+
var escapeHtml20 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
15915
16151
|
var getWebhookVerificationUrl = (webhook, input) => {
|
|
15916
16152
|
if (!webhook?.verificationUrl) {
|
|
15917
16153
|
return;
|
|
@@ -15954,23 +16190,23 @@ var buildTwilioVoiceSetupStatus = async (options, input) => {
|
|
|
15954
16190
|
};
|
|
15955
16191
|
var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
15956
16192
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
|
|
15957
|
-
<h1>${
|
|
16193
|
+
<h1>${escapeHtml20(title)}</h1>
|
|
15958
16194
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
15959
16195
|
<section>
|
|
15960
16196
|
<h2>URLs</h2>
|
|
15961
16197
|
<ul>
|
|
15962
|
-
<li><strong>TwiML:</strong> <code>${
|
|
15963
|
-
<li><strong>Media stream:</strong> <code>${
|
|
15964
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
16198
|
+
<li><strong>TwiML:</strong> <code>${escapeHtml20(status.urls.twiml)}</code></li>
|
|
16199
|
+
<li><strong>Media stream:</strong> <code>${escapeHtml20(status.urls.stream)}</code></li>
|
|
16200
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml20(status.urls.webhook)}</code></li>
|
|
15965
16201
|
</ul>
|
|
15966
16202
|
</section>
|
|
15967
16203
|
<section>
|
|
15968
16204
|
<h2>Signing</h2>
|
|
15969
16205
|
<p>Mode: <code>${status.signing.mode}</code></p>
|
|
15970
|
-
${status.signing.verificationUrl ? `<p>Verification URL: <code>${
|
|
16206
|
+
${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml20(status.signing.verificationUrl)}</code></p>` : ""}
|
|
15971
16207
|
</section>
|
|
15972
|
-
${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${
|
|
15973
|
-
${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
16208
|
+
${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml20(name)}</code></li>`).join("")}</ul></section>` : ""}
|
|
16209
|
+
${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml20(warning)}</li>`).join("")}</ul></section>` : ""}
|
|
15974
16210
|
</main>`;
|
|
15975
16211
|
var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&", "&");
|
|
15976
16212
|
var createSmokeCheck = (name, status, message, details) => ({
|
|
@@ -15981,20 +16217,20 @@ var createSmokeCheck = (name, status, message, details) => ({
|
|
|
15981
16217
|
});
|
|
15982
16218
|
var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
15983
16219
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
|
|
15984
|
-
<h1>${
|
|
16220
|
+
<h1>${escapeHtml20(title)}</h1>
|
|
15985
16221
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
15986
16222
|
<section>
|
|
15987
16223
|
<h2>Checks</h2>
|
|
15988
16224
|
<ul>
|
|
15989
|
-
${report.checks.map((check) => `<li><strong>${
|
|
16225
|
+
${report.checks.map((check) => `<li><strong>${escapeHtml20(check.name)}</strong>: ${escapeHtml20(check.status)}${check.message ? ` - ${escapeHtml20(check.message)}` : ""}</li>`).join("")}
|
|
15990
16226
|
</ul>
|
|
15991
16227
|
</section>
|
|
15992
16228
|
<section>
|
|
15993
16229
|
<h2>Observed URLs</h2>
|
|
15994
16230
|
<ul>
|
|
15995
|
-
<li><strong>TwiML:</strong> <code>${
|
|
15996
|
-
<li><strong>Stream:</strong> <code>${
|
|
15997
|
-
<li><strong>Webhook:</strong> <code>${
|
|
16231
|
+
<li><strong>TwiML:</strong> <code>${escapeHtml20(report.setup.urls.twiml)}</code></li>
|
|
16232
|
+
<li><strong>Stream:</strong> <code>${escapeHtml20(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
|
|
16233
|
+
<li><strong>Webhook:</strong> <code>${escapeHtml20(report.setup.urls.webhook)}</code></li>
|
|
15998
16234
|
</ul>
|
|
15999
16235
|
</section>
|
|
16000
16236
|
</main>`;
|
|
@@ -16454,7 +16690,7 @@ var createTwilioVoiceRoutes = (options) => {
|
|
|
16454
16690
|
const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
|
|
16455
16691
|
const bridges = new WeakMap;
|
|
16456
16692
|
const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
|
|
16457
|
-
const app = new
|
|
16693
|
+
const app = new Elysia21({
|
|
16458
16694
|
name: options.name ?? "absolutejs-voice-twilio"
|
|
16459
16695
|
}).get(twimlPath, async ({ query, request }) => {
|
|
16460
16696
|
const streamUrl = await resolveTwilioStreamUrl(options, {
|
|
@@ -16590,9 +16826,9 @@ var createTwilioVoiceRoutes = (options) => {
|
|
|
16590
16826
|
};
|
|
16591
16827
|
// src/telephony/telnyx.ts
|
|
16592
16828
|
import { Buffer as Buffer4 } from "buffer";
|
|
16593
|
-
import { Elysia as
|
|
16829
|
+
import { Elysia as Elysia22 } from "elysia";
|
|
16594
16830
|
var escapeXml3 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
16595
|
-
var
|
|
16831
|
+
var escapeHtml21 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
16596
16832
|
var joinUrlPath2 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
16597
16833
|
var resolveRequestOrigin2 = (request) => {
|
|
16598
16834
|
const url = new URL(request.url);
|
|
@@ -16793,21 +17029,21 @@ var buildTelnyxVoiceSetupStatus = async (options, input) => {
|
|
|
16793
17029
|
};
|
|
16794
17030
|
var renderTelnyxSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
16795
17031
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx setup</p>
|
|
16796
|
-
<h1>${
|
|
17032
|
+
<h1>${escapeHtml21(title)}</h1>
|
|
16797
17033
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
16798
17034
|
<ul>
|
|
16799
|
-
<li><strong>TeXML:</strong> <code>${
|
|
16800
|
-
<li><strong>Media stream:</strong> <code>${
|
|
16801
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
17035
|
+
<li><strong>TeXML:</strong> <code>${escapeHtml21(status.urls.texml)}</code></li>
|
|
17036
|
+
<li><strong>Media stream:</strong> <code>${escapeHtml21(status.urls.stream)}</code></li>
|
|
17037
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml21(status.urls.webhook)}</code></li>
|
|
16802
17038
|
</ul>
|
|
16803
|
-
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${
|
|
16804
|
-
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
17039
|
+
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml21(name)}</code></li>`).join("")}</ul>` : ""}
|
|
17040
|
+
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml21(warning)}</li>`).join("")}</ul>` : ""}
|
|
16805
17041
|
</main>`;
|
|
16806
17042
|
var renderTelnyxSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
16807
17043
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx smoke test</p>
|
|
16808
|
-
<h1>${
|
|
17044
|
+
<h1>${escapeHtml21(title)}</h1>
|
|
16809
17045
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
16810
|
-
<ul>${report.checks.map((check) => `<li><strong>${
|
|
17046
|
+
<ul>${report.checks.map((check) => `<li><strong>${escapeHtml21(check.name)}</strong>: ${escapeHtml21(check.status)}${check.message ? ` - ${escapeHtml21(check.message)}` : ""}</li>`).join("")}</ul>
|
|
16811
17047
|
</main>`;
|
|
16812
17048
|
var runTelnyxSmokeTest = async (input) => {
|
|
16813
17049
|
const setup = await buildTelnyxVoiceSetupStatus(input.options, input);
|
|
@@ -16901,7 +17137,7 @@ var createTelnyxVoiceRoutes = (options = {}) => {
|
|
|
16901
17137
|
publicKey: options.webhook?.publicKey,
|
|
16902
17138
|
toleranceSeconds: options.webhook?.toleranceSeconds
|
|
16903
17139
|
}) : undefined);
|
|
16904
|
-
const app = new
|
|
17140
|
+
const app = new Elysia22({
|
|
16905
17141
|
name: options.name ?? "absolutejs-voice-telnyx"
|
|
16906
17142
|
}).get(texmlPath, async ({ query, request }) => {
|
|
16907
17143
|
const streamUrl = await resolveTelnyxStreamUrl(options, {
|
|
@@ -17011,9 +17247,9 @@ var createTelnyxVoiceRoutes = (options = {}) => {
|
|
|
17011
17247
|
};
|
|
17012
17248
|
// src/telephony/plivo.ts
|
|
17013
17249
|
import { Buffer as Buffer5 } from "buffer";
|
|
17014
|
-
import { Elysia as
|
|
17250
|
+
import { Elysia as Elysia23 } from "elysia";
|
|
17015
17251
|
var escapeXml4 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
17016
|
-
var
|
|
17252
|
+
var escapeHtml22 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
17017
17253
|
var joinUrlPath3 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
17018
17254
|
var resolveRequestOrigin3 = (request) => {
|
|
17019
17255
|
const url = new URL(request.url);
|
|
@@ -17264,21 +17500,21 @@ var buildPlivoVoiceSetupStatus = async (options, input) => {
|
|
|
17264
17500
|
};
|
|
17265
17501
|
var renderPlivoSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
17266
17502
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo setup</p>
|
|
17267
|
-
<h1>${
|
|
17503
|
+
<h1>${escapeHtml22(title)}</h1>
|
|
17268
17504
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
17269
17505
|
<ul>
|
|
17270
|
-
<li><strong>Answer XML:</strong> <code>${
|
|
17271
|
-
<li><strong>Audio stream:</strong> <code>${
|
|
17272
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
17506
|
+
<li><strong>Answer XML:</strong> <code>${escapeHtml22(status.urls.answer)}</code></li>
|
|
17507
|
+
<li><strong>Audio stream:</strong> <code>${escapeHtml22(status.urls.stream)}</code></li>
|
|
17508
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml22(status.urls.webhook)}</code></li>
|
|
17273
17509
|
</ul>
|
|
17274
|
-
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${
|
|
17275
|
-
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
17510
|
+
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml22(name)}</code></li>`).join("")}</ul>` : ""}
|
|
17511
|
+
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml22(warning)}</li>`).join("")}</ul>` : ""}
|
|
17276
17512
|
</main>`;
|
|
17277
17513
|
var renderPlivoSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
17278
17514
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo smoke test</p>
|
|
17279
|
-
<h1>${
|
|
17515
|
+
<h1>${escapeHtml22(title)}</h1>
|
|
17280
17516
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
17281
|
-
<ul>${report.checks.map((check) => `<li><strong>${
|
|
17517
|
+
<ul>${report.checks.map((check) => `<li><strong>${escapeHtml22(check.name)}</strong>: ${escapeHtml22(check.status)}${check.message ? ` - ${escapeHtml22(check.message)}` : ""}</li>`).join("")}</ul>
|
|
17282
17518
|
</main>`;
|
|
17283
17519
|
var runPlivoSmokeTest = async (input) => {
|
|
17284
17520
|
const setup = await buildPlivoVoiceSetupStatus(input.options, input);
|
|
@@ -17373,7 +17609,7 @@ var createPlivoVoiceRoutes = (options = {}) => {
|
|
|
17373
17609
|
request: input.request
|
|
17374
17610
|
}) : verificationUrl ?? input.request.url
|
|
17375
17611
|
}) : undefined);
|
|
17376
|
-
const app = new
|
|
17612
|
+
const app = new Elysia23({
|
|
17377
17613
|
name: options.name ?? "absolutejs-voice-plivo"
|
|
17378
17614
|
}).get(answerPath, async ({ query, request }) => {
|
|
17379
17615
|
const streamUrl = await resolvePlivoStreamUrl(options, {
|
|
@@ -17546,6 +17782,7 @@ export {
|
|
|
17546
17782
|
transcodeTwilioInboundPayloadToPCM16,
|
|
17547
17783
|
transcodePCMToTwilioOutboundPayload,
|
|
17548
17784
|
summarizeVoiceTurnQuality,
|
|
17785
|
+
summarizeVoiceTraceTimeline,
|
|
17549
17786
|
summarizeVoiceTraceSinkDeliveries,
|
|
17550
17787
|
summarizeVoiceTrace,
|
|
17551
17788
|
summarizeVoiceSessions,
|
|
@@ -17591,6 +17828,8 @@ export {
|
|
|
17591
17828
|
requeueVoiceOpsTask,
|
|
17592
17829
|
reopenVoiceOpsTask,
|
|
17593
17830
|
renderVoiceTurnQualityHTML,
|
|
17831
|
+
renderVoiceTraceTimelineSessionHTML,
|
|
17832
|
+
renderVoiceTraceTimelineHTML,
|
|
17594
17833
|
renderVoiceTraceMarkdown,
|
|
17595
17834
|
renderVoiceTraceHTML,
|
|
17596
17835
|
renderVoiceToolContractHTML,
|
|
@@ -17653,6 +17892,7 @@ export {
|
|
|
17653
17892
|
createVoiceTurnQualityRoutes,
|
|
17654
17893
|
createVoiceTurnQualityJSONHandler,
|
|
17655
17894
|
createVoiceTurnQualityHTMLHandler,
|
|
17895
|
+
createVoiceTraceTimelineRoutes,
|
|
17656
17896
|
createVoiceTraceSinkStore,
|
|
17657
17897
|
createVoiceTraceSinkDeliveryWorkerLoop,
|
|
17658
17898
|
createVoiceTraceSinkDeliveryWorker,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { evaluateVoiceTrace, type StoredVoiceTraceEvent, type VoiceTraceEvaluationOptions, type VoiceTraceEventStore, type VoiceTraceRedactionConfig, type VoiceTraceSummary } from './trace';
|
|
3
|
+
export type VoiceTraceTimelineEvent = {
|
|
4
|
+
at: number;
|
|
5
|
+
elapsedMs?: number;
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
offsetMs: number;
|
|
9
|
+
provider?: string;
|
|
10
|
+
status?: string;
|
|
11
|
+
turnId?: string;
|
|
12
|
+
type: StoredVoiceTraceEvent['type'];
|
|
13
|
+
};
|
|
14
|
+
export type VoiceTraceTimelineProviderSummary = {
|
|
15
|
+
averageElapsedMs?: number;
|
|
16
|
+
errorCount: number;
|
|
17
|
+
eventCount: number;
|
|
18
|
+
fallbackCount: number;
|
|
19
|
+
maxElapsedMs?: number;
|
|
20
|
+
provider: string;
|
|
21
|
+
successCount: number;
|
|
22
|
+
timeoutCount: number;
|
|
23
|
+
};
|
|
24
|
+
export type VoiceTraceTimelineSession = {
|
|
25
|
+
endedAt?: number;
|
|
26
|
+
evaluation: ReturnType<typeof evaluateVoiceTrace>;
|
|
27
|
+
events: VoiceTraceTimelineEvent[];
|
|
28
|
+
lastEventAt?: number;
|
|
29
|
+
providers: VoiceTraceTimelineProviderSummary[];
|
|
30
|
+
sessionId: string;
|
|
31
|
+
startedAt?: number;
|
|
32
|
+
status: 'failed' | 'healthy' | 'warning';
|
|
33
|
+
summary: VoiceTraceSummary;
|
|
34
|
+
};
|
|
35
|
+
export type VoiceTraceTimelineReport = {
|
|
36
|
+
checkedAt: number;
|
|
37
|
+
failed: number;
|
|
38
|
+
sessions: VoiceTraceTimelineSession[];
|
|
39
|
+
total: number;
|
|
40
|
+
warnings: number;
|
|
41
|
+
};
|
|
42
|
+
export type VoiceTraceTimelineRoutesOptions = {
|
|
43
|
+
evaluation?: VoiceTraceEvaluationOptions;
|
|
44
|
+
headers?: HeadersInit;
|
|
45
|
+
htmlPath?: string;
|
|
46
|
+
limit?: number;
|
|
47
|
+
name?: string;
|
|
48
|
+
path?: string;
|
|
49
|
+
redact?: VoiceTraceRedactionConfig;
|
|
50
|
+
render?: (report: VoiceTraceTimelineReport) => string | Promise<string>;
|
|
51
|
+
renderSession?: (session: VoiceTraceTimelineSession) => string | Promise<string>;
|
|
52
|
+
store: VoiceTraceEventStore;
|
|
53
|
+
title?: string;
|
|
54
|
+
};
|
|
55
|
+
export declare const summarizeVoiceTraceTimeline: (events: StoredVoiceTraceEvent[], options?: {
|
|
56
|
+
evaluation?: VoiceTraceEvaluationOptions;
|
|
57
|
+
limit?: number;
|
|
58
|
+
redact?: VoiceTraceRedactionConfig;
|
|
59
|
+
}) => VoiceTraceTimelineReport;
|
|
60
|
+
export declare const renderVoiceTraceTimelineSessionHTML: (session: VoiceTraceTimelineSession, options?: {
|
|
61
|
+
title?: string;
|
|
62
|
+
}) => string;
|
|
63
|
+
export declare const renderVoiceTraceTimelineHTML: (report: VoiceTraceTimelineReport, options?: {
|
|
64
|
+
title?: string;
|
|
65
|
+
}) => string;
|
|
66
|
+
export declare const createVoiceTraceTimelineRoutes: (options: VoiceTraceTimelineRoutesOptions) => Elysia<"", {
|
|
67
|
+
decorator: {};
|
|
68
|
+
store: {};
|
|
69
|
+
derive: {};
|
|
70
|
+
resolve: {};
|
|
71
|
+
}, {
|
|
72
|
+
typebox: {};
|
|
73
|
+
error: {};
|
|
74
|
+
}, {
|
|
75
|
+
schema: {};
|
|
76
|
+
standaloneSchema: {};
|
|
77
|
+
macro: {};
|
|
78
|
+
macroFn: {};
|
|
79
|
+
parser: {};
|
|
80
|
+
response: {};
|
|
81
|
+
}, {}, {
|
|
82
|
+
derive: {};
|
|
83
|
+
resolve: {};
|
|
84
|
+
schema: {};
|
|
85
|
+
standaloneSchema: {};
|
|
86
|
+
response: {};
|
|
87
|
+
}, {
|
|
88
|
+
derive: {};
|
|
89
|
+
resolve: {};
|
|
90
|
+
schema: {};
|
|
91
|
+
standaloneSchema: {};
|
|
92
|
+
response: {};
|
|
93
|
+
}>;
|