@absolutejs/voice 0.0.22-beta.101 → 0.0.22-beta.103
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 +167 -60
- package/dist/liveLatency.d.ts +78 -0
- package/dist/productionReadiness.d.ts +10 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from '.
|
|
|
11
11
|
export { createVoiceToolIdempotencyKey, createVoiceToolRuntime } from './toolRuntime';
|
|
12
12
|
export { createVoiceToolContract, createVoiceToolContractHTMLHandler, createVoiceToolContractJSONHandler, createVoiceToolContractRoutes, createVoiceToolRuntimeContractDefaults, renderVoiceToolContractHTML, runVoiceToolContractSuite, runVoiceToolContract } from './toolContract';
|
|
13
13
|
export { createVoiceTurnLatencyHTMLHandler, createVoiceTurnLatencyJSONHandler, createVoiceTurnLatencyRoutes, renderVoiceTurnLatencyHTML, summarizeVoiceTurnLatency } from './turnLatency';
|
|
14
|
+
export { createVoiceLiveLatencyRoutes, renderVoiceLiveLatencyHTML, summarizeVoiceLiveLatency } from './liveLatency';
|
|
14
15
|
export { createVoiceTurnQualityHTMLHandler, createVoiceTurnQualityJSONHandler, createVoiceTurnQualityRoutes, renderVoiceTurnQualityHTML, summarizeVoiceTurnQuality } from './turnQuality';
|
|
15
16
|
export { createVoiceOutcomeContractHTMLHandler, createVoiceOutcomeContractJSONHandler, createVoiceOutcomeContractRoutes, renderVoiceOutcomeContractHTML, runVoiceOutcomeContractSuite } from './outcomeContract';
|
|
16
17
|
export { applyVoiceTelephonyOutcome, createMemoryVoiceTelephonyWebhookIdempotencyStore, createVoiceTelephonyOutcomePolicy, createVoiceTelephonyWebhookHandler, createVoiceTelephonyWebhookRoutes, parseVoiceTelephonyWebhookEvent, resolveVoiceTelephonyOutcome, signVoiceTwilioWebhook, verifyVoiceTwilioWebhookSignature, voiceTelephonyOutcomeToRouteResult } from './telephonyOutcome';
|
|
@@ -63,6 +64,7 @@ export type { OpenAIVoiceTTSOptions, OpenAIVoiceTTSVoice } from './openaiTTS';
|
|
|
63
64
|
export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
|
|
64
65
|
export type { VoiceProviderCapabilityDefinition, VoiceProviderCapabilityHandlerOptions, VoiceProviderCapabilityHTMLHandlerOptions, VoiceProviderCapabilityKind, VoiceProviderCapabilityOptions, VoiceProviderCapabilityReport, VoiceProviderCapabilityRoutesOptions, VoiceProviderCapabilitySummary } from './providerCapabilities';
|
|
65
66
|
export type { VoiceTurnLatencyHTMLHandlerOptions, VoiceTurnLatencyItem, VoiceTurnLatencyOptions, VoiceTurnLatencyReport, VoiceTurnLatencyRoutesOptions, VoiceTurnLatencyStage, VoiceTurnLatencyStatus } from './turnLatency';
|
|
67
|
+
export type { VoiceLiveLatencyOptions, VoiceLiveLatencyReport, VoiceLiveLatencyRoutesOptions, VoiceLiveLatencySample, VoiceLiveLatencyStatus } from './liveLatency';
|
|
66
68
|
export type { VoiceTurnQualityHTMLHandlerOptions, VoiceTurnQualityItem, VoiceTurnQualityOptions, VoiceTurnQualityReport, VoiceTurnQualityRoutesOptions, VoiceTurnQualityStatus } from './turnQuality';
|
|
67
69
|
export type { VoiceOutcomeContractDefinition, VoiceOutcomeContractHTMLHandlerOptions, VoiceOutcomeContractIssue, VoiceOutcomeContractOptions, VoiceOutcomeContractReport, VoiceOutcomeContractRoutesOptions, VoiceOutcomeContractStatus, VoiceOutcomeContractSuiteReport } from './outcomeContract';
|
|
68
70
|
export type { VoiceTelephonyOutcomeAction, VoiceTelephonyOutcomeDecision, VoiceTelephonyOutcomePolicy, VoiceTelephonyOutcomeProviderEvent, VoiceTelephonyOutcomeRouteResult, VoiceTelephonyOutcomeStatusDecision, VoiceTelephonyWebhookDecision, VoiceTelephonyWebhookHandlerOptions, VoiceTelephonyWebhookIdempotencyStore, VoiceTelephonyWebhookParseInput, VoiceTelephonyWebhookProvider, VoiceTelephonyWebhookRoutesOptions, VoiceTelephonyWebhookVerificationResult, StoredVoiceTelephonyWebhookDecision } from './telephonyOutcome';
|
package/dist/index.js
CHANGED
|
@@ -9744,12 +9744,28 @@ var resolveCarriers = async (options, input) => {
|
|
|
9744
9744
|
providers: [...providers]
|
|
9745
9745
|
});
|
|
9746
9746
|
};
|
|
9747
|
+
var summarizeLiveLatency = (events, options) => {
|
|
9748
|
+
const warnAfterMs = options.liveLatencyWarnAfterMs ?? 1800;
|
|
9749
|
+
const failAfterMs = options.liveLatencyFailAfterMs ?? 3200;
|
|
9750
|
+
const latencies = events.filter((event) => event.type === "client.live_latency").map((event) => typeof event.payload.latencyMs === "number" ? event.payload.latencyMs : typeof event.payload.elapsedMs === "number" ? event.payload.elapsedMs : undefined).filter((value) => typeof value === "number");
|
|
9751
|
+
const failed = latencies.filter((value) => value > failAfterMs).length;
|
|
9752
|
+
const warnings = latencies.filter((value) => value > warnAfterMs && value <= failAfterMs).length;
|
|
9753
|
+
const averageLatencyMs = latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined;
|
|
9754
|
+
return {
|
|
9755
|
+
averageLatencyMs,
|
|
9756
|
+
failed,
|
|
9757
|
+
status: latencies.length === 0 ? "warn" : failed > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
|
|
9758
|
+
total: latencies.length,
|
|
9759
|
+
warnings
|
|
9760
|
+
};
|
|
9761
|
+
};
|
|
9747
9762
|
var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
9748
9763
|
const request = input.request ?? new Request("http://localhost/");
|
|
9749
9764
|
const query = input.query ?? {};
|
|
9750
9765
|
const events = await options.store.list();
|
|
9751
9766
|
const routingEvents = listVoiceRoutingEvents(events);
|
|
9752
9767
|
const routingSessions = summarizeVoiceRoutingSessions(routingEvents);
|
|
9768
|
+
const liveLatency = summarizeLiveLatency(events, options);
|
|
9753
9769
|
const [quality, providers, sessions, handoffs, carriers] = await Promise.all([
|
|
9754
9770
|
evaluateVoiceQuality({ events }),
|
|
9755
9771
|
Promise.all([
|
|
@@ -9856,6 +9872,20 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
9856
9872
|
]
|
|
9857
9873
|
}
|
|
9858
9874
|
];
|
|
9875
|
+
checks.push({
|
|
9876
|
+
detail: liveLatency.total === 0 ? "No browser live-latency measurements are recorded yet." : liveLatency.status === "pass" ? `Live browser turn latency averages ${liveLatency.averageLatencyMs}ms.` : `${liveLatency.failed} failed and ${liveLatency.warnings} warned live-latency measurement(s).`,
|
|
9877
|
+
href: options.links?.liveLatency ?? "/traces",
|
|
9878
|
+
label: "Live latency proof",
|
|
9879
|
+
status: liveLatency.status,
|
|
9880
|
+
value: liveLatency.averageLatencyMs === undefined ? `${liveLatency.total} samples` : `${liveLatency.averageLatencyMs}ms avg`,
|
|
9881
|
+
actions: liveLatency.status === "pass" ? [] : [
|
|
9882
|
+
{
|
|
9883
|
+
description: "Run a live browser voice turn and inspect the persisted latency trace.",
|
|
9884
|
+
href: options.links?.liveLatency ?? "/traces",
|
|
9885
|
+
label: "Open live latency traces"
|
|
9886
|
+
}
|
|
9887
|
+
]
|
|
9888
|
+
});
|
|
9859
9889
|
const carrierSummary = carriers ? {
|
|
9860
9890
|
failing: carriers.summary.failing,
|
|
9861
9891
|
providers: carriers.summary.providers,
|
|
@@ -9886,6 +9916,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
9886
9916
|
carriers: "/carriers",
|
|
9887
9917
|
handoffs: "/handoffs",
|
|
9888
9918
|
handoffRetry: "/api/voice-handoffs/retry",
|
|
9919
|
+
liveLatency: "/traces",
|
|
9889
9920
|
quality: "/quality",
|
|
9890
9921
|
resilience: "/resilience",
|
|
9891
9922
|
sessions: "/sessions",
|
|
@@ -9898,6 +9929,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
9898
9929
|
failed: handoffs.failed,
|
|
9899
9930
|
total: handoffs.total
|
|
9900
9931
|
},
|
|
9932
|
+
liveLatency,
|
|
9901
9933
|
providers: {
|
|
9902
9934
|
degraded: degradedProviders,
|
|
9903
9935
|
total: providers.length
|
|
@@ -11375,10 +11407,82 @@ var createVoiceTurnLatencyRoutes = (options) => {
|
|
|
11375
11407
|
}
|
|
11376
11408
|
return routes;
|
|
11377
11409
|
};
|
|
11378
|
-
// src/
|
|
11410
|
+
// src/liveLatency.ts
|
|
11379
11411
|
import { Elysia as Elysia19 } from "elysia";
|
|
11380
|
-
var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
|
|
11381
11412
|
var escapeHtml20 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11413
|
+
var percentile = (values, percentileValue) => {
|
|
11414
|
+
if (values.length === 0) {
|
|
11415
|
+
return;
|
|
11416
|
+
}
|
|
11417
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
11418
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(percentileValue / 100 * sorted.length) - 1));
|
|
11419
|
+
return Math.round(sorted[index] ?? 0);
|
|
11420
|
+
};
|
|
11421
|
+
var getLatency = (payload) => typeof payload.latencyMs === "number" ? payload.latencyMs : typeof payload.elapsedMs === "number" ? payload.elapsedMs : undefined;
|
|
11422
|
+
var summarizeVoiceLiveLatency = async (options) => {
|
|
11423
|
+
const warnAfterMs = options.warnAfterMs ?? 1800;
|
|
11424
|
+
const failAfterMs = options.failAfterMs ?? 3200;
|
|
11425
|
+
const events = await options.store.list({
|
|
11426
|
+
limit: options.limit ?? 100,
|
|
11427
|
+
type: "client.live_latency"
|
|
11428
|
+
});
|
|
11429
|
+
const recent = events.map((event) => {
|
|
11430
|
+
const latencyMs = getLatency(event.payload);
|
|
11431
|
+
if (latencyMs === undefined) {
|
|
11432
|
+
return;
|
|
11433
|
+
}
|
|
11434
|
+
return {
|
|
11435
|
+
at: event.at,
|
|
11436
|
+
latencyMs,
|
|
11437
|
+
sessionId: event.sessionId,
|
|
11438
|
+
status: typeof event.payload.status === "string" ? event.payload.status : undefined,
|
|
11439
|
+
traceId: event.traceId
|
|
11440
|
+
};
|
|
11441
|
+
}).filter((sample) => Boolean(sample)).sort((left, right) => right.at - left.at);
|
|
11442
|
+
const latencies = recent.map((sample) => sample.latencyMs);
|
|
11443
|
+
const failed = latencies.filter((value) => value > failAfterMs).length;
|
|
11444
|
+
const warnings = latencies.filter((value) => value > warnAfterMs && value <= failAfterMs).length;
|
|
11445
|
+
return {
|
|
11446
|
+
averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
|
|
11447
|
+
checkedAt: Date.now(),
|
|
11448
|
+
failed,
|
|
11449
|
+
p50LatencyMs: percentile(latencies, 50),
|
|
11450
|
+
p95LatencyMs: percentile(latencies, 95),
|
|
11451
|
+
recent,
|
|
11452
|
+
status: latencies.length === 0 ? "empty" : failed > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
|
|
11453
|
+
total: latencies.length,
|
|
11454
|
+
warnings
|
|
11455
|
+
};
|
|
11456
|
+
};
|
|
11457
|
+
var formatMs3 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
|
|
11458
|
+
var renderVoiceLiveLatencyHTML = (report, options = {}) => {
|
|
11459
|
+
const title = options.title ?? "Voice Live Latency";
|
|
11460
|
+
const rows = report.recent.map((sample) => `<tr><td>${escapeHtml20(sample.sessionId)}</td><td>${escapeHtml20(formatMs3(sample.latencyMs))}</td><td>${escapeHtml20(sample.status ?? "unknown")}</td><td>${escapeHtml20(new Date(sample.at).toLocaleString())}</td></tr>`).join("");
|
|
11461
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml20(title)}</title><style>body{background:#0c0f14;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1060px;padding:32px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(245,158,11,.1));border:1px solid #26313d;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #3f3f46;border-radius:999px;display:inline-flex;padding:8px 12px}.pass{color:#86efac}.warn,.empty{color:#fbbf24}.fail{color:#fca5a5}.metrics{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:18px 0}.metrics article,table{background:#141922;border:1px solid #26313d;border-radius:18px}.metrics article{padding:16px}.metrics span{color:#a8b0b8}.metrics strong{display:block;font-size:2rem;margin-top:.25rem}table{border-collapse:collapse;overflow:hidden;width:100%}td,th{border-bottom:1px solid #26313d;padding:12px;text-align:left}@media(max-width:760px){main{padding:20px}}</style></head><body><main><section class="hero"><p class="eyebrow">Browser proof</p><h1>${escapeHtml20(title)}</h1><p>Recent real browser speech-to-assistant response measurements from persisted <code>client.live_latency</code> traces.</p><p class="status ${escapeHtml20(report.status)}">Status: ${escapeHtml20(report.status)}</p><section class="metrics"><article><span>p50</span><strong>${escapeHtml20(formatMs3(report.p50LatencyMs))}</strong></article><article><span>p95</span><strong>${escapeHtml20(formatMs3(report.p95LatencyMs))}</strong></article><article><span>Average</span><strong>${escapeHtml20(formatMs3(report.averageLatencyMs))}</strong></article><article><span>Samples</span><strong>${String(report.total)}</strong></article></section></section><table><thead><tr><th>Session</th><th>Latency</th><th>Status</th><th>Measured</th></tr></thead><tbody>${rows || '<tr><td colspan="4">No live latency samples yet.</td></tr>'}</tbody></table></main></body></html>`;
|
|
11462
|
+
};
|
|
11463
|
+
var createVoiceLiveLatencyRoutes = (options) => {
|
|
11464
|
+
const path = options.path ?? "/api/live-latency";
|
|
11465
|
+
const htmlPath = options.htmlPath === undefined ? "/live-latency" : options.htmlPath;
|
|
11466
|
+
const routes = new Elysia19({
|
|
11467
|
+
name: options.name ?? "absolutejs-voice-live-latency"
|
|
11468
|
+
}).get(path, () => summarizeVoiceLiveLatency(options));
|
|
11469
|
+
if (htmlPath) {
|
|
11470
|
+
routes.get(htmlPath, async () => {
|
|
11471
|
+
const report = await summarizeVoiceLiveLatency(options);
|
|
11472
|
+
return new Response(renderVoiceLiveLatencyHTML(report, options), {
|
|
11473
|
+
headers: {
|
|
11474
|
+
"content-type": "text/html; charset=utf-8",
|
|
11475
|
+
...options.headers
|
|
11476
|
+
}
|
|
11477
|
+
});
|
|
11478
|
+
});
|
|
11479
|
+
}
|
|
11480
|
+
return routes;
|
|
11481
|
+
};
|
|
11482
|
+
// src/turnQuality.ts
|
|
11483
|
+
import { Elysia as Elysia20 } from "elysia";
|
|
11484
|
+
var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
|
|
11485
|
+
var escapeHtml21 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11382
11486
|
var getTurnLatencyMs = (turn) => {
|
|
11383
11487
|
const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
|
|
11384
11488
|
if (firstTranscriptAt === undefined) {
|
|
@@ -11449,24 +11553,24 @@ var summarizeVoiceTurnQuality = async (options) => {
|
|
|
11449
11553
|
};
|
|
11450
11554
|
var renderVoiceTurnQualityHTML = (report, options = {}) => {
|
|
11451
11555
|
const title = options.title ?? "Voice Turn Quality";
|
|
11452
|
-
const turns = report.turns.map((turn) => `<article class="turn ${
|
|
11556
|
+
const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml21(turn.status)}">
|
|
11453
11557
|
<div class="turn-header">
|
|
11454
11558
|
<div>
|
|
11455
|
-
<p class="eyebrow">${
|
|
11456
|
-
<h2>${
|
|
11559
|
+
<p class="eyebrow">${escapeHtml21(turn.sessionId)} \xB7 ${escapeHtml21(turn.turnId)}</p>
|
|
11560
|
+
<h2>${escapeHtml21(turn.text || "Empty turn")}</h2>
|
|
11457
11561
|
</div>
|
|
11458
|
-
<strong>${
|
|
11562
|
+
<strong>${escapeHtml21(turn.status)}</strong>
|
|
11459
11563
|
</div>
|
|
11460
11564
|
<dl>
|
|
11461
|
-
<div><dt>Source</dt><dd>${
|
|
11565
|
+
<div><dt>Source</dt><dd>${escapeHtml21(turn.source ?? "unknown")}</dd></div>
|
|
11462
11566
|
<div><dt>Confidence</dt><dd>${turn.averageConfidence === undefined ? "n/a" : `${Math.round(turn.averageConfidence * 100)}%`}</dd></div>
|
|
11463
|
-
<div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${
|
|
11464
|
-
<div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${
|
|
11567
|
+
<div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml21(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
|
|
11568
|
+
<div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml21(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
|
|
11465
11569
|
<div><dt>Transcripts</dt><dd>${String(turn.selectedTranscriptCount)} selected \xB7 ${String(turn.finalTranscriptCount)} final \xB7 ${String(turn.partialTranscriptCount)} partial</dd></div>
|
|
11466
11570
|
<div><dt>Cost</dt><dd>${turn.costUnits === undefined ? "n/a" : String(turn.costUnits)}</dd></div>
|
|
11467
11571
|
</dl>
|
|
11468
11572
|
</article>`).join("");
|
|
11469
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
11573
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml21(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>${escapeHtml21(title)}</h1><div class="summary"><span class="pill ${escapeHtml21(report.status)}">${escapeHtml21(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>`;
|
|
11470
11574
|
};
|
|
11471
11575
|
var createVoiceTurnQualityJSONHandler = (options) => async () => summarizeVoiceTurnQuality(options);
|
|
11472
11576
|
var createVoiceTurnQualityHTMLHandler = (options) => async () => {
|
|
@@ -11483,7 +11587,7 @@ var createVoiceTurnQualityHTMLHandler = (options) => async () => {
|
|
|
11483
11587
|
var createVoiceTurnQualityRoutes = (options) => {
|
|
11484
11588
|
const path = options.path ?? "/api/turn-quality";
|
|
11485
11589
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
11486
|
-
const routes = new
|
|
11590
|
+
const routes = new Elysia20({
|
|
11487
11591
|
name: options.name ?? "absolutejs-voice-turn-quality"
|
|
11488
11592
|
}).get(path, createVoiceTurnQualityJSONHandler(options));
|
|
11489
11593
|
if (htmlPath) {
|
|
@@ -11492,8 +11596,8 @@ var createVoiceTurnQualityRoutes = (options) => {
|
|
|
11492
11596
|
return routes;
|
|
11493
11597
|
};
|
|
11494
11598
|
// src/outcomeContract.ts
|
|
11495
|
-
import { Elysia as
|
|
11496
|
-
var
|
|
11599
|
+
import { Elysia as Elysia21 } from "elysia";
|
|
11600
|
+
var escapeHtml22 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11497
11601
|
var getPayloadString = (event, key) => typeof event.payload[key] === "string" ? event.payload[key] : undefined;
|
|
11498
11602
|
var toList = async (input) => Array.isArray(input) ? input : await input?.list() ?? [];
|
|
11499
11603
|
var hydrateSessions = async (input) => {
|
|
@@ -11601,9 +11705,9 @@ var renderVoiceOutcomeContractHTML = (report, options = {}) => {
|
|
|
11601
11705
|
const contracts = report.contracts.map((contract) => `<section class="contract ${contract.pass ? "pass" : "fail"}">
|
|
11602
11706
|
<div class="contract-header">
|
|
11603
11707
|
<div>
|
|
11604
|
-
<p class="eyebrow">${
|
|
11605
|
-
<h2>${
|
|
11606
|
-
${contract.description ? `<p>${
|
|
11708
|
+
<p class="eyebrow">${escapeHtml22(contract.contractId)}</p>
|
|
11709
|
+
<h2>${escapeHtml22(contract.label ?? contract.contractId)}</h2>
|
|
11710
|
+
${contract.description ? `<p>${escapeHtml22(contract.description)}</p>` : ""}
|
|
11607
11711
|
</div>
|
|
11608
11712
|
<strong>${contract.pass ? "pass" : "fail"}</strong>
|
|
11609
11713
|
</div>
|
|
@@ -11614,9 +11718,9 @@ var renderVoiceOutcomeContractHTML = (report, options = {}) => {
|
|
|
11614
11718
|
<span>handoffs ${String(contract.matched.handoffs)}</span>
|
|
11615
11719
|
<span>events ${String(contract.matched.integrationEvents)}</span>
|
|
11616
11720
|
</div>
|
|
11617
|
-
${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${
|
|
11721
|
+
${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml22(issue.message)}</li>`).join("")}</ul>` : ""}
|
|
11618
11722
|
</section>`).join("");
|
|
11619
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
11723
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml22(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>${escapeHtml22(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>`;
|
|
11620
11724
|
};
|
|
11621
11725
|
var createVoiceOutcomeContractJSONHandler = (options) => async () => runVoiceOutcomeContractSuite(options);
|
|
11622
11726
|
var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
|
|
@@ -11632,7 +11736,7 @@ var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
|
|
|
11632
11736
|
var createVoiceOutcomeContractRoutes = (options) => {
|
|
11633
11737
|
const path = options.path ?? "/api/outcome-contracts";
|
|
11634
11738
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
11635
|
-
const routes = new
|
|
11739
|
+
const routes = new Elysia21({
|
|
11636
11740
|
name: options.name ?? "absolutejs-voice-outcome-contracts"
|
|
11637
11741
|
}).get(path, createVoiceOutcomeContractJSONHandler(options));
|
|
11638
11742
|
if (htmlPath) {
|
|
@@ -11641,7 +11745,7 @@ var createVoiceOutcomeContractRoutes = (options) => {
|
|
|
11641
11745
|
return routes;
|
|
11642
11746
|
};
|
|
11643
11747
|
// src/telephonyOutcome.ts
|
|
11644
|
-
import { Elysia as
|
|
11748
|
+
import { Elysia as Elysia22 } from "elysia";
|
|
11645
11749
|
var DEFAULT_COMPLETED_STATUSES = [
|
|
11646
11750
|
"answered",
|
|
11647
11751
|
"completed",
|
|
@@ -12288,7 +12392,7 @@ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
|
|
|
12288
12392
|
var createVoiceTelephonyWebhookRoutes = (options = {}) => {
|
|
12289
12393
|
const path = options.path ?? "/api/voice/telephony/webhook";
|
|
12290
12394
|
const handler = createVoiceTelephonyWebhookHandler(options);
|
|
12291
|
-
return new
|
|
12395
|
+
return new Elysia22({
|
|
12292
12396
|
name: options.name ?? "absolutejs-voice-telephony-webhooks"
|
|
12293
12397
|
}).post(path, async ({ query, request }) => {
|
|
12294
12398
|
try {
|
|
@@ -14556,7 +14660,7 @@ var createVoiceMemoryStore = () => {
|
|
|
14556
14660
|
return { get, getOrCreate, list, remove, set };
|
|
14557
14661
|
};
|
|
14558
14662
|
// src/opsWebhook.ts
|
|
14559
|
-
import { Elysia as
|
|
14663
|
+
import { Elysia as Elysia23 } from "elysia";
|
|
14560
14664
|
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
14561
14665
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
14562
14666
|
const encoder = new TextEncoder;
|
|
@@ -14686,7 +14790,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
|
14686
14790
|
};
|
|
14687
14791
|
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
14688
14792
|
const path = options.path ?? "/api/voice-ops/webhook";
|
|
14689
|
-
return new
|
|
14793
|
+
return new Elysia23().post(path, async ({ body, request, set }) => {
|
|
14690
14794
|
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
14691
14795
|
if (options.signingSecret) {
|
|
14692
14796
|
const verification = await verifyVoiceOpsWebhookSignature({
|
|
@@ -16415,7 +16519,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
|
|
|
16415
16519
|
};
|
|
16416
16520
|
// src/telephony/twilio.ts
|
|
16417
16521
|
import { Buffer as Buffer3 } from "buffer";
|
|
16418
|
-
import { Elysia as
|
|
16522
|
+
import { Elysia as Elysia24 } from "elysia";
|
|
16419
16523
|
var TWILIO_MULAW_SAMPLE_RATE = 8000;
|
|
16420
16524
|
var VOICE_PCM_SAMPLE_RATE = 16000;
|
|
16421
16525
|
var escapeXml2 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
@@ -16445,7 +16549,7 @@ var resolveTwilioStreamParameters = async (parameters, input) => {
|
|
|
16445
16549
|
return parameters;
|
|
16446
16550
|
};
|
|
16447
16551
|
var joinUrlPath = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
16448
|
-
var
|
|
16552
|
+
var escapeHtml23 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
16449
16553
|
var getWebhookVerificationUrl = (webhook, input) => {
|
|
16450
16554
|
if (!webhook?.verificationUrl) {
|
|
16451
16555
|
return;
|
|
@@ -16488,23 +16592,23 @@ var buildTwilioVoiceSetupStatus = async (options, input) => {
|
|
|
16488
16592
|
};
|
|
16489
16593
|
var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
16490
16594
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
|
|
16491
|
-
<h1>${
|
|
16595
|
+
<h1>${escapeHtml23(title)}</h1>
|
|
16492
16596
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
16493
16597
|
<section>
|
|
16494
16598
|
<h2>URLs</h2>
|
|
16495
16599
|
<ul>
|
|
16496
|
-
<li><strong>TwiML:</strong> <code>${
|
|
16497
|
-
<li><strong>Media stream:</strong> <code>${
|
|
16498
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
16600
|
+
<li><strong>TwiML:</strong> <code>${escapeHtml23(status.urls.twiml)}</code></li>
|
|
16601
|
+
<li><strong>Media stream:</strong> <code>${escapeHtml23(status.urls.stream)}</code></li>
|
|
16602
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml23(status.urls.webhook)}</code></li>
|
|
16499
16603
|
</ul>
|
|
16500
16604
|
</section>
|
|
16501
16605
|
<section>
|
|
16502
16606
|
<h2>Signing</h2>
|
|
16503
16607
|
<p>Mode: <code>${status.signing.mode}</code></p>
|
|
16504
|
-
${status.signing.verificationUrl ? `<p>Verification URL: <code>${
|
|
16608
|
+
${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml23(status.signing.verificationUrl)}</code></p>` : ""}
|
|
16505
16609
|
</section>
|
|
16506
|
-
${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${
|
|
16507
|
-
${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
16610
|
+
${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml23(name)}</code></li>`).join("")}</ul></section>` : ""}
|
|
16611
|
+
${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml23(warning)}</li>`).join("")}</ul></section>` : ""}
|
|
16508
16612
|
</main>`;
|
|
16509
16613
|
var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&", "&");
|
|
16510
16614
|
var createSmokeCheck = (name, status, message, details) => ({
|
|
@@ -16515,20 +16619,20 @@ var createSmokeCheck = (name, status, message, details) => ({
|
|
|
16515
16619
|
});
|
|
16516
16620
|
var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
16517
16621
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
|
|
16518
|
-
<h1>${
|
|
16622
|
+
<h1>${escapeHtml23(title)}</h1>
|
|
16519
16623
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
16520
16624
|
<section>
|
|
16521
16625
|
<h2>Checks</h2>
|
|
16522
16626
|
<ul>
|
|
16523
|
-
${report.checks.map((check) => `<li><strong>${
|
|
16627
|
+
${report.checks.map((check) => `<li><strong>${escapeHtml23(check.name)}</strong>: ${escapeHtml23(check.status)}${check.message ? ` - ${escapeHtml23(check.message)}` : ""}</li>`).join("")}
|
|
16524
16628
|
</ul>
|
|
16525
16629
|
</section>
|
|
16526
16630
|
<section>
|
|
16527
16631
|
<h2>Observed URLs</h2>
|
|
16528
16632
|
<ul>
|
|
16529
|
-
<li><strong>TwiML:</strong> <code>${
|
|
16530
|
-
<li><strong>Stream:</strong> <code>${
|
|
16531
|
-
<li><strong>Webhook:</strong> <code>${
|
|
16633
|
+
<li><strong>TwiML:</strong> <code>${escapeHtml23(report.setup.urls.twiml)}</code></li>
|
|
16634
|
+
<li><strong>Stream:</strong> <code>${escapeHtml23(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
|
|
16635
|
+
<li><strong>Webhook:</strong> <code>${escapeHtml23(report.setup.urls.webhook)}</code></li>
|
|
16532
16636
|
</ul>
|
|
16533
16637
|
</section>
|
|
16534
16638
|
</main>`;
|
|
@@ -16988,7 +17092,7 @@ var createTwilioVoiceRoutes = (options) => {
|
|
|
16988
17092
|
const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
|
|
16989
17093
|
const bridges = new WeakMap;
|
|
16990
17094
|
const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
|
|
16991
|
-
const app = new
|
|
17095
|
+
const app = new Elysia24({
|
|
16992
17096
|
name: options.name ?? "absolutejs-voice-twilio"
|
|
16993
17097
|
}).get(twimlPath, async ({ query, request }) => {
|
|
16994
17098
|
const streamUrl = await resolveTwilioStreamUrl(options, {
|
|
@@ -17124,9 +17228,9 @@ var createTwilioVoiceRoutes = (options) => {
|
|
|
17124
17228
|
};
|
|
17125
17229
|
// src/telephony/telnyx.ts
|
|
17126
17230
|
import { Buffer as Buffer4 } from "buffer";
|
|
17127
|
-
import { Elysia as
|
|
17231
|
+
import { Elysia as Elysia25 } from "elysia";
|
|
17128
17232
|
var escapeXml3 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
17129
|
-
var
|
|
17233
|
+
var escapeHtml24 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
17130
17234
|
var joinUrlPath2 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
17131
17235
|
var resolveRequestOrigin2 = (request) => {
|
|
17132
17236
|
const url = new URL(request.url);
|
|
@@ -17327,21 +17431,21 @@ var buildTelnyxVoiceSetupStatus = async (options, input) => {
|
|
|
17327
17431
|
};
|
|
17328
17432
|
var renderTelnyxSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
17329
17433
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx setup</p>
|
|
17330
|
-
<h1>${
|
|
17434
|
+
<h1>${escapeHtml24(title)}</h1>
|
|
17331
17435
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
17332
17436
|
<ul>
|
|
17333
|
-
<li><strong>TeXML:</strong> <code>${
|
|
17334
|
-
<li><strong>Media stream:</strong> <code>${
|
|
17335
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
17437
|
+
<li><strong>TeXML:</strong> <code>${escapeHtml24(status.urls.texml)}</code></li>
|
|
17438
|
+
<li><strong>Media stream:</strong> <code>${escapeHtml24(status.urls.stream)}</code></li>
|
|
17439
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml24(status.urls.webhook)}</code></li>
|
|
17336
17440
|
</ul>
|
|
17337
|
-
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${
|
|
17338
|
-
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
17441
|
+
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml24(name)}</code></li>`).join("")}</ul>` : ""}
|
|
17442
|
+
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml24(warning)}</li>`).join("")}</ul>` : ""}
|
|
17339
17443
|
</main>`;
|
|
17340
17444
|
var renderTelnyxSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
17341
17445
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx smoke test</p>
|
|
17342
|
-
<h1>${
|
|
17446
|
+
<h1>${escapeHtml24(title)}</h1>
|
|
17343
17447
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
17344
|
-
<ul>${report.checks.map((check) => `<li><strong>${
|
|
17448
|
+
<ul>${report.checks.map((check) => `<li><strong>${escapeHtml24(check.name)}</strong>: ${escapeHtml24(check.status)}${check.message ? ` - ${escapeHtml24(check.message)}` : ""}</li>`).join("")}</ul>
|
|
17345
17449
|
</main>`;
|
|
17346
17450
|
var runTelnyxSmokeTest = async (input) => {
|
|
17347
17451
|
const setup = await buildTelnyxVoiceSetupStatus(input.options, input);
|
|
@@ -17435,7 +17539,7 @@ var createTelnyxVoiceRoutes = (options = {}) => {
|
|
|
17435
17539
|
publicKey: options.webhook?.publicKey,
|
|
17436
17540
|
toleranceSeconds: options.webhook?.toleranceSeconds
|
|
17437
17541
|
}) : undefined);
|
|
17438
|
-
const app = new
|
|
17542
|
+
const app = new Elysia25({
|
|
17439
17543
|
name: options.name ?? "absolutejs-voice-telnyx"
|
|
17440
17544
|
}).get(texmlPath, async ({ query, request }) => {
|
|
17441
17545
|
const streamUrl = await resolveTelnyxStreamUrl(options, {
|
|
@@ -17545,9 +17649,9 @@ var createTelnyxVoiceRoutes = (options = {}) => {
|
|
|
17545
17649
|
};
|
|
17546
17650
|
// src/telephony/plivo.ts
|
|
17547
17651
|
import { Buffer as Buffer5 } from "buffer";
|
|
17548
|
-
import { Elysia as
|
|
17652
|
+
import { Elysia as Elysia26 } from "elysia";
|
|
17549
17653
|
var escapeXml4 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
17550
|
-
var
|
|
17654
|
+
var escapeHtml25 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
17551
17655
|
var joinUrlPath3 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
17552
17656
|
var resolveRequestOrigin3 = (request) => {
|
|
17553
17657
|
const url = new URL(request.url);
|
|
@@ -17798,21 +17902,21 @@ var buildPlivoVoiceSetupStatus = async (options, input) => {
|
|
|
17798
17902
|
};
|
|
17799
17903
|
var renderPlivoSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
17800
17904
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo setup</p>
|
|
17801
|
-
<h1>${
|
|
17905
|
+
<h1>${escapeHtml25(title)}</h1>
|
|
17802
17906
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
17803
17907
|
<ul>
|
|
17804
|
-
<li><strong>Answer XML:</strong> <code>${
|
|
17805
|
-
<li><strong>Audio stream:</strong> <code>${
|
|
17806
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
17908
|
+
<li><strong>Answer XML:</strong> <code>${escapeHtml25(status.urls.answer)}</code></li>
|
|
17909
|
+
<li><strong>Audio stream:</strong> <code>${escapeHtml25(status.urls.stream)}</code></li>
|
|
17910
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml25(status.urls.webhook)}</code></li>
|
|
17807
17911
|
</ul>
|
|
17808
|
-
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${
|
|
17809
|
-
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
17912
|
+
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml25(name)}</code></li>`).join("")}</ul>` : ""}
|
|
17913
|
+
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml25(warning)}</li>`).join("")}</ul>` : ""}
|
|
17810
17914
|
</main>`;
|
|
17811
17915
|
var renderPlivoSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
17812
17916
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo smoke test</p>
|
|
17813
|
-
<h1>${
|
|
17917
|
+
<h1>${escapeHtml25(title)}</h1>
|
|
17814
17918
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
17815
|
-
<ul>${report.checks.map((check) => `<li><strong>${
|
|
17919
|
+
<ul>${report.checks.map((check) => `<li><strong>${escapeHtml25(check.name)}</strong>: ${escapeHtml25(check.status)}${check.message ? ` - ${escapeHtml25(check.message)}` : ""}</li>`).join("")}</ul>
|
|
17816
17920
|
</main>`;
|
|
17817
17921
|
var runPlivoSmokeTest = async (input) => {
|
|
17818
17922
|
const setup = await buildPlivoVoiceSetupStatus(input.options, input);
|
|
@@ -17907,7 +18011,7 @@ var createPlivoVoiceRoutes = (options = {}) => {
|
|
|
17907
18011
|
request: input.request
|
|
17908
18012
|
}) : verificationUrl ?? input.request.url
|
|
17909
18013
|
}) : undefined);
|
|
17910
|
-
const app = new
|
|
18014
|
+
const app = new Elysia26({
|
|
17911
18015
|
name: options.name ?? "absolutejs-voice-plivo"
|
|
17912
18016
|
}).get(answerPath, async ({ query, request }) => {
|
|
17913
18017
|
const streamUrl = await resolvePlivoStreamUrl(options, {
|
|
@@ -18093,6 +18197,7 @@ export {
|
|
|
18093
18197
|
summarizeVoiceOpsTasks,
|
|
18094
18198
|
summarizeVoiceOpsTaskQueue,
|
|
18095
18199
|
summarizeVoiceOpsTaskAnalytics,
|
|
18200
|
+
summarizeVoiceLiveLatency,
|
|
18096
18201
|
summarizeVoiceIntegrationEvents,
|
|
18097
18202
|
summarizeVoiceHandoffHealth,
|
|
18098
18203
|
summarizeVoiceHandoffDeliveries,
|
|
@@ -18145,6 +18250,7 @@ export {
|
|
|
18145
18250
|
renderVoiceProductionReadinessHTML,
|
|
18146
18251
|
renderVoiceOutcomeContractHTML,
|
|
18147
18252
|
renderVoiceOpsConsoleHTML,
|
|
18253
|
+
renderVoiceLiveLatencyHTML,
|
|
18148
18254
|
renderVoiceHandoffHealthHTML,
|
|
18149
18255
|
renderVoiceEvalHTML,
|
|
18150
18256
|
renderVoiceEvalBaselineHTML,
|
|
@@ -18282,6 +18388,7 @@ export {
|
|
|
18282
18388
|
createVoiceMemoryStore,
|
|
18283
18389
|
createVoiceMemoryHandoffDeliveryStore,
|
|
18284
18390
|
createVoiceMemoryAssistantMemoryStore,
|
|
18391
|
+
createVoiceLiveLatencyRoutes,
|
|
18285
18392
|
createVoiceLinearIssueUpdateSink,
|
|
18286
18393
|
createVoiceLinearIssueSyncSinks,
|
|
18287
18394
|
createVoiceLinearIssueSink,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import type { VoiceTraceEventStore } from './trace';
|
|
3
|
+
export type VoiceLiveLatencyStatus = 'empty' | 'fail' | 'pass' | 'warn';
|
|
4
|
+
export type VoiceLiveLatencySample = {
|
|
5
|
+
at: number;
|
|
6
|
+
latencyMs: number;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
status?: string;
|
|
9
|
+
traceId?: string;
|
|
10
|
+
};
|
|
11
|
+
export type VoiceLiveLatencyReport = {
|
|
12
|
+
averageLatencyMs?: number;
|
|
13
|
+
checkedAt: number;
|
|
14
|
+
failed: number;
|
|
15
|
+
p50LatencyMs?: number;
|
|
16
|
+
p95LatencyMs?: number;
|
|
17
|
+
recent: VoiceLiveLatencySample[];
|
|
18
|
+
status: VoiceLiveLatencyStatus;
|
|
19
|
+
total: number;
|
|
20
|
+
warnings: number;
|
|
21
|
+
};
|
|
22
|
+
export type VoiceLiveLatencyOptions = {
|
|
23
|
+
failAfterMs?: number;
|
|
24
|
+
limit?: number;
|
|
25
|
+
store: VoiceTraceEventStore;
|
|
26
|
+
warnAfterMs?: number;
|
|
27
|
+
};
|
|
28
|
+
export type VoiceLiveLatencyRoutesOptions = VoiceLiveLatencyOptions & {
|
|
29
|
+
headers?: HeadersInit;
|
|
30
|
+
htmlPath?: false | string;
|
|
31
|
+
name?: string;
|
|
32
|
+
path?: string;
|
|
33
|
+
title?: string;
|
|
34
|
+
};
|
|
35
|
+
export declare const summarizeVoiceLiveLatency: (options: VoiceLiveLatencyOptions) => Promise<VoiceLiveLatencyReport>;
|
|
36
|
+
export declare const renderVoiceLiveLatencyHTML: (report: VoiceLiveLatencyReport, options?: {
|
|
37
|
+
title?: string;
|
|
38
|
+
}) => string;
|
|
39
|
+
export declare const createVoiceLiveLatencyRoutes: (options: VoiceLiveLatencyRoutesOptions) => Elysia<"", {
|
|
40
|
+
decorator: {};
|
|
41
|
+
store: {};
|
|
42
|
+
derive: {};
|
|
43
|
+
resolve: {};
|
|
44
|
+
}, {
|
|
45
|
+
typebox: {};
|
|
46
|
+
error: {};
|
|
47
|
+
}, {
|
|
48
|
+
schema: {};
|
|
49
|
+
standaloneSchema: {};
|
|
50
|
+
macro: {};
|
|
51
|
+
macroFn: {};
|
|
52
|
+
parser: {};
|
|
53
|
+
response: {};
|
|
54
|
+
}, {
|
|
55
|
+
[x: string]: {
|
|
56
|
+
get: {
|
|
57
|
+
body: unknown;
|
|
58
|
+
params: {};
|
|
59
|
+
query: unknown;
|
|
60
|
+
headers: unknown;
|
|
61
|
+
response: {
|
|
62
|
+
200: VoiceLiveLatencyReport;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
}, {
|
|
67
|
+
derive: {};
|
|
68
|
+
resolve: {};
|
|
69
|
+
schema: {};
|
|
70
|
+
standaloneSchema: {};
|
|
71
|
+
response: {};
|
|
72
|
+
}, {
|
|
73
|
+
derive: {};
|
|
74
|
+
resolve: {};
|
|
75
|
+
schema: {};
|
|
76
|
+
standaloneSchema: {};
|
|
77
|
+
response: {};
|
|
78
|
+
}>;
|
|
@@ -23,6 +23,7 @@ export type VoiceProductionReadinessReport = {
|
|
|
23
23
|
carriers?: string;
|
|
24
24
|
handoffs?: string;
|
|
25
25
|
handoffRetry?: string;
|
|
26
|
+
liveLatency?: string;
|
|
26
27
|
quality?: string;
|
|
27
28
|
resilience?: string;
|
|
28
29
|
sessions?: string;
|
|
@@ -40,6 +41,13 @@ export type VoiceProductionReadinessReport = {
|
|
|
40
41
|
failed: number;
|
|
41
42
|
total: number;
|
|
42
43
|
};
|
|
44
|
+
liveLatency: {
|
|
45
|
+
averageLatencyMs?: number;
|
|
46
|
+
failed: number;
|
|
47
|
+
status: VoiceProductionReadinessStatus;
|
|
48
|
+
total: number;
|
|
49
|
+
warnings: number;
|
|
50
|
+
};
|
|
43
51
|
providers: {
|
|
44
52
|
degraded: number;
|
|
45
53
|
total: number;
|
|
@@ -73,6 +81,8 @@ export type VoiceProductionReadinessRoutesOptions = {
|
|
|
73
81
|
sttProviders?: readonly string[];
|
|
74
82
|
title?: string;
|
|
75
83
|
ttsProviders?: readonly string[];
|
|
84
|
+
liveLatencyWarnAfterMs?: number;
|
|
85
|
+
liveLatencyFailAfterMs?: number;
|
|
76
86
|
};
|
|
77
87
|
export declare const buildVoiceProductionReadinessReport: (options: VoiceProductionReadinessRoutesOptions, input?: {
|
|
78
88
|
query?: Record<string, unknown>;
|