@absolutejs/voice 0.0.22-beta.48 → 0.0.22-beta.49
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 +44 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1178 -1006
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5473,6 +5473,12 @@ var voice = (config) => {
|
|
|
5473
5473
|
}
|
|
5474
5474
|
}).use(htmxRoutes());
|
|
5475
5475
|
};
|
|
5476
|
+
// src/appKit.ts
|
|
5477
|
+
import { Elysia as Elysia11 } from "elysia";
|
|
5478
|
+
|
|
5479
|
+
// src/assistantHealth.ts
|
|
5480
|
+
import { Elysia as Elysia3 } from "elysia";
|
|
5481
|
+
|
|
5476
5482
|
// src/agent.ts
|
|
5477
5483
|
var normalizeText3 = (value) => typeof value === "string" ? value.trim() : "";
|
|
5478
5484
|
var toErrorMessage3 = (error) => error instanceof Error ? error.message : String(error);
|
|
@@ -6522,8 +6528,6 @@ var summarizeVoiceAssistantRuns = async (input) => {
|
|
|
6522
6528
|
totalRuns: assistantRuns.length
|
|
6523
6529
|
};
|
|
6524
6530
|
};
|
|
6525
|
-
// src/assistantHealth.ts
|
|
6526
|
-
import { Elysia as Elysia3 } from "elysia";
|
|
6527
6531
|
|
|
6528
6532
|
// src/providerHealth.ts
|
|
6529
6533
|
import { Elysia as Elysia2 } from "elysia";
|
|
@@ -6822,6 +6826,7 @@ var createVoiceAssistantHealthRoutes = (options) => {
|
|
|
6822
6826
|
}
|
|
6823
6827
|
return routes;
|
|
6824
6828
|
};
|
|
6829
|
+
|
|
6825
6830
|
// src/diagnosticsRoutes.ts
|
|
6826
6831
|
import { Elysia as Elysia4 } from "elysia";
|
|
6827
6832
|
|
|
@@ -7623,6 +7628,7 @@ var createVoiceDiagnosticsRoutes = (options) => {
|
|
|
7623
7628
|
});
|
|
7624
7629
|
return routes;
|
|
7625
7630
|
};
|
|
7631
|
+
|
|
7626
7632
|
// src/evalRoutes.ts
|
|
7627
7633
|
import { Elysia as Elysia7 } from "elysia";
|
|
7628
7634
|
import { mkdir } from "fs/promises";
|
|
@@ -8454,159 +8460,1002 @@ var createVoiceEvalRoutes = (options) => {
|
|
|
8454
8460
|
});
|
|
8455
8461
|
return routes;
|
|
8456
8462
|
};
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8463
|
+
|
|
8464
|
+
// src/opsConsoleRoutes.ts
|
|
8465
|
+
import { Elysia as Elysia10 } from "elysia";
|
|
8466
|
+
|
|
8467
|
+
// src/resilienceRoutes.ts
|
|
8468
|
+
import { Elysia as Elysia8 } from "elysia";
|
|
8469
|
+
var escapeHtml10 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
8470
|
+
var getString7 = (value) => typeof value === "string" ? value : undefined;
|
|
8471
|
+
var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
8472
|
+
var getBoolean2 = (value) => value === true;
|
|
8473
|
+
var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
|
|
8474
|
+
var listVoiceRoutingEvents = (events) => {
|
|
8475
|
+
const routingEvents = [];
|
|
8476
|
+
for (const event of events) {
|
|
8477
|
+
if (event.type !== "session.error") {
|
|
8478
|
+
continue;
|
|
8465
8479
|
}
|
|
8466
|
-
|
|
8480
|
+
const provider = getString7(event.payload.provider);
|
|
8481
|
+
const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
8482
|
+
if (!provider || !providerStatus) {
|
|
8483
|
+
continue;
|
|
8484
|
+
}
|
|
8485
|
+
const kind = getString7(event.payload.kind);
|
|
8486
|
+
routingEvents.push({
|
|
8487
|
+
at: event.at,
|
|
8488
|
+
attempt: getNumber4(event.payload.attempt),
|
|
8489
|
+
elapsedMs: getNumber4(event.payload.elapsedMs),
|
|
8490
|
+
error: getString7(event.payload.error),
|
|
8491
|
+
fallbackProvider: getString7(event.payload.fallbackProvider),
|
|
8492
|
+
kind: kind === "stt" || kind === "tts" ? kind : "llm",
|
|
8493
|
+
latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
|
|
8494
|
+
operation: getString7(event.payload.operation),
|
|
8495
|
+
provider,
|
|
8496
|
+
selectedProvider: getString7(event.payload.selectedProvider),
|
|
8497
|
+
sessionId: event.sessionId,
|
|
8498
|
+
status: providerStatus,
|
|
8499
|
+
timedOut: getBoolean2(event.payload.timedOut),
|
|
8500
|
+
turnId: event.turnId
|
|
8501
|
+
});
|
|
8467
8502
|
}
|
|
8468
|
-
return
|
|
8503
|
+
return routingEvents.sort((left, right) => right.at - left.at);
|
|
8469
8504
|
};
|
|
8470
|
-
var
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8505
|
+
var summarizeRoutingEvents = (events) => {
|
|
8506
|
+
const byKind = new Map;
|
|
8507
|
+
let errors = 0;
|
|
8508
|
+
let fallbacks = 0;
|
|
8509
|
+
let timeouts = 0;
|
|
8510
|
+
for (const event of events) {
|
|
8511
|
+
byKind.set(event.kind, (byKind.get(event.kind) ?? 0) + 1);
|
|
8512
|
+
if (event.status === "error") {
|
|
8513
|
+
errors += 1;
|
|
8514
|
+
}
|
|
8515
|
+
if (event.status === "fallback") {
|
|
8516
|
+
fallbacks += 1;
|
|
8517
|
+
}
|
|
8518
|
+
if (event.timedOut) {
|
|
8519
|
+
timeouts += 1;
|
|
8520
|
+
}
|
|
8483
8521
|
}
|
|
8522
|
+
return {
|
|
8523
|
+
byKind,
|
|
8524
|
+
errors,
|
|
8525
|
+
fallbacks,
|
|
8526
|
+
timeouts,
|
|
8527
|
+
total: events.length
|
|
8528
|
+
};
|
|
8484
8529
|
};
|
|
8485
|
-
var
|
|
8486
|
-
if (
|
|
8487
|
-
return "
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8530
|
+
var renderProviderCards = (title, providers) => {
|
|
8531
|
+
if (providers.length === 0) {
|
|
8532
|
+
return `<p class="muted">No ${escapeHtml10(title)} provider health yet.</p>`;
|
|
8533
|
+
}
|
|
8534
|
+
return `<div class="provider-grid">${providers.map((provider) => `
|
|
8535
|
+
<article class="card provider ${escapeHtml10(provider.status)}">
|
|
8536
|
+
<div class="card-header">
|
|
8537
|
+
<strong>${escapeHtml10(provider.provider)}</strong>
|
|
8538
|
+
<span>${escapeHtml10(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
|
|
8539
|
+
</div>
|
|
8540
|
+
<dl>
|
|
8541
|
+
<div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
|
|
8542
|
+
<div><dt>Avg latency</dt><dd>${provider.averageElapsedMs ?? 0}ms</dd></div>
|
|
8543
|
+
<div><dt>Errors</dt><dd>${provider.errorCount}</dd></div>
|
|
8544
|
+
<div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
|
|
8545
|
+
<div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
|
|
8546
|
+
</dl>
|
|
8547
|
+
${provider.lastError ? `<p class="muted">${escapeHtml10(provider.lastError)}</p>` : ""}
|
|
8548
|
+
</article>
|
|
8549
|
+
`).join("")}</div>`;
|
|
8497
8550
|
};
|
|
8498
|
-
var
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
const missingFields = [];
|
|
8502
|
-
const outcome = resolveOutcome2(routeResult);
|
|
8503
|
-
if (definition.outcome && outcome !== definition.outcome) {
|
|
8504
|
-
issues.push({
|
|
8505
|
-
code: "workflow.outcome_mismatch",
|
|
8506
|
-
message: `Expected workflow outcome ${definition.outcome}, saw ${outcome ?? "none"}.`
|
|
8507
|
-
});
|
|
8551
|
+
var renderTimeline2 = (events) => {
|
|
8552
|
+
if (events.length === 0) {
|
|
8553
|
+
return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
|
|
8508
8554
|
}
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8555
|
+
return `<div class="timeline">${events.slice(0, 40).map((event) => `
|
|
8556
|
+
<article class="card event ${escapeHtml10(event.status ?? "unknown")}">
|
|
8557
|
+
<div class="card-header">
|
|
8558
|
+
<strong>${escapeHtml10(event.kind.toUpperCase())} ${escapeHtml10(event.operation ?? "generate")}</strong>
|
|
8559
|
+
<span>${new Date(event.at).toLocaleString()}</span>
|
|
8560
|
+
</div>
|
|
8561
|
+
<p>
|
|
8562
|
+
<span class="pill">${escapeHtml10(event.status ?? "unknown")}</span>
|
|
8563
|
+
<span class="pill">provider: ${escapeHtml10(event.provider ?? "unknown")}</span>
|
|
8564
|
+
${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml10(event.fallbackProvider)}</span>` : ""}
|
|
8565
|
+
${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
|
|
8566
|
+
</p>
|
|
8567
|
+
<dl>
|
|
8568
|
+
<div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
|
|
8569
|
+
<div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
|
|
8570
|
+
<div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
|
|
8571
|
+
<div><dt>Session</dt><dd>${escapeHtml10(event.sessionId)}</dd></div>
|
|
8572
|
+
</dl>
|
|
8573
|
+
${event.error ? `<p class="muted">${escapeHtml10(event.error)}</p>` : ""}
|
|
8574
|
+
</article>
|
|
8575
|
+
`).join("")}</div>`;
|
|
8576
|
+
};
|
|
8577
|
+
var renderSimulationControls = (kind, simulation) => {
|
|
8578
|
+
if (!simulation) {
|
|
8579
|
+
return "";
|
|
8522
8580
|
}
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
}
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8581
|
+
const configuredProviders = simulation.providers.filter((provider) => provider.configured !== false);
|
|
8582
|
+
if (configuredProviders.length === 0) {
|
|
8583
|
+
return `<p class="muted">No ${kind.toUpperCase()} providers are configured for simulation.</p>`;
|
|
8584
|
+
}
|
|
8585
|
+
const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
|
|
8586
|
+
const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
|
|
8587
|
+
const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
|
|
8588
|
+
return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml10(pathPrefix)}">
|
|
8589
|
+
<p class="muted">${escapeHtml10(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
|
|
8590
|
+
<div class="simulate-actions">
|
|
8591
|
+
${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml10(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml10(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
|
|
8592
|
+
${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml10(provider.provider)}">Mark ${escapeHtml10(provider.provider)} recovered</button>`).join("")}
|
|
8593
|
+
</div>
|
|
8594
|
+
${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml10(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
|
|
8595
|
+
<pre class="simulate-output" hidden></pre>
|
|
8596
|
+
</div>`;
|
|
8535
8597
|
};
|
|
8536
|
-
var
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
}
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
}
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8598
|
+
var renderVoiceResilienceHTML = (input) => {
|
|
8599
|
+
const summary = summarizeRoutingEvents(input.routingEvents);
|
|
8600
|
+
const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml10(kind)}: ${String(count)}</span>`).join("");
|
|
8601
|
+
const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml10(link.href)}">${escapeHtml10(link.label)}</a>`).join(" \xB7 ") : "";
|
|
8602
|
+
return `<!doctype html>
|
|
8603
|
+
<html lang="en">
|
|
8604
|
+
<head>
|
|
8605
|
+
<meta charset="utf-8" />
|
|
8606
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8607
|
+
<title>${escapeHtml10(input.title ?? "AbsoluteJS Voice Resilience")}</title>
|
|
8608
|
+
<style>
|
|
8609
|
+
:root { color-scheme: dark; }
|
|
8610
|
+
body { background: radial-gradient(circle at top left, #172554, #09090b 36%, #050505); color: #f4f4f5; font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; padding: 24px; }
|
|
8611
|
+
main { display: grid; gap: 16px; margin: 0 auto; max-width: 1180px; }
|
|
8612
|
+
section, .card { background: rgba(19, 22, 27, 0.92); border: 1px solid #27272a; border-radius: 20px; padding: 20px; }
|
|
8613
|
+
.hero { background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(245, 158, 11, 0.12)); }
|
|
8614
|
+
.grid, .provider-grid { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
8615
|
+
.timeline { display: grid; gap: 12px; }
|
|
8616
|
+
.card-header { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
|
|
8617
|
+
.card-header strong { font-size: 1.05rem; }
|
|
8618
|
+
.metric strong { display: block; font-size: 2rem; margin-top: 6px; }
|
|
8619
|
+
.muted, dt, span { color: #a1a1aa; }
|
|
8620
|
+
dl { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
8621
|
+
dl div { background: #0f1217; border: 1px solid #27272a; border-radius: 12px; padding: 10px; }
|
|
8622
|
+
dd { font-weight: 800; margin: 4px 0 0; }
|
|
8623
|
+
.pill { background: #0f1217; border: 1px solid #3f3f46; border-radius: 999px; color: #d4d4d8; display: inline-flex; margin: 3px 4px 3px 0; padding: 5px 9px; }
|
|
8624
|
+
.danger { border-color: rgba(239, 68, 68, 0.75); color: #fecaca; }
|
|
8625
|
+
.event.error { border-color: rgba(239, 68, 68, 0.7); }
|
|
8626
|
+
.event.fallback { border-color: rgba(245, 158, 11, 0.7); }
|
|
8627
|
+
.event.success, .provider.healthy { border-color: rgba(34, 197, 94, 0.5); }
|
|
8628
|
+
.provider.suppressed, .provider.degraded, .provider.rate-limited { border-color: rgba(239, 68, 68, 0.7); }
|
|
8629
|
+
.provider.recoverable { border-color: rgba(59, 130, 246, 0.7); }
|
|
8630
|
+
button { background: #f59e0b; border: 0; border-radius: 999px; color: #111827; cursor: pointer; font-weight: 800; padding: 10px 14px; }
|
|
8631
|
+
button:disabled { cursor: not-allowed; opacity: 0.45; }
|
|
8632
|
+
.simulate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
|
|
8633
|
+
.simulate-output { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #d4d4d8; overflow: auto; padding: 12px; white-space: pre-wrap; }
|
|
8634
|
+
a { color: #f59e0b; }
|
|
8635
|
+
@media (max-width: 850px) { .grid, .provider-grid, dl { grid-template-columns: 1fr; } }
|
|
8636
|
+
</style>
|
|
8637
|
+
</head>
|
|
8638
|
+
<body>
|
|
8639
|
+
<main>
|
|
8640
|
+
<section class="hero">
|
|
8641
|
+
<h1>Provider routing and resilience</h1>
|
|
8642
|
+
<p>One view for the production reliability story: LLM failover, STT/TTS routing, latency budgets, timeouts, and fallback decisions.</p>
|
|
8643
|
+
${links ? `<p>${links}</p>` : ""}
|
|
8644
|
+
<p>${kindCounts || '<span class="pill">No routing events yet</span>'}</p>
|
|
8645
|
+
</section>
|
|
8646
|
+
<section class="grid">
|
|
8647
|
+
<article class="card metric"><span>Total routing events</span><strong>${summary.total}</strong></article>
|
|
8648
|
+
<article class="card metric"><span>Fallbacks</span><strong>${summary.fallbacks}</strong></article>
|
|
8649
|
+
<article class="card metric"><span>Errors</span><strong>${summary.errors}</strong></article>
|
|
8650
|
+
<article class="card metric"><span>Timeouts</span><strong>${summary.timeouts}</strong></article>
|
|
8651
|
+
</section>
|
|
8652
|
+
<section>
|
|
8653
|
+
<h2>LLM provider health</h2>
|
|
8654
|
+
${renderProviderCards("LLM", input.llmProviderHealth)}
|
|
8655
|
+
</section>
|
|
8656
|
+
<section>
|
|
8657
|
+
<h2>STT provider health</h2>
|
|
8658
|
+
${renderSimulationControls("stt", input.sttSimulation)}
|
|
8659
|
+
${renderProviderCards("STT", input.sttProviderHealth)}
|
|
8660
|
+
</section>
|
|
8661
|
+
<section>
|
|
8662
|
+
<h2>TTS provider health</h2>
|
|
8663
|
+
${renderSimulationControls("tts", input.ttsSimulation)}
|
|
8664
|
+
${renderProviderCards("TTS", input.ttsProviderHealth)}
|
|
8665
|
+
</section>
|
|
8666
|
+
<section>
|
|
8667
|
+
<h2>Routing timeline</h2>
|
|
8668
|
+
${renderTimeline2(input.routingEvents)}
|
|
8669
|
+
</section>
|
|
8670
|
+
</main>
|
|
8671
|
+
<script>
|
|
8672
|
+
const showResult = (panel, result) => {
|
|
8673
|
+
const output = panel.querySelector(".simulate-output");
|
|
8674
|
+
if (!output) return;
|
|
8675
|
+
output.hidden = false;
|
|
8676
|
+
output.textContent = JSON.stringify(result, null, 2);
|
|
8677
|
+
};
|
|
8678
|
+
document.querySelectorAll("[data-sim-prefix]").forEach((panel) => {
|
|
8679
|
+
const prefix = panel.getAttribute("data-sim-prefix");
|
|
8680
|
+
panel.querySelectorAll("[data-provider-fail]").forEach((button) => {
|
|
8681
|
+
button.addEventListener("click", async () => {
|
|
8682
|
+
const provider = button.getAttribute("data-provider-fail");
|
|
8683
|
+
const response = await fetch(prefix + "/failure?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
|
|
8684
|
+
showResult(panel, await response.json());
|
|
8685
|
+
if (response.ok) window.setTimeout(() => window.location.reload(), 450);
|
|
8686
|
+
});
|
|
8687
|
+
});
|
|
8688
|
+
panel.querySelectorAll("[data-provider-recover]").forEach((button) => {
|
|
8689
|
+
button.addEventListener("click", async () => {
|
|
8690
|
+
const provider = button.getAttribute("data-provider-recover");
|
|
8691
|
+
const response = await fetch(prefix + "/recovery?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
|
|
8692
|
+
showResult(panel, await response.json());
|
|
8693
|
+
if (response.ok) window.setTimeout(() => window.location.reload(), 450);
|
|
8694
|
+
});
|
|
8695
|
+
});
|
|
8696
|
+
});
|
|
8697
|
+
</script>
|
|
8698
|
+
</body>
|
|
8699
|
+
</html>`;
|
|
8700
|
+
};
|
|
8701
|
+
var providerFromQuery = (value, providers) => typeof value === "string" && providers.some((provider) => provider.provider === value && provider.configured !== false) ? value : undefined;
|
|
8702
|
+
var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
|
|
8703
|
+
if (!simulation) {
|
|
8704
|
+
return routes;
|
|
8705
|
+
}
|
|
8706
|
+
const pathPrefix = simulation.pathPrefix ?? defaultPathPrefix;
|
|
8707
|
+
routes.post(`${pathPrefix}/failure`, async ({ query, set }) => {
|
|
8708
|
+
const provider = providerFromQuery(query.provider, simulation.providers);
|
|
8709
|
+
if (!provider) {
|
|
8710
|
+
set.status = 400;
|
|
8711
|
+
return {
|
|
8712
|
+
error: "Provider is not configured for simulation."
|
|
8713
|
+
};
|
|
8714
|
+
}
|
|
8715
|
+
if (simulation.failureProviders && !simulation.failureProviders.includes(provider)) {
|
|
8716
|
+
set.status = 400;
|
|
8717
|
+
return {
|
|
8718
|
+
error: `${provider} is not configured for failure simulation.`
|
|
8719
|
+
};
|
|
8720
|
+
}
|
|
8721
|
+
if (simulation.fallbackRequiredProvider && !simulation.providers.some((entry) => entry.provider === simulation.fallbackRequiredProvider && entry.configured !== false)) {
|
|
8722
|
+
set.status = 400;
|
|
8723
|
+
return {
|
|
8724
|
+
error: simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} before simulating fallback.`
|
|
8725
|
+
};
|
|
8726
|
+
}
|
|
8727
|
+
return simulation.run(provider, "failure");
|
|
8728
|
+
});
|
|
8729
|
+
routes.post(`${pathPrefix}/recovery`, async ({ query, set }) => {
|
|
8730
|
+
const provider = providerFromQuery(query.provider, simulation.providers);
|
|
8731
|
+
if (!provider) {
|
|
8732
|
+
set.status = 400;
|
|
8733
|
+
return {
|
|
8734
|
+
error: "Provider is not configured for simulation."
|
|
8735
|
+
};
|
|
8736
|
+
}
|
|
8737
|
+
return simulation.run(provider, "recovery");
|
|
8738
|
+
});
|
|
8739
|
+
return routes;
|
|
8740
|
+
};
|
|
8741
|
+
var createVoiceResilienceRoutes = (options) => {
|
|
8742
|
+
const path = options.path ?? "/resilience";
|
|
8743
|
+
const routes = new Elysia8({
|
|
8744
|
+
name: options.name ?? "absolutejs-voice-resilience"
|
|
8745
|
+
}).get(path, async () => {
|
|
8746
|
+
const events = await options.store.list();
|
|
8747
|
+
const sttEvents = events.filter((event) => event.payload.kind === "stt");
|
|
8748
|
+
const ttsEvents = events.filter((event) => event.payload.kind === "tts");
|
|
8749
|
+
const data = {
|
|
8750
|
+
links: options.links,
|
|
8751
|
+
llmProviderHealth: await summarizeVoiceProviderHealth({
|
|
8752
|
+
events,
|
|
8753
|
+
providers: options.llmProviders ?? []
|
|
8754
|
+
}),
|
|
8755
|
+
routingEvents: listVoiceRoutingEvents(events),
|
|
8756
|
+
sttProviderHealth: await summarizeVoiceProviderHealth({
|
|
8757
|
+
events: sttEvents,
|
|
8758
|
+
providers: options.sttProviders ?? []
|
|
8759
|
+
}),
|
|
8760
|
+
sttSimulation: options.sttSimulation,
|
|
8761
|
+
title: options.title,
|
|
8762
|
+
ttsProviderHealth: await summarizeVoiceProviderHealth({
|
|
8763
|
+
events: ttsEvents,
|
|
8764
|
+
providers: options.ttsProviders ?? []
|
|
8765
|
+
}),
|
|
8766
|
+
ttsSimulation: options.ttsSimulation
|
|
8767
|
+
};
|
|
8768
|
+
const body = await (options.render ?? renderVoiceResilienceHTML)(data);
|
|
8769
|
+
return new Response(body, {
|
|
8770
|
+
headers: {
|
|
8771
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
8772
|
+
...options.headers
|
|
8773
|
+
}
|
|
8774
|
+
});
|
|
8775
|
+
});
|
|
8776
|
+
registerSimulationRoutes(routes, options.sttSimulation, "/api/stt-simulate");
|
|
8777
|
+
registerSimulationRoutes(routes, options.ttsSimulation, "/api/tts-simulate");
|
|
8778
|
+
return routes;
|
|
8779
|
+
};
|
|
8780
|
+
|
|
8781
|
+
// src/sessionReplay.ts
|
|
8782
|
+
import { Elysia as Elysia9 } from "elysia";
|
|
8783
|
+
var getString8 = (value) => typeof value === "string" ? value : undefined;
|
|
8784
|
+
var escapeHtml11 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
8785
|
+
var increment3 = (record, key) => {
|
|
8786
|
+
record[key] = (record[key] ?? 0) + 1;
|
|
8787
|
+
};
|
|
8788
|
+
var buildReplayTurns = (events) => {
|
|
8789
|
+
const turns = new Map;
|
|
8790
|
+
const getTurn = (turnId) => {
|
|
8791
|
+
const existing = turns.get(turnId);
|
|
8792
|
+
if (existing) {
|
|
8793
|
+
return existing;
|
|
8794
|
+
}
|
|
8795
|
+
const turn = {
|
|
8796
|
+
assistantReplies: [],
|
|
8797
|
+
errors: [],
|
|
8798
|
+
id: turnId,
|
|
8799
|
+
modelCalls: [],
|
|
8800
|
+
tools: [],
|
|
8801
|
+
transcripts: []
|
|
8802
|
+
};
|
|
8803
|
+
turns.set(turnId, turn);
|
|
8804
|
+
return turn;
|
|
8805
|
+
};
|
|
8806
|
+
for (const event of events) {
|
|
8807
|
+
const turnId = event.turnId ?? "session";
|
|
8808
|
+
const turn = getTurn(turnId);
|
|
8809
|
+
switch (event.type) {
|
|
8810
|
+
case "turn.transcript":
|
|
8811
|
+
turn.transcripts.push({
|
|
8812
|
+
isFinal: event.payload.isFinal === true,
|
|
8813
|
+
text: getString8(event.payload.text)
|
|
8814
|
+
});
|
|
8815
|
+
break;
|
|
8816
|
+
case "turn.committed":
|
|
8817
|
+
turn.committedText = getString8(event.payload.text);
|
|
8818
|
+
break;
|
|
8819
|
+
case "turn.assistant": {
|
|
8820
|
+
const text = getString8(event.payload.text);
|
|
8821
|
+
if (text) {
|
|
8822
|
+
turn.assistantReplies.push(text);
|
|
8823
|
+
}
|
|
8824
|
+
break;
|
|
8825
|
+
}
|
|
8826
|
+
case "agent.model":
|
|
8827
|
+
case "assistant.run":
|
|
8828
|
+
turn.modelCalls.push(event.payload);
|
|
8829
|
+
break;
|
|
8830
|
+
case "agent.tool":
|
|
8831
|
+
turn.tools.push(event.payload);
|
|
8832
|
+
break;
|
|
8833
|
+
case "session.error":
|
|
8834
|
+
turn.errors.push(event.payload);
|
|
8835
|
+
break;
|
|
8836
|
+
}
|
|
8837
|
+
}
|
|
8838
|
+
return [...turns.values()];
|
|
8839
|
+
};
|
|
8840
|
+
var summarizeVoiceSessionReplay = async (options) => {
|
|
8841
|
+
const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
|
|
8842
|
+
const events = filterVoiceTraceEvents(sourceEvents, {
|
|
8843
|
+
sessionId: options.sessionId
|
|
8844
|
+
});
|
|
8845
|
+
const replay = buildVoiceTraceReplay(events, {
|
|
8846
|
+
evaluation: options.evaluation,
|
|
8847
|
+
redact: options.redact,
|
|
8848
|
+
title: options.title ?? `Voice Session ${options.sessionId}`
|
|
8849
|
+
});
|
|
8850
|
+
const startedAt = replay.summary.startedAt;
|
|
8851
|
+
return {
|
|
8852
|
+
evaluation: replay.evaluation,
|
|
8853
|
+
events,
|
|
8854
|
+
html: replay.html,
|
|
8855
|
+
markdown: replay.markdown,
|
|
8856
|
+
sessionId: options.sessionId,
|
|
8857
|
+
summary: replay.summary,
|
|
8858
|
+
timeline: events.map((event) => ({
|
|
8859
|
+
at: event.at,
|
|
8860
|
+
offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
|
|
8861
|
+
payload: event.payload,
|
|
8862
|
+
turnId: event.turnId,
|
|
8863
|
+
type: event.type
|
|
8864
|
+
})),
|
|
8865
|
+
turns: buildReplayTurns(events)
|
|
8866
|
+
};
|
|
8867
|
+
};
|
|
8868
|
+
var summarizeVoiceSessions = async (options = {}) => {
|
|
8869
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
8870
|
+
const grouped = new Map;
|
|
8871
|
+
for (const event of events) {
|
|
8872
|
+
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
8873
|
+
}
|
|
8874
|
+
const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
|
|
8875
|
+
const sorted = filterVoiceTraceEvents(sessionEvents);
|
|
8876
|
+
const summary = buildVoiceTraceReplay(sorted, {
|
|
8877
|
+
evaluation: {
|
|
8878
|
+
requireAssistantReply: false,
|
|
8879
|
+
requireCompletedCall: false,
|
|
8880
|
+
requireTranscript: false,
|
|
8881
|
+
requireTurn: false
|
|
8882
|
+
}
|
|
8883
|
+
}).summary;
|
|
8884
|
+
const providerErrors = {};
|
|
8885
|
+
const providers = new Set;
|
|
8886
|
+
let latestOutcome;
|
|
8887
|
+
let errorCount = 0;
|
|
8888
|
+
for (const event of sorted) {
|
|
8889
|
+
const provider = getString8(event.payload.provider);
|
|
8890
|
+
if (provider) {
|
|
8891
|
+
providers.add(provider);
|
|
8892
|
+
}
|
|
8893
|
+
if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
|
|
8894
|
+
errorCount += 1;
|
|
8895
|
+
increment3(providerErrors, provider ?? "unknown");
|
|
8896
|
+
}
|
|
8897
|
+
const outcome = getString8(event.payload.outcome);
|
|
8898
|
+
if (outcome) {
|
|
8899
|
+
latestOutcome = outcome;
|
|
8900
|
+
}
|
|
8901
|
+
}
|
|
8902
|
+
const item = {
|
|
8903
|
+
endedAt: summary.endedAt,
|
|
8904
|
+
errorCount,
|
|
8905
|
+
eventCount: summary.eventCount,
|
|
8906
|
+
latestOutcome,
|
|
8907
|
+
providerErrors,
|
|
8908
|
+
providers: [...providers].sort(),
|
|
8909
|
+
sessionId,
|
|
8910
|
+
startedAt: summary.startedAt,
|
|
8911
|
+
status: errorCount > 0 ? "failed" : "healthy",
|
|
8912
|
+
transcriptCount: summary.transcriptCount,
|
|
8913
|
+
turnCount: summary.turnCount
|
|
8914
|
+
};
|
|
8915
|
+
const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
|
|
8916
|
+
return {
|
|
8917
|
+
...item,
|
|
8918
|
+
replayHref
|
|
8919
|
+
};
|
|
8920
|
+
});
|
|
8921
|
+
const search = options.q?.trim().toLowerCase();
|
|
8922
|
+
return sessions.filter((session) => {
|
|
8923
|
+
if (options.status && options.status !== "all" && session.status !== options.status) {
|
|
8924
|
+
return false;
|
|
8925
|
+
}
|
|
8926
|
+
if (options.provider && !session.providers.includes(options.provider)) {
|
|
8927
|
+
return false;
|
|
8928
|
+
}
|
|
8929
|
+
if (!search) {
|
|
8930
|
+
return true;
|
|
8931
|
+
}
|
|
8932
|
+
return [
|
|
8933
|
+
session.sessionId,
|
|
8934
|
+
session.latestOutcome,
|
|
8935
|
+
session.status,
|
|
8936
|
+
...session.providers
|
|
8937
|
+
].some((value) => value?.toLowerCase().includes(search));
|
|
8938
|
+
}).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
|
|
8939
|
+
};
|
|
8940
|
+
var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
|
|
8941
|
+
'<div class="voice-sessions-list">',
|
|
8942
|
+
...sessions.map((session) => [
|
|
8943
|
+
`<article class="voice-session-card ${escapeHtml11(session.status)}">`,
|
|
8944
|
+
'<div class="voice-session-card-header">',
|
|
8945
|
+
`<strong>${escapeHtml11(session.sessionId)}</strong>`,
|
|
8946
|
+
`<span>${escapeHtml11(session.status)}</span>`,
|
|
8947
|
+
"</div>",
|
|
8948
|
+
"<dl>",
|
|
8949
|
+
`<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
|
|
8950
|
+
`<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
|
|
8951
|
+
`<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
|
|
8952
|
+
`<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
|
|
8953
|
+
"</dl>",
|
|
8954
|
+
session.latestOutcome ? `<p>Outcome: ${escapeHtml11(session.latestOutcome)}</p>` : "",
|
|
8955
|
+
session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml11).join(", ")}</p>` : "",
|
|
8956
|
+
session.replayHref ? `<p><a href="${escapeHtml11(session.replayHref)}">Open replay</a></p>` : "",
|
|
8957
|
+
"</article>"
|
|
8958
|
+
].join("")),
|
|
8959
|
+
"</div>"
|
|
8960
|
+
].join("");
|
|
8961
|
+
var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
|
|
8962
|
+
...options,
|
|
8963
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
8964
|
+
provider: query?.provider ?? options.provider,
|
|
8965
|
+
q: query?.q ?? options.q,
|
|
8966
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
8967
|
+
});
|
|
8968
|
+
var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
|
|
8969
|
+
const sessions = await summarizeVoiceSessions({
|
|
8970
|
+
...options,
|
|
8971
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
8972
|
+
provider: query?.provider ?? options.provider,
|
|
8973
|
+
q: query?.q ?? options.q,
|
|
8974
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
8975
|
+
});
|
|
8976
|
+
const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
|
|
8977
|
+
return new Response(body, {
|
|
8978
|
+
headers: {
|
|
8979
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
8980
|
+
...options.headers
|
|
8981
|
+
}
|
|
8982
|
+
});
|
|
8983
|
+
};
|
|
8984
|
+
var createVoiceSessionListRoutes = (options = {}) => {
|
|
8985
|
+
const path = options.path ?? "/api/voice-sessions";
|
|
8986
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
8987
|
+
const routes = new Elysia9({
|
|
8988
|
+
name: options.name ?? "absolutejs-voice-session-list"
|
|
8989
|
+
}).get(path, createVoiceSessionsJSONHandler(options));
|
|
8990
|
+
if (htmlPath) {
|
|
8991
|
+
routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
|
|
8992
|
+
}
|
|
8993
|
+
return routes;
|
|
8994
|
+
};
|
|
8995
|
+
var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
|
|
8996
|
+
...options,
|
|
8997
|
+
sessionId: params.sessionId ?? ""
|
|
8998
|
+
});
|
|
8999
|
+
var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
|
|
9000
|
+
const replay = await summarizeVoiceSessionReplay({
|
|
9001
|
+
...options,
|
|
9002
|
+
sessionId: params.sessionId ?? ""
|
|
9003
|
+
});
|
|
9004
|
+
const body = await (options.render?.(replay) ?? replay.html);
|
|
9005
|
+
return new Response(body, {
|
|
9006
|
+
headers: {
|
|
9007
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9008
|
+
...options.headers
|
|
9009
|
+
}
|
|
9010
|
+
});
|
|
9011
|
+
};
|
|
9012
|
+
var createVoiceSessionReplayRoutes = (options) => {
|
|
9013
|
+
const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
|
|
9014
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
9015
|
+
const routes = new Elysia9({
|
|
9016
|
+
name: options.name ?? "absolutejs-voice-session-replay"
|
|
9017
|
+
}).get(path, createVoiceSessionReplayJSONHandler(options));
|
|
9018
|
+
if (htmlPath) {
|
|
9019
|
+
routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
|
|
9020
|
+
}
|
|
9021
|
+
return routes;
|
|
9022
|
+
};
|
|
9023
|
+
|
|
9024
|
+
// src/opsConsoleRoutes.ts
|
|
9025
|
+
var DEFAULT_LINKS = [
|
|
9026
|
+
{
|
|
9027
|
+
description: "Quality gates for CI, deploy checks, and production readiness.",
|
|
9028
|
+
href: "/quality",
|
|
9029
|
+
label: "Quality",
|
|
9030
|
+
statusHref: "/quality/status"
|
|
9031
|
+
},
|
|
9032
|
+
{
|
|
9033
|
+
description: "Replay stored sessions against acceptance gates over time.",
|
|
9034
|
+
href: "/evals",
|
|
9035
|
+
label: "Evals",
|
|
9036
|
+
statusHref: "/evals/status"
|
|
9037
|
+
},
|
|
9038
|
+
{
|
|
9039
|
+
description: "Provider health, fallback paths, and failure simulation.",
|
|
9040
|
+
href: "/resilience",
|
|
9041
|
+
label: "Resilience"
|
|
9042
|
+
},
|
|
9043
|
+
{
|
|
9044
|
+
description: "Redacted trace exports for debugging and support handoffs.",
|
|
9045
|
+
href: "/diagnostics",
|
|
9046
|
+
label: "Diagnostics"
|
|
9047
|
+
},
|
|
9048
|
+
{
|
|
9049
|
+
description: "Recent sessions with replay links.",
|
|
9050
|
+
href: "/sessions",
|
|
9051
|
+
label: "Sessions"
|
|
9052
|
+
},
|
|
9053
|
+
{
|
|
9054
|
+
description: "Transfer and webhook delivery health.",
|
|
9055
|
+
href: "/handoffs",
|
|
9056
|
+
label: "Handoffs"
|
|
9057
|
+
}
|
|
9058
|
+
];
|
|
9059
|
+
var escapeHtml12 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9060
|
+
var countProviderStatuses = (providers) => {
|
|
9061
|
+
const degradedStatuses = new Set(["degraded", "rate-limited", "suppressed"]);
|
|
9062
|
+
const healthy = providers.filter((provider) => provider.status === "healthy").length;
|
|
9063
|
+
const degraded = providers.filter((provider) => degradedStatuses.has(provider.status)).length;
|
|
9064
|
+
return {
|
|
9065
|
+
degraded,
|
|
9066
|
+
healthy,
|
|
9067
|
+
total: providers.length
|
|
9068
|
+
};
|
|
9069
|
+
};
|
|
9070
|
+
var buildVoiceOpsConsoleReport = async (options) => {
|
|
9071
|
+
const events = await options.store.list();
|
|
9072
|
+
const providers = [
|
|
9073
|
+
...await summarizeVoiceProviderHealth({
|
|
9074
|
+
events,
|
|
9075
|
+
providers: options.llmProviders
|
|
9076
|
+
}),
|
|
9077
|
+
...await summarizeVoiceProviderHealth({
|
|
9078
|
+
events,
|
|
9079
|
+
providers: options.sttProviders
|
|
9080
|
+
}),
|
|
9081
|
+
...await summarizeVoiceProviderHealth({
|
|
9082
|
+
events,
|
|
9083
|
+
providers: options.ttsProviders
|
|
9084
|
+
})
|
|
9085
|
+
];
|
|
9086
|
+
const handoffs = await summarizeVoiceHandoffHealth({ events });
|
|
9087
|
+
const sessions = await summarizeVoiceSessions({
|
|
9088
|
+
events,
|
|
9089
|
+
limit: 8,
|
|
9090
|
+
status: "all"
|
|
9091
|
+
});
|
|
9092
|
+
const quality = await evaluateVoiceQuality({ events });
|
|
9093
|
+
const routingEvents = listVoiceRoutingEvents(events).slice(0, 10);
|
|
9094
|
+
const trace = summarizeVoiceTrace(events);
|
|
9095
|
+
return {
|
|
9096
|
+
checkedAt: Date.now(),
|
|
9097
|
+
eventCount: events.length,
|
|
9098
|
+
handoffs: {
|
|
9099
|
+
failed: handoffs.failed,
|
|
9100
|
+
total: handoffs.total
|
|
9101
|
+
},
|
|
9102
|
+
links: options.links ?? DEFAULT_LINKS,
|
|
9103
|
+
providers: countProviderStatuses(providers),
|
|
9104
|
+
quality,
|
|
9105
|
+
recentRoutingEvents: routingEvents,
|
|
9106
|
+
recentSessions: sessions,
|
|
9107
|
+
sessions: {
|
|
9108
|
+
failed: sessions.filter((session) => session.status === "failed").length,
|
|
9109
|
+
healthy: sessions.filter((session) => session.status === "healthy").length,
|
|
9110
|
+
total: sessions.length
|
|
9111
|
+
},
|
|
9112
|
+
trace
|
|
9113
|
+
};
|
|
9114
|
+
};
|
|
9115
|
+
var renderMetricCard = (input) => `<article class="metric"><span>${escapeHtml12(input.label)}</span><strong>${escapeHtml12(String(input.value))}</strong>${input.status ? `<p class="${escapeHtml12(input.status)}">${escapeHtml12(input.status)}</p>` : ""}${input.href ? `<a href="${escapeHtml12(input.href)}">Open</a>` : ""}</article>`;
|
|
9116
|
+
var renderVoiceOpsConsoleHTML = (report, options = {}) => {
|
|
9117
|
+
const links = report.links.map((link) => `<article class="surface">
|
|
9118
|
+
<div><h2>${escapeHtml12(link.label)}</h2>${link.description ? `<p>${escapeHtml12(link.description)}</p>` : ""}</div>
|
|
9119
|
+
<p><a href="${escapeHtml12(link.href)}">Open ${escapeHtml12(link.label)}</a>${link.statusHref ? ` \xB7 <a href="${escapeHtml12(link.statusHref)}">Status</a>` : ""}</p>
|
|
9120
|
+
</article>`).join("");
|
|
9121
|
+
const sessions = report.recentSessions.length ? report.recentSessions.map((session) => `<tr><td>${escapeHtml12(session.sessionId)}</td><td>${escapeHtml12(session.status)}</td><td>${session.turnCount}</td><td>${session.errorCount}</td><td>${session.replayHref ? `<a href="${escapeHtml12(session.replayHref)}">Replay</a>` : ""}</td></tr>`).join("") : '<tr><td colspan="5">No sessions yet.</td></tr>';
|
|
9122
|
+
const routing = report.recentRoutingEvents.length ? report.recentRoutingEvents.map((event) => `<tr><td>${escapeHtml12(event.kind)}</td><td>${escapeHtml12(event.provider ?? "unknown")}</td><td>${escapeHtml12(event.status ?? "unknown")}</td><td>${event.elapsedMs ?? 0}ms</td><td>${escapeHtml12(event.sessionId)}</td></tr>`).join("") : '<tr><td colspan="5">No provider routing events yet.</td></tr>';
|
|
9123
|
+
const title = options.title ?? "AbsoluteJS Voice Ops Console";
|
|
9124
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml12(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;background:#101316;color:#f6f2e8;margin:0}main{max-width:1180px;margin:auto;padding:32px}a{color:#fbbf24}header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}.eyebrow{color:#fbbf24;font-weight:800;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.2rem,5vw,4.5rem);line-height:.95;margin:.2rem 0 1rem}.muted{color:#a8b0b8}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.metric,.surface{background:#181d22;border:1px solid #2a323a;border-radius:20px;padding:18px}.metric strong{display:block;font-size:2.2rem;margin:.25rem 0}.pass,.healthy{color:#86efac}.fail,.failed,.degraded{color:#fca5a5}.surfaces{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin:24px 0}table{width:100%;border-collapse:collapse;background:#181d22;border-radius:16px;overflow:hidden;margin:12px 0 28px}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left}section{margin-top:30px}@media(max-width:700px){main{padding:20px}header{display:block}}</style></head><body><main><header><div><p class="eyebrow">Self-hosted voice operations</p><h1>${escapeHtml12(title)}</h1><p class="muted">One deployable control plane for quality gates, failover, traces, sessions, handoffs, and provider health.</p></div><p class="muted">Checked ${escapeHtml12(new Date(report.checkedAt).toLocaleString())}</p></header><div class="grid">${renderMetricCard({ label: "Quality", value: report.quality.status, status: report.quality.status, href: "/quality" })}${renderMetricCard({ label: "Events", value: report.eventCount, href: "/diagnostics" })}${renderMetricCard({ label: "Sessions", value: report.sessions.total, status: report.sessions.failed > 0 ? "failed" : "healthy", href: "/sessions" })}${renderMetricCard({ label: "Handoffs failed", value: report.handoffs.failed, status: report.handoffs.failed > 0 ? "failed" : "healthy", href: "/handoffs" })}${renderMetricCard({ label: "Providers degraded", value: report.providers.degraded, status: report.providers.degraded > 0 ? "degraded" : "healthy", href: "/resilience" })}</div><section><h2>Operational Surfaces</h2><div class="surfaces">${links}</div></section><section><h2>Recent Sessions</h2><table><thead><tr><th>Session</th><th>Status</th><th>Turns</th><th>Errors</th><th>Replay</th></tr></thead><tbody>${sessions}</tbody></table></section><section><h2>Recent Provider Routing</h2><table><thead><tr><th>Kind</th><th>Provider</th><th>Status</th><th>Elapsed</th><th>Session</th></tr></thead><tbody>${routing}</tbody></table></section></main></body></html>`;
|
|
9125
|
+
};
|
|
9126
|
+
var createVoiceOpsConsoleRoutes = (options) => {
|
|
9127
|
+
const path = options.path ?? "/ops-console";
|
|
9128
|
+
const routes = new Elysia10({
|
|
9129
|
+
name: options.name ?? "absolutejs-voice-ops-console"
|
|
9130
|
+
});
|
|
9131
|
+
const getReport = () => buildVoiceOpsConsoleReport(options);
|
|
9132
|
+
routes.get(path, async () => {
|
|
9133
|
+
const report = await getReport();
|
|
9134
|
+
return new Response(renderVoiceOpsConsoleHTML(report, { title: options.title }), {
|
|
9135
|
+
headers: {
|
|
9136
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9137
|
+
...options.headers
|
|
9138
|
+
}
|
|
9139
|
+
});
|
|
9140
|
+
});
|
|
9141
|
+
routes.get(`${path}/json`, async () => getReport());
|
|
9142
|
+
return routes;
|
|
9143
|
+
};
|
|
9144
|
+
|
|
9145
|
+
// src/appKit.ts
|
|
9146
|
+
var DEFAULT_LINKS2 = [
|
|
9147
|
+
{
|
|
9148
|
+
description: "Integrated voice operations console.",
|
|
9149
|
+
href: "/ops-console",
|
|
9150
|
+
label: "Ops Console"
|
|
9151
|
+
},
|
|
9152
|
+
{
|
|
9153
|
+
description: "Production quality gates.",
|
|
9154
|
+
href: "/quality",
|
|
9155
|
+
label: "Quality",
|
|
9156
|
+
statusHref: "/quality/status"
|
|
9157
|
+
},
|
|
9158
|
+
{
|
|
9159
|
+
description: "Replay sessions against evals and workflow contracts.",
|
|
9160
|
+
href: "/evals",
|
|
9161
|
+
label: "Evals",
|
|
9162
|
+
statusHref: "/evals/status"
|
|
9163
|
+
},
|
|
9164
|
+
{
|
|
9165
|
+
description: "Provider routing, fallback, and resilience controls.",
|
|
9166
|
+
href: "/resilience",
|
|
9167
|
+
label: "Resilience"
|
|
9168
|
+
},
|
|
9169
|
+
{
|
|
9170
|
+
description: "Recent sessions and replay links.",
|
|
9171
|
+
href: "/sessions",
|
|
9172
|
+
label: "Sessions"
|
|
9173
|
+
},
|
|
9174
|
+
{
|
|
9175
|
+
description: "Handoff delivery health.",
|
|
9176
|
+
href: "/handoffs",
|
|
9177
|
+
label: "Handoffs"
|
|
9178
|
+
},
|
|
9179
|
+
{
|
|
9180
|
+
description: "Redacted traces and bug-report exports.",
|
|
9181
|
+
href: "/diagnostics",
|
|
9182
|
+
label: "Diagnostics"
|
|
9183
|
+
}
|
|
9184
|
+
];
|
|
9185
|
+
var resolveLinks = (links) => links ?? DEFAULT_LINKS2;
|
|
9186
|
+
var toBasicLinks = (links) => links.map(({ href, label }) => ({ href, label }));
|
|
9187
|
+
var toOpsLinks = (links) => links.map((link) => ({
|
|
9188
|
+
description: link.description ?? link.label,
|
|
9189
|
+
href: link.href,
|
|
9190
|
+
label: link.label,
|
|
9191
|
+
statusHref: link.statusHref
|
|
9192
|
+
}));
|
|
9193
|
+
var toResilienceLinks = (links) => links.map(({ href, label }) => ({ href, label }));
|
|
9194
|
+
var createVoiceAppKitRoutes = (options) => {
|
|
9195
|
+
const routes = new Elysia11({
|
|
9196
|
+
name: options.name ?? "absolutejs-voice-app-kit"
|
|
9197
|
+
});
|
|
9198
|
+
const links = resolveLinks(options.links);
|
|
9199
|
+
const common = {
|
|
9200
|
+
headers: options.headers,
|
|
9201
|
+
store: options.store
|
|
9202
|
+
};
|
|
9203
|
+
const surfaces = [];
|
|
9204
|
+
if (options.providerHealth !== false) {
|
|
9205
|
+
surfaces.push("providerHealth");
|
|
9206
|
+
routes.use(createVoiceProviderHealthRoutes({
|
|
9207
|
+
...common,
|
|
9208
|
+
providers: options.llmProviders,
|
|
9209
|
+
...options.providerHealth
|
|
9210
|
+
}));
|
|
9211
|
+
}
|
|
9212
|
+
if (options.assistantHealth !== false) {
|
|
9213
|
+
surfaces.push("assistantHealth");
|
|
9214
|
+
routes.use(createVoiceAssistantHealthRoutes({
|
|
9215
|
+
...common,
|
|
9216
|
+
providers: options.llmProviders,
|
|
9217
|
+
...options.assistantHealth
|
|
9218
|
+
}));
|
|
9219
|
+
}
|
|
9220
|
+
if (options.quality !== false) {
|
|
9221
|
+
surfaces.push("quality");
|
|
9222
|
+
routes.use(createVoiceQualityRoutes({
|
|
9223
|
+
...common,
|
|
9224
|
+
links: toBasicLinks(links),
|
|
9225
|
+
...options.quality
|
|
9226
|
+
}));
|
|
9227
|
+
}
|
|
9228
|
+
if (options.evals !== false) {
|
|
9229
|
+
surfaces.push("evals");
|
|
9230
|
+
routes.use(createVoiceEvalRoutes({
|
|
9231
|
+
...common,
|
|
9232
|
+
links: toBasicLinks(links),
|
|
9233
|
+
title: options.title ? `${options.title} Evals` : undefined,
|
|
9234
|
+
...options.evals
|
|
9235
|
+
}));
|
|
9236
|
+
}
|
|
9237
|
+
if (options.sessions !== false) {
|
|
9238
|
+
surfaces.push("sessions");
|
|
9239
|
+
routes.use(createVoiceSessionListRoutes({
|
|
9240
|
+
...common,
|
|
9241
|
+
htmlPath: "/sessions",
|
|
9242
|
+
path: "/api/voice-sessions",
|
|
9243
|
+
replayHref: "/sessions/:sessionId",
|
|
9244
|
+
...options.sessions
|
|
9245
|
+
}));
|
|
9246
|
+
}
|
|
9247
|
+
if (options.sessionReplay !== false) {
|
|
9248
|
+
surfaces.push("sessionReplay");
|
|
9249
|
+
routes.use(createVoiceSessionReplayRoutes({
|
|
9250
|
+
...common,
|
|
9251
|
+
htmlPath: "/sessions/:sessionId",
|
|
9252
|
+
path: "/api/voice-sessions/:sessionId/replay",
|
|
9253
|
+
...options.sessionReplay
|
|
9254
|
+
}));
|
|
9255
|
+
}
|
|
9256
|
+
if (options.handoffs !== false) {
|
|
9257
|
+
surfaces.push("handoffs");
|
|
9258
|
+
routes.use(createVoiceHandoffHealthRoutes({
|
|
9259
|
+
...common,
|
|
9260
|
+
htmlPath: "/handoffs",
|
|
9261
|
+
path: "/api/voice-handoffs",
|
|
9262
|
+
...options.handoffs
|
|
9263
|
+
}));
|
|
9264
|
+
}
|
|
9265
|
+
if (options.diagnostics !== false) {
|
|
9266
|
+
surfaces.push("diagnostics");
|
|
9267
|
+
routes.use(createVoiceDiagnosticsRoutes({
|
|
9268
|
+
...common,
|
|
9269
|
+
path: "/diagnostics",
|
|
9270
|
+
title: options.title ? `${options.title} Diagnostics` : undefined,
|
|
9271
|
+
...options.diagnostics
|
|
9272
|
+
}));
|
|
9273
|
+
}
|
|
9274
|
+
if (options.resilience !== false) {
|
|
9275
|
+
surfaces.push("resilience");
|
|
9276
|
+
routes.use(createVoiceResilienceRoutes({
|
|
9277
|
+
...common,
|
|
9278
|
+
links: toResilienceLinks(links),
|
|
9279
|
+
llmProviders: options.llmProviders,
|
|
9280
|
+
sttProviders: options.sttProviders,
|
|
9281
|
+
title: options.title ? `${options.title} Resilience` : undefined,
|
|
9282
|
+
ttsProviders: options.ttsProviders,
|
|
9283
|
+
...options.resilience
|
|
9284
|
+
}));
|
|
9285
|
+
}
|
|
9286
|
+
if (options.opsConsole !== false) {
|
|
9287
|
+
surfaces.push("opsConsole");
|
|
9288
|
+
routes.use(createVoiceOpsConsoleRoutes({
|
|
9289
|
+
...common,
|
|
9290
|
+
links: toOpsLinks(links),
|
|
9291
|
+
llmProviders: options.llmProviders,
|
|
9292
|
+
sttProviders: options.sttProviders,
|
|
9293
|
+
title: options.title,
|
|
9294
|
+
ttsProviders: options.ttsProviders,
|
|
9295
|
+
...options.opsConsole
|
|
9296
|
+
}));
|
|
9297
|
+
}
|
|
9298
|
+
return {
|
|
9299
|
+
links,
|
|
9300
|
+
routes,
|
|
9301
|
+
surfaces,
|
|
9302
|
+
use: routes.use.bind(routes)
|
|
9303
|
+
};
|
|
9304
|
+
};
|
|
9305
|
+
var createVoiceAppKit = createVoiceAppKitRoutes;
|
|
9306
|
+
// src/workflowContract.ts
|
|
9307
|
+
var getObject2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
9308
|
+
var getPathValue2 = (value, path) => {
|
|
9309
|
+
let current = value;
|
|
9310
|
+
for (const part of path.split(".").filter(Boolean)) {
|
|
9311
|
+
const record = getObject2(current);
|
|
9312
|
+
if (!record || !(part in record)) {
|
|
9313
|
+
return;
|
|
9314
|
+
}
|
|
9315
|
+
current = record[part];
|
|
9316
|
+
}
|
|
9317
|
+
return current;
|
|
9318
|
+
};
|
|
9319
|
+
var hasValue = (value, match) => {
|
|
9320
|
+
switch (match) {
|
|
9321
|
+
case "boolean":
|
|
9322
|
+
return typeof value === "boolean";
|
|
9323
|
+
case "number":
|
|
9324
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
9325
|
+
case "string":
|
|
9326
|
+
return typeof value === "string";
|
|
9327
|
+
case "truthy":
|
|
9328
|
+
return Boolean(value);
|
|
9329
|
+
case "non-empty":
|
|
9330
|
+
default:
|
|
9331
|
+
return Array.isArray(value) ? value.length > 0 : typeof value === "string" ? value.trim().length > 0 : value !== undefined && value !== null;
|
|
9332
|
+
}
|
|
9333
|
+
};
|
|
9334
|
+
var resolveOutcome2 = (routeResult) => {
|
|
9335
|
+
if (routeResult.complete)
|
|
9336
|
+
return "complete";
|
|
9337
|
+
if (routeResult.transfer)
|
|
9338
|
+
return "transfer";
|
|
9339
|
+
if (routeResult.escalate)
|
|
9340
|
+
return "escalate";
|
|
9341
|
+
if (routeResult.voicemail)
|
|
9342
|
+
return "voicemail";
|
|
9343
|
+
if (routeResult.noAnswer)
|
|
9344
|
+
return "no-answer";
|
|
9345
|
+
return;
|
|
9346
|
+
};
|
|
9347
|
+
var validateVoiceWorkflowRouteResult = (definition, routeResult) => {
|
|
9348
|
+
const issues = [];
|
|
9349
|
+
const requiredFields = (definition.fields ?? []).filter((field) => field.required !== false).map((field) => field.path);
|
|
9350
|
+
const missingFields = [];
|
|
9351
|
+
const outcome = resolveOutcome2(routeResult);
|
|
9352
|
+
if (definition.outcome && outcome !== definition.outcome) {
|
|
9353
|
+
issues.push({
|
|
9354
|
+
code: "workflow.outcome_mismatch",
|
|
9355
|
+
message: `Expected workflow outcome ${definition.outcome}, saw ${outcome ?? "none"}.`
|
|
9356
|
+
});
|
|
9357
|
+
}
|
|
9358
|
+
for (const field of definition.fields ?? []) {
|
|
9359
|
+
if (field.required === false)
|
|
9360
|
+
continue;
|
|
9361
|
+
const paths = [field.path, ...field.aliases ?? []];
|
|
9362
|
+
const present = paths.some((path) => hasValue(getPathValue2(routeResult.result, path), field.match ?? "non-empty"));
|
|
9363
|
+
if (!present) {
|
|
9364
|
+
missingFields.push(field.path);
|
|
9365
|
+
issues.push({
|
|
9366
|
+
code: "workflow.missing_field",
|
|
9367
|
+
field: field.path,
|
|
9368
|
+
message: `Missing required workflow field: ${field.label ?? field.path}.`
|
|
9369
|
+
});
|
|
9370
|
+
}
|
|
9371
|
+
}
|
|
9372
|
+
issues.push(...definition.validate?.({
|
|
9373
|
+
result: routeResult.result,
|
|
9374
|
+
routeResult
|
|
9375
|
+
}) ?? []);
|
|
9376
|
+
return {
|
|
9377
|
+
contractId: definition.id,
|
|
9378
|
+
issues,
|
|
9379
|
+
missingFields,
|
|
9380
|
+
outcome,
|
|
9381
|
+
pass: issues.length === 0,
|
|
9382
|
+
requiredFields
|
|
9383
|
+
};
|
|
9384
|
+
};
|
|
9385
|
+
var createVoiceWorkflowScenario = (definition, overrides = {}) => ({
|
|
9386
|
+
description: definition.description,
|
|
9387
|
+
forbiddenHandoffActions: definition.forbiddenHandoffActions,
|
|
9388
|
+
id: definition.id,
|
|
9389
|
+
label: definition.label,
|
|
9390
|
+
maxProviderErrors: definition.maxProviderErrors,
|
|
9391
|
+
maxSessionErrors: definition.maxSessionErrors,
|
|
9392
|
+
minSessions: definition.minSessions,
|
|
9393
|
+
minTurns: definition.minTurns,
|
|
9394
|
+
requiredAssistantIncludes: definition.requiredAssistantIncludes,
|
|
9395
|
+
requiredDisposition: definition.requiredDisposition,
|
|
9396
|
+
requiredHandoffActions: definition.requiredHandoffActions,
|
|
9397
|
+
requiredLifecycleTypes: definition.requiredLifecycleTypes,
|
|
9398
|
+
requiredTranscriptIncludes: definition.requiredTranscriptIncludes,
|
|
9399
|
+
requiredWorkflowContracts: [definition.id],
|
|
9400
|
+
scenarioId: definition.scenarioId,
|
|
9401
|
+
...overrides
|
|
9402
|
+
});
|
|
9403
|
+
var createVoiceWorkflowContract = (definition) => ({
|
|
9404
|
+
assertRouteResult: (routeResult) => {
|
|
9405
|
+
const validation = validateVoiceWorkflowRouteResult(definition, routeResult);
|
|
9406
|
+
if (!validation.pass) {
|
|
9407
|
+
throw new Error(`Voice workflow contract ${definition.id} failed: ${validation.issues.map((issue) => issue.message).join(" ")}`);
|
|
9408
|
+
}
|
|
9409
|
+
},
|
|
9410
|
+
definition,
|
|
9411
|
+
toScenarioEval: (overrides) => createVoiceWorkflowScenario(definition, overrides),
|
|
9412
|
+
validateRouteResult: (routeResult) => validateVoiceWorkflowRouteResult(definition, routeResult)
|
|
9413
|
+
});
|
|
9414
|
+
var presetDefinitions = {
|
|
9415
|
+
"appointment-booking": {
|
|
9416
|
+
description: "Appointment booking should complete with enough identity, appointment, and follow-up details to act on.",
|
|
9417
|
+
fields: [
|
|
9418
|
+
{ aliases: ["name", "customer.name"], label: "Caller name", path: "caller.name" },
|
|
9419
|
+
{
|
|
9420
|
+
aliases: ["phone", "customer.phone"],
|
|
9421
|
+
label: "Caller phone",
|
|
9422
|
+
path: "caller.phone"
|
|
9423
|
+
},
|
|
9424
|
+
{
|
|
9425
|
+
aliases: ["appointment.start", "appointment.time", "scheduledAt"],
|
|
9426
|
+
label: "Appointment time",
|
|
9427
|
+
path: "appointment.startsAt"
|
|
9428
|
+
},
|
|
9429
|
+
{
|
|
9430
|
+
aliases: ["summary", "assistantSummary"],
|
|
9431
|
+
label: "Summary",
|
|
9432
|
+
path: "appointment.summary"
|
|
9433
|
+
}
|
|
9434
|
+
],
|
|
9435
|
+
id: "appointment-booking",
|
|
9436
|
+
label: "Appointment booking",
|
|
9437
|
+
outcome: "complete",
|
|
9438
|
+
requiredDisposition: "completed"
|
|
9439
|
+
},
|
|
9440
|
+
"lead-qualification": {
|
|
9441
|
+
description: "Lead qualification should complete with contact, need, qualification, and next-step fields.",
|
|
9442
|
+
fields: [
|
|
9443
|
+
{ aliases: ["name", "lead.name"], label: "Lead name", path: "contact.name" },
|
|
9444
|
+
{
|
|
9445
|
+
aliases: ["email", "lead.email"],
|
|
9446
|
+
label: "Lead email",
|
|
9447
|
+
path: "contact.email"
|
|
9448
|
+
},
|
|
9449
|
+
{
|
|
9450
|
+
aliases: ["need", "pain", "summary"],
|
|
9451
|
+
label: "Need",
|
|
9452
|
+
path: "qualification.need"
|
|
9453
|
+
},
|
|
9454
|
+
{
|
|
9455
|
+
aliases: ["qualified", "qualification.qualified"],
|
|
9456
|
+
label: "Qualified",
|
|
9457
|
+
match: "boolean",
|
|
9458
|
+
path: "qualification.isQualified"
|
|
8610
9459
|
},
|
|
8611
9460
|
{
|
|
8612
9461
|
aliases: ["nextStep", "followUp"],
|
|
@@ -8668,327 +9517,85 @@ var presetDefinitions = {
|
|
|
8668
9517
|
}
|
|
8669
9518
|
],
|
|
8670
9519
|
id: "transfer-handoff",
|
|
8671
|
-
label: "Transfer handoff",
|
|
8672
|
-
outcome: "transfer",
|
|
8673
|
-
requiredDisposition: "transferred",
|
|
8674
|
-
requiredHandoffActions: ["transfer"]
|
|
8675
|
-
},
|
|
8676
|
-
"voicemail-callback": {
|
|
8677
|
-
description: "Voicemail callback should preserve enough caller and callback context for follow-up.",
|
|
8678
|
-
fields: [
|
|
8679
|
-
{
|
|
8680
|
-
aliases: ["name", "caller.name"],
|
|
8681
|
-
label: "Caller name",
|
|
8682
|
-
path: "voicemail.callerName"
|
|
8683
|
-
},
|
|
8684
|
-
{
|
|
8685
|
-
aliases: ["phone", "caller.phone"],
|
|
8686
|
-
label: "Callback phone",
|
|
8687
|
-
path: "voicemail.callbackPhone"
|
|
8688
|
-
},
|
|
8689
|
-
{
|
|
8690
|
-
aliases: ["message", "summary", "assistantSummary"],
|
|
8691
|
-
label: "Voicemail summary",
|
|
8692
|
-
path: "voicemail.summary"
|
|
8693
|
-
}
|
|
8694
|
-
],
|
|
8695
|
-
id: "voicemail-callback",
|
|
8696
|
-
label: "Voicemail callback",
|
|
8697
|
-
outcome: "voicemail",
|
|
8698
|
-
requiredDisposition: "voicemail",
|
|
8699
|
-
requiredHandoffActions: ["voicemail"]
|
|
8700
|
-
}
|
|
8701
|
-
};
|
|
8702
|
-
var createVoiceWorkflowContractPreset = (name, options = {}) => {
|
|
8703
|
-
const preset = presetDefinitions[name];
|
|
8704
|
-
return createVoiceWorkflowContract({
|
|
8705
|
-
...preset,
|
|
8706
|
-
...options,
|
|
8707
|
-
fields: options.fields ?? preset.fields,
|
|
8708
|
-
id: options.id ?? preset.id
|
|
8709
|
-
});
|
|
8710
|
-
};
|
|
8711
|
-
var recordVoiceWorkflowContractTrace = async (input) => input.store.append({
|
|
8712
|
-
at: input.at ?? Date.now(),
|
|
8713
|
-
payload: {
|
|
8714
|
-
contractId: input.contractId ?? input.validation.contractId,
|
|
8715
|
-
issues: input.validation.issues,
|
|
8716
|
-
missingFields: input.validation.missingFields,
|
|
8717
|
-
outcome: input.validation.outcome,
|
|
8718
|
-
requiredFields: input.validation.requiredFields,
|
|
8719
|
-
status: input.validation.pass ? "pass" : "fail"
|
|
8720
|
-
},
|
|
8721
|
-
scenarioId: input.scenarioId,
|
|
8722
|
-
sessionId: input.sessionId,
|
|
8723
|
-
traceId: input.traceId,
|
|
8724
|
-
turnId: input.turnId,
|
|
8725
|
-
type: "workflow.contract"
|
|
8726
|
-
});
|
|
8727
|
-
var createVoiceWorkflowContractHandler = (input) => {
|
|
8728
|
-
return async (session, turn, api, context) => {
|
|
8729
|
-
const legacyHandler = input.handler;
|
|
8730
|
-
const objectHandler = input.handler;
|
|
8731
|
-
const result = input.handler.length >= 4 ? await legacyHandler(session, turn, api, context) : await objectHandler({ api, context, session, turn });
|
|
8732
|
-
if (!result)
|
|
8733
|
-
return result;
|
|
8734
|
-
const resolved = input.resolveContract?.({ context, result, session, turn }) ?? input.contract;
|
|
8735
|
-
if (!resolved)
|
|
8736
|
-
return result;
|
|
8737
|
-
const contract = "validateRouteResult" in resolved ? resolved : createVoiceWorkflowContract(resolved);
|
|
8738
|
-
const validation = contract.validateRouteResult(result);
|
|
8739
|
-
if (input.store) {
|
|
8740
|
-
await recordVoiceWorkflowContractTrace({
|
|
8741
|
-
scenarioId: session.scenarioId,
|
|
8742
|
-
sessionId: session.id,
|
|
8743
|
-
store: input.store,
|
|
8744
|
-
turnId: turn.id,
|
|
8745
|
-
validation
|
|
8746
|
-
});
|
|
8747
|
-
}
|
|
8748
|
-
return result;
|
|
8749
|
-
};
|
|
8750
|
-
};
|
|
8751
|
-
// src/sessionReplay.ts
|
|
8752
|
-
import { Elysia as Elysia8 } from "elysia";
|
|
8753
|
-
var getString7 = (value) => typeof value === "string" ? value : undefined;
|
|
8754
|
-
var escapeHtml10 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
8755
|
-
var increment3 = (record, key) => {
|
|
8756
|
-
record[key] = (record[key] ?? 0) + 1;
|
|
8757
|
-
};
|
|
8758
|
-
var buildReplayTurns = (events) => {
|
|
8759
|
-
const turns = new Map;
|
|
8760
|
-
const getTurn = (turnId) => {
|
|
8761
|
-
const existing = turns.get(turnId);
|
|
8762
|
-
if (existing) {
|
|
8763
|
-
return existing;
|
|
8764
|
-
}
|
|
8765
|
-
const turn = {
|
|
8766
|
-
assistantReplies: [],
|
|
8767
|
-
errors: [],
|
|
8768
|
-
id: turnId,
|
|
8769
|
-
modelCalls: [],
|
|
8770
|
-
tools: [],
|
|
8771
|
-
transcripts: []
|
|
8772
|
-
};
|
|
8773
|
-
turns.set(turnId, turn);
|
|
8774
|
-
return turn;
|
|
8775
|
-
};
|
|
8776
|
-
for (const event of events) {
|
|
8777
|
-
const turnId = event.turnId ?? "session";
|
|
8778
|
-
const turn = getTurn(turnId);
|
|
8779
|
-
switch (event.type) {
|
|
8780
|
-
case "turn.transcript":
|
|
8781
|
-
turn.transcripts.push({
|
|
8782
|
-
isFinal: event.payload.isFinal === true,
|
|
8783
|
-
text: getString7(event.payload.text)
|
|
8784
|
-
});
|
|
8785
|
-
break;
|
|
8786
|
-
case "turn.committed":
|
|
8787
|
-
turn.committedText = getString7(event.payload.text);
|
|
8788
|
-
break;
|
|
8789
|
-
case "turn.assistant": {
|
|
8790
|
-
const text = getString7(event.payload.text);
|
|
8791
|
-
if (text) {
|
|
8792
|
-
turn.assistantReplies.push(text);
|
|
8793
|
-
}
|
|
8794
|
-
break;
|
|
8795
|
-
}
|
|
8796
|
-
case "agent.model":
|
|
8797
|
-
case "assistant.run":
|
|
8798
|
-
turn.modelCalls.push(event.payload);
|
|
8799
|
-
break;
|
|
8800
|
-
case "agent.tool":
|
|
8801
|
-
turn.tools.push(event.payload);
|
|
8802
|
-
break;
|
|
8803
|
-
case "session.error":
|
|
8804
|
-
turn.errors.push(event.payload);
|
|
8805
|
-
break;
|
|
8806
|
-
}
|
|
8807
|
-
}
|
|
8808
|
-
return [...turns.values()];
|
|
8809
|
-
};
|
|
8810
|
-
var summarizeVoiceSessionReplay = async (options) => {
|
|
8811
|
-
const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
|
|
8812
|
-
const events = filterVoiceTraceEvents(sourceEvents, {
|
|
8813
|
-
sessionId: options.sessionId
|
|
8814
|
-
});
|
|
8815
|
-
const replay = buildVoiceTraceReplay(events, {
|
|
8816
|
-
evaluation: options.evaluation,
|
|
8817
|
-
redact: options.redact,
|
|
8818
|
-
title: options.title ?? `Voice Session ${options.sessionId}`
|
|
8819
|
-
});
|
|
8820
|
-
const startedAt = replay.summary.startedAt;
|
|
8821
|
-
return {
|
|
8822
|
-
evaluation: replay.evaluation,
|
|
8823
|
-
events,
|
|
8824
|
-
html: replay.html,
|
|
8825
|
-
markdown: replay.markdown,
|
|
8826
|
-
sessionId: options.sessionId,
|
|
8827
|
-
summary: replay.summary,
|
|
8828
|
-
timeline: events.map((event) => ({
|
|
8829
|
-
at: event.at,
|
|
8830
|
-
offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
|
|
8831
|
-
payload: event.payload,
|
|
8832
|
-
turnId: event.turnId,
|
|
8833
|
-
type: event.type
|
|
8834
|
-
})),
|
|
8835
|
-
turns: buildReplayTurns(events)
|
|
8836
|
-
};
|
|
8837
|
-
};
|
|
8838
|
-
var summarizeVoiceSessions = async (options = {}) => {
|
|
8839
|
-
const events = options.events ?? await options.store?.list() ?? [];
|
|
8840
|
-
const grouped = new Map;
|
|
8841
|
-
for (const event of events) {
|
|
8842
|
-
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
8843
|
-
}
|
|
8844
|
-
const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
|
|
8845
|
-
const sorted = filterVoiceTraceEvents(sessionEvents);
|
|
8846
|
-
const summary = buildVoiceTraceReplay(sorted, {
|
|
8847
|
-
evaluation: {
|
|
8848
|
-
requireAssistantReply: false,
|
|
8849
|
-
requireCompletedCall: false,
|
|
8850
|
-
requireTranscript: false,
|
|
8851
|
-
requireTurn: false
|
|
8852
|
-
}
|
|
8853
|
-
}).summary;
|
|
8854
|
-
const providerErrors = {};
|
|
8855
|
-
const providers = new Set;
|
|
8856
|
-
let latestOutcome;
|
|
8857
|
-
let errorCount = 0;
|
|
8858
|
-
for (const event of sorted) {
|
|
8859
|
-
const provider = getString7(event.payload.provider);
|
|
8860
|
-
if (provider) {
|
|
8861
|
-
providers.add(provider);
|
|
8862
|
-
}
|
|
8863
|
-
if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
|
|
8864
|
-
errorCount += 1;
|
|
8865
|
-
increment3(providerErrors, provider ?? "unknown");
|
|
8866
|
-
}
|
|
8867
|
-
const outcome = getString7(event.payload.outcome);
|
|
8868
|
-
if (outcome) {
|
|
8869
|
-
latestOutcome = outcome;
|
|
8870
|
-
}
|
|
8871
|
-
}
|
|
8872
|
-
const item = {
|
|
8873
|
-
endedAt: summary.endedAt,
|
|
8874
|
-
errorCount,
|
|
8875
|
-
eventCount: summary.eventCount,
|
|
8876
|
-
latestOutcome,
|
|
8877
|
-
providerErrors,
|
|
8878
|
-
providers: [...providers].sort(),
|
|
8879
|
-
sessionId,
|
|
8880
|
-
startedAt: summary.startedAt,
|
|
8881
|
-
status: errorCount > 0 ? "failed" : "healthy",
|
|
8882
|
-
transcriptCount: summary.transcriptCount,
|
|
8883
|
-
turnCount: summary.turnCount
|
|
8884
|
-
};
|
|
8885
|
-
const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
|
|
8886
|
-
return {
|
|
8887
|
-
...item,
|
|
8888
|
-
replayHref
|
|
8889
|
-
};
|
|
8890
|
-
});
|
|
8891
|
-
const search = options.q?.trim().toLowerCase();
|
|
8892
|
-
return sessions.filter((session) => {
|
|
8893
|
-
if (options.status && options.status !== "all" && session.status !== options.status) {
|
|
8894
|
-
return false;
|
|
8895
|
-
}
|
|
8896
|
-
if (options.provider && !session.providers.includes(options.provider)) {
|
|
8897
|
-
return false;
|
|
8898
|
-
}
|
|
8899
|
-
if (!search) {
|
|
8900
|
-
return true;
|
|
8901
|
-
}
|
|
8902
|
-
return [
|
|
8903
|
-
session.sessionId,
|
|
8904
|
-
session.latestOutcome,
|
|
8905
|
-
session.status,
|
|
8906
|
-
...session.providers
|
|
8907
|
-
].some((value) => value?.toLowerCase().includes(search));
|
|
8908
|
-
}).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
|
|
9520
|
+
label: "Transfer handoff",
|
|
9521
|
+
outcome: "transfer",
|
|
9522
|
+
requiredDisposition: "transferred",
|
|
9523
|
+
requiredHandoffActions: ["transfer"]
|
|
9524
|
+
},
|
|
9525
|
+
"voicemail-callback": {
|
|
9526
|
+
description: "Voicemail callback should preserve enough caller and callback context for follow-up.",
|
|
9527
|
+
fields: [
|
|
9528
|
+
{
|
|
9529
|
+
aliases: ["name", "caller.name"],
|
|
9530
|
+
label: "Caller name",
|
|
9531
|
+
path: "voicemail.callerName"
|
|
9532
|
+
},
|
|
9533
|
+
{
|
|
9534
|
+
aliases: ["phone", "caller.phone"],
|
|
9535
|
+
label: "Callback phone",
|
|
9536
|
+
path: "voicemail.callbackPhone"
|
|
9537
|
+
},
|
|
9538
|
+
{
|
|
9539
|
+
aliases: ["message", "summary", "assistantSummary"],
|
|
9540
|
+
label: "Voicemail summary",
|
|
9541
|
+
path: "voicemail.summary"
|
|
9542
|
+
}
|
|
9543
|
+
],
|
|
9544
|
+
id: "voicemail-callback",
|
|
9545
|
+
label: "Voicemail callback",
|
|
9546
|
+
outcome: "voicemail",
|
|
9547
|
+
requiredDisposition: "voicemail",
|
|
9548
|
+
requiredHandoffActions: ["voicemail"]
|
|
9549
|
+
}
|
|
8909
9550
|
};
|
|
8910
|
-
var
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
'<div class="voice-session-card-header">',
|
|
8915
|
-
`<strong>${escapeHtml10(session.sessionId)}</strong>`,
|
|
8916
|
-
`<span>${escapeHtml10(session.status)}</span>`,
|
|
8917
|
-
"</div>",
|
|
8918
|
-
"<dl>",
|
|
8919
|
-
`<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
|
|
8920
|
-
`<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
|
|
8921
|
-
`<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
|
|
8922
|
-
`<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
|
|
8923
|
-
"</dl>",
|
|
8924
|
-
session.latestOutcome ? `<p>Outcome: ${escapeHtml10(session.latestOutcome)}</p>` : "",
|
|
8925
|
-
session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml10).join(", ")}</p>` : "",
|
|
8926
|
-
session.replayHref ? `<p><a href="${escapeHtml10(session.replayHref)}">Open replay</a></p>` : "",
|
|
8927
|
-
"</article>"
|
|
8928
|
-
].join("")),
|
|
8929
|
-
"</div>"
|
|
8930
|
-
].join("");
|
|
8931
|
-
var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
|
|
8932
|
-
...options,
|
|
8933
|
-
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
8934
|
-
provider: query?.provider ?? options.provider,
|
|
8935
|
-
q: query?.q ?? options.q,
|
|
8936
|
-
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
8937
|
-
});
|
|
8938
|
-
var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
|
|
8939
|
-
const sessions = await summarizeVoiceSessions({
|
|
9551
|
+
var createVoiceWorkflowContractPreset = (name, options = {}) => {
|
|
9552
|
+
const preset = presetDefinitions[name];
|
|
9553
|
+
return createVoiceWorkflowContract({
|
|
9554
|
+
...preset,
|
|
8940
9555
|
...options,
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
q: query?.q ?? options.q,
|
|
8944
|
-
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
8945
|
-
});
|
|
8946
|
-
const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
|
|
8947
|
-
return new Response(body, {
|
|
8948
|
-
headers: {
|
|
8949
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
8950
|
-
...options.headers
|
|
8951
|
-
}
|
|
9556
|
+
fields: options.fields ?? preset.fields,
|
|
9557
|
+
id: options.id ?? preset.id
|
|
8952
9558
|
});
|
|
8953
9559
|
};
|
|
8954
|
-
var
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
9560
|
+
var recordVoiceWorkflowContractTrace = async (input) => input.store.append({
|
|
9561
|
+
at: input.at ?? Date.now(),
|
|
9562
|
+
payload: {
|
|
9563
|
+
contractId: input.contractId ?? input.validation.contractId,
|
|
9564
|
+
issues: input.validation.issues,
|
|
9565
|
+
missingFields: input.validation.missingFields,
|
|
9566
|
+
outcome: input.validation.outcome,
|
|
9567
|
+
requiredFields: input.validation.requiredFields,
|
|
9568
|
+
status: input.validation.pass ? "pass" : "fail"
|
|
9569
|
+
},
|
|
9570
|
+
scenarioId: input.scenarioId,
|
|
9571
|
+
sessionId: input.sessionId,
|
|
9572
|
+
traceId: input.traceId,
|
|
9573
|
+
turnId: input.turnId,
|
|
9574
|
+
type: "workflow.contract"
|
|
8968
9575
|
});
|
|
8969
|
-
var
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
9576
|
+
var createVoiceWorkflowContractHandler = (input) => {
|
|
9577
|
+
return async (session, turn, api, context) => {
|
|
9578
|
+
const legacyHandler = input.handler;
|
|
9579
|
+
const objectHandler = input.handler;
|
|
9580
|
+
const result = input.handler.length >= 4 ? await legacyHandler(session, turn, api, context) : await objectHandler({ api, context, session, turn });
|
|
9581
|
+
if (!result)
|
|
9582
|
+
return result;
|
|
9583
|
+
const resolved = input.resolveContract?.({ context, result, session, turn }) ?? input.contract;
|
|
9584
|
+
if (!resolved)
|
|
9585
|
+
return result;
|
|
9586
|
+
const contract = "validateRouteResult" in resolved ? resolved : createVoiceWorkflowContract(resolved);
|
|
9587
|
+
const validation = contract.validateRouteResult(result);
|
|
9588
|
+
if (input.store) {
|
|
9589
|
+
await recordVoiceWorkflowContractTrace({
|
|
9590
|
+
scenarioId: session.scenarioId,
|
|
9591
|
+
sessionId: session.id,
|
|
9592
|
+
store: input.store,
|
|
9593
|
+
turnId: turn.id,
|
|
9594
|
+
validation
|
|
9595
|
+
});
|
|
8979
9596
|
}
|
|
8980
|
-
|
|
8981
|
-
};
|
|
8982
|
-
var createVoiceSessionReplayRoutes = (options) => {
|
|
8983
|
-
const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
|
|
8984
|
-
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
8985
|
-
const routes = new Elysia8({
|
|
8986
|
-
name: options.name ?? "absolutejs-voice-session-replay"
|
|
8987
|
-
}).get(path, createVoiceSessionReplayJSONHandler(options));
|
|
8988
|
-
if (htmlPath) {
|
|
8989
|
-
routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
|
|
8990
|
-
}
|
|
8991
|
-
return routes;
|
|
9597
|
+
return result;
|
|
9598
|
+
};
|
|
8992
9599
|
};
|
|
8993
9600
|
// src/fileStore.ts
|
|
8994
9601
|
import { mkdir as mkdir2, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
@@ -9989,571 +10596,134 @@ var createAnthropicVoiceAssistantModel = (options) => {
|
|
|
9989
10596
|
method: "POST"
|
|
9990
10597
|
});
|
|
9991
10598
|
if (!response.ok) {
|
|
9992
|
-
throw createHTTPError("Anthropic", response);
|
|
9993
|
-
}
|
|
9994
|
-
const body = await response.json();
|
|
9995
|
-
if (body.usage && typeof body.usage === "object") {
|
|
9996
|
-
await options.onUsage?.(body.usage);
|
|
9997
|
-
}
|
|
9998
|
-
const toolCalls = extractAnthropicToolCalls(body);
|
|
9999
|
-
if (toolCalls.length) {
|
|
10000
|
-
return {
|
|
10001
|
-
assistantText: extractAnthropicText(body) || undefined,
|
|
10002
|
-
toolCalls
|
|
10003
|
-
};
|
|
10004
|
-
}
|
|
10005
|
-
return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
|
|
10006
|
-
}
|
|
10007
|
-
};
|
|
10008
|
-
};
|
|
10009
|
-
var extractGeminiCandidateParts = (response) => {
|
|
10010
|
-
const candidates = Array.isArray(response.candidates) ? response.candidates : [];
|
|
10011
|
-
const first = candidates[0];
|
|
10012
|
-
if (!first || typeof first !== "object") {
|
|
10013
|
-
return [];
|
|
10014
|
-
}
|
|
10015
|
-
const content = first.content;
|
|
10016
|
-
if (!content || typeof content !== "object") {
|
|
10017
|
-
return [];
|
|
10018
|
-
}
|
|
10019
|
-
const parts = content.parts;
|
|
10020
|
-
return Array.isArray(parts) ? parts : [];
|
|
10021
|
-
};
|
|
10022
|
-
var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
10023
|
-
`);
|
|
10024
|
-
var extractGeminiToolCalls = (response) => {
|
|
10025
|
-
const toolCalls = [];
|
|
10026
|
-
for (const part of extractGeminiCandidateParts(response)) {
|
|
10027
|
-
if (!part || typeof part !== "object") {
|
|
10028
|
-
continue;
|
|
10029
|
-
}
|
|
10030
|
-
const functionCall = part.functionCall;
|
|
10031
|
-
if (!functionCall || typeof functionCall !== "object") {
|
|
10032
|
-
continue;
|
|
10033
|
-
}
|
|
10034
|
-
const record = functionCall;
|
|
10035
|
-
if (typeof record.name !== "string") {
|
|
10036
|
-
continue;
|
|
10037
|
-
}
|
|
10038
|
-
toolCalls.push({
|
|
10039
|
-
args: record.args && typeof record.args === "object" ? record.args : {},
|
|
10040
|
-
id: typeof record.id === "string" ? record.id : undefined,
|
|
10041
|
-
name: record.name
|
|
10042
|
-
});
|
|
10043
|
-
}
|
|
10044
|
-
return toolCalls;
|
|
10045
|
-
};
|
|
10046
|
-
var createGeminiVoiceAssistantModel = (options) => {
|
|
10047
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
10048
|
-
const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
10049
|
-
const model = options.model ?? "gemini-2.5-flash";
|
|
10050
|
-
const maxRetries = Math.max(0, options.maxRetries ?? 2);
|
|
10051
|
-
return {
|
|
10052
|
-
generate: async (input) => {
|
|
10053
|
-
const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
|
|
10054
|
-
let response;
|
|
10055
|
-
for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
|
|
10056
|
-
response = await fetchImpl(endpoint, {
|
|
10057
|
-
body: JSON.stringify({
|
|
10058
|
-
contents: input.messages.map(messageToGeminiContent).filter(Boolean),
|
|
10059
|
-
generationConfig: {
|
|
10060
|
-
maxOutputTokens: options.maxOutputTokens,
|
|
10061
|
-
...input.tools.length ? {} : {
|
|
10062
|
-
responseMimeType: "application/json",
|
|
10063
|
-
responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
|
|
10064
|
-
},
|
|
10065
|
-
temperature: options.temperature
|
|
10066
|
-
},
|
|
10067
|
-
systemInstruction: {
|
|
10068
|
-
parts: [
|
|
10069
|
-
{
|
|
10070
|
-
text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
10071
|
-
|
|
10072
|
-
`)
|
|
10073
|
-
}
|
|
10074
|
-
]
|
|
10075
|
-
},
|
|
10076
|
-
tools: input.tools.length ? [
|
|
10077
|
-
{
|
|
10078
|
-
functionDeclarations: input.tools.map((tool) => ({
|
|
10079
|
-
description: tool.description,
|
|
10080
|
-
name: tool.name,
|
|
10081
|
-
parameters: toGeminiSchema(tool.parameters ?? {
|
|
10082
|
-
additionalProperties: true,
|
|
10083
|
-
type: "object"
|
|
10084
|
-
})
|
|
10085
|
-
}))
|
|
10086
|
-
}
|
|
10087
|
-
] : undefined
|
|
10088
|
-
}),
|
|
10089
|
-
headers: {
|
|
10090
|
-
"content-type": "application/json"
|
|
10091
|
-
},
|
|
10092
|
-
method: "POST"
|
|
10093
|
-
});
|
|
10094
|
-
if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
|
|
10095
|
-
break;
|
|
10096
|
-
}
|
|
10097
|
-
const retryAfter = Number(response.headers.get("retry-after"));
|
|
10098
|
-
await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
|
|
10099
|
-
}
|
|
10100
|
-
if (!response) {
|
|
10101
|
-
throw new Error("Gemini voice assistant model failed: no response");
|
|
10102
|
-
}
|
|
10103
|
-
if (!response.ok) {
|
|
10104
|
-
throw createHTTPError("Gemini", response);
|
|
10599
|
+
throw createHTTPError("Anthropic", response);
|
|
10105
10600
|
}
|
|
10106
10601
|
const body = await response.json();
|
|
10107
|
-
if (body.
|
|
10108
|
-
await options.onUsage?.(body.
|
|
10602
|
+
if (body.usage && typeof body.usage === "object") {
|
|
10603
|
+
await options.onUsage?.(body.usage);
|
|
10109
10604
|
}
|
|
10110
|
-
const toolCalls =
|
|
10605
|
+
const toolCalls = extractAnthropicToolCalls(body);
|
|
10111
10606
|
if (toolCalls.length) {
|
|
10112
10607
|
return {
|
|
10113
|
-
assistantText:
|
|
10608
|
+
assistantText: extractAnthropicText(body) || undefined,
|
|
10114
10609
|
toolCalls
|
|
10115
10610
|
};
|
|
10116
10611
|
}
|
|
10117
|
-
return normalizeRouteOutput(parseJSON(
|
|
10118
|
-
}
|
|
10119
|
-
};
|
|
10120
|
-
};
|
|
10121
|
-
// src/opsConsoleRoutes.ts
|
|
10122
|
-
import { Elysia as Elysia10 } from "elysia";
|
|
10123
|
-
|
|
10124
|
-
// src/resilienceRoutes.ts
|
|
10125
|
-
import { Elysia as Elysia9 } from "elysia";
|
|
10126
|
-
var escapeHtml11 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10127
|
-
var getString8 = (value) => typeof value === "string" ? value : undefined;
|
|
10128
|
-
var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
10129
|
-
var getBoolean2 = (value) => value === true;
|
|
10130
|
-
var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
|
|
10131
|
-
var listVoiceRoutingEvents = (events) => {
|
|
10132
|
-
const routingEvents = [];
|
|
10133
|
-
for (const event of events) {
|
|
10134
|
-
if (event.type !== "session.error") {
|
|
10135
|
-
continue;
|
|
10136
|
-
}
|
|
10137
|
-
const provider = getString8(event.payload.provider);
|
|
10138
|
-
const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
10139
|
-
if (!provider || !providerStatus) {
|
|
10140
|
-
continue;
|
|
10141
|
-
}
|
|
10142
|
-
const kind = getString8(event.payload.kind);
|
|
10143
|
-
routingEvents.push({
|
|
10144
|
-
at: event.at,
|
|
10145
|
-
attempt: getNumber4(event.payload.attempt),
|
|
10146
|
-
elapsedMs: getNumber4(event.payload.elapsedMs),
|
|
10147
|
-
error: getString8(event.payload.error),
|
|
10148
|
-
fallbackProvider: getString8(event.payload.fallbackProvider),
|
|
10149
|
-
kind: kind === "stt" || kind === "tts" ? kind : "llm",
|
|
10150
|
-
latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
|
|
10151
|
-
operation: getString8(event.payload.operation),
|
|
10152
|
-
provider,
|
|
10153
|
-
selectedProvider: getString8(event.payload.selectedProvider),
|
|
10154
|
-
sessionId: event.sessionId,
|
|
10155
|
-
status: providerStatus,
|
|
10156
|
-
timedOut: getBoolean2(event.payload.timedOut),
|
|
10157
|
-
turnId: event.turnId
|
|
10158
|
-
});
|
|
10159
|
-
}
|
|
10160
|
-
return routingEvents.sort((left, right) => right.at - left.at);
|
|
10161
|
-
};
|
|
10162
|
-
var summarizeRoutingEvents = (events) => {
|
|
10163
|
-
const byKind = new Map;
|
|
10164
|
-
let errors = 0;
|
|
10165
|
-
let fallbacks = 0;
|
|
10166
|
-
let timeouts = 0;
|
|
10167
|
-
for (const event of events) {
|
|
10168
|
-
byKind.set(event.kind, (byKind.get(event.kind) ?? 0) + 1);
|
|
10169
|
-
if (event.status === "error") {
|
|
10170
|
-
errors += 1;
|
|
10171
|
-
}
|
|
10172
|
-
if (event.status === "fallback") {
|
|
10173
|
-
fallbacks += 1;
|
|
10174
|
-
}
|
|
10175
|
-
if (event.timedOut) {
|
|
10176
|
-
timeouts += 1;
|
|
10612
|
+
return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
|
|
10177
10613
|
}
|
|
10178
|
-
}
|
|
10179
|
-
return {
|
|
10180
|
-
byKind,
|
|
10181
|
-
errors,
|
|
10182
|
-
fallbacks,
|
|
10183
|
-
timeouts,
|
|
10184
|
-
total: events.length
|
|
10185
10614
|
};
|
|
10186
10615
|
};
|
|
10187
|
-
var
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
<article class="card provider ${escapeHtml11(provider.status)}">
|
|
10193
|
-
<div class="card-header">
|
|
10194
|
-
<strong>${escapeHtml11(provider.provider)}</strong>
|
|
10195
|
-
<span>${escapeHtml11(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
|
|
10196
|
-
</div>
|
|
10197
|
-
<dl>
|
|
10198
|
-
<div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
|
|
10199
|
-
<div><dt>Avg latency</dt><dd>${provider.averageElapsedMs ?? 0}ms</dd></div>
|
|
10200
|
-
<div><dt>Errors</dt><dd>${provider.errorCount}</dd></div>
|
|
10201
|
-
<div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
|
|
10202
|
-
<div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
|
|
10203
|
-
</dl>
|
|
10204
|
-
${provider.lastError ? `<p class="muted">${escapeHtml11(provider.lastError)}</p>` : ""}
|
|
10205
|
-
</article>
|
|
10206
|
-
`).join("")}</div>`;
|
|
10207
|
-
};
|
|
10208
|
-
var renderTimeline2 = (events) => {
|
|
10209
|
-
if (events.length === 0) {
|
|
10210
|
-
return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
|
|
10211
|
-
}
|
|
10212
|
-
return `<div class="timeline">${events.slice(0, 40).map((event) => `
|
|
10213
|
-
<article class="card event ${escapeHtml11(event.status ?? "unknown")}">
|
|
10214
|
-
<div class="card-header">
|
|
10215
|
-
<strong>${escapeHtml11(event.kind.toUpperCase())} ${escapeHtml11(event.operation ?? "generate")}</strong>
|
|
10216
|
-
<span>${new Date(event.at).toLocaleString()}</span>
|
|
10217
|
-
</div>
|
|
10218
|
-
<p>
|
|
10219
|
-
<span class="pill">${escapeHtml11(event.status ?? "unknown")}</span>
|
|
10220
|
-
<span class="pill">provider: ${escapeHtml11(event.provider ?? "unknown")}</span>
|
|
10221
|
-
${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml11(event.fallbackProvider)}</span>` : ""}
|
|
10222
|
-
${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
|
|
10223
|
-
</p>
|
|
10224
|
-
<dl>
|
|
10225
|
-
<div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
|
|
10226
|
-
<div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
|
|
10227
|
-
<div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
|
|
10228
|
-
<div><dt>Session</dt><dd>${escapeHtml11(event.sessionId)}</dd></div>
|
|
10229
|
-
</dl>
|
|
10230
|
-
${event.error ? `<p class="muted">${escapeHtml11(event.error)}</p>` : ""}
|
|
10231
|
-
</article>
|
|
10232
|
-
`).join("")}</div>`;
|
|
10233
|
-
};
|
|
10234
|
-
var renderSimulationControls = (kind, simulation) => {
|
|
10235
|
-
if (!simulation) {
|
|
10236
|
-
return "";
|
|
10616
|
+
var extractGeminiCandidateParts = (response) => {
|
|
10617
|
+
const candidates = Array.isArray(response.candidates) ? response.candidates : [];
|
|
10618
|
+
const first = candidates[0];
|
|
10619
|
+
if (!first || typeof first !== "object") {
|
|
10620
|
+
return [];
|
|
10237
10621
|
}
|
|
10238
|
-
const
|
|
10239
|
-
if (
|
|
10240
|
-
return
|
|
10622
|
+
const content = first.content;
|
|
10623
|
+
if (!content || typeof content !== "object") {
|
|
10624
|
+
return [];
|
|
10241
10625
|
}
|
|
10242
|
-
const
|
|
10243
|
-
|
|
10244
|
-
const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
|
|
10245
|
-
return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml11(pathPrefix)}">
|
|
10246
|
-
<p class="muted">${escapeHtml11(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
|
|
10247
|
-
<div class="simulate-actions">
|
|
10248
|
-
${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml11(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml11(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
|
|
10249
|
-
${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml11(provider.provider)}">Mark ${escapeHtml11(provider.provider)} recovered</button>`).join("")}
|
|
10250
|
-
</div>
|
|
10251
|
-
${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml11(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
|
|
10252
|
-
<pre class="simulate-output" hidden></pre>
|
|
10253
|
-
</div>`;
|
|
10254
|
-
};
|
|
10255
|
-
var renderVoiceResilienceHTML = (input) => {
|
|
10256
|
-
const summary = summarizeRoutingEvents(input.routingEvents);
|
|
10257
|
-
const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml11(kind)}: ${String(count)}</span>`).join("");
|
|
10258
|
-
const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml11(link.href)}">${escapeHtml11(link.label)}</a>`).join(" \xB7 ") : "";
|
|
10259
|
-
return `<!doctype html>
|
|
10260
|
-
<html lang="en">
|
|
10261
|
-
<head>
|
|
10262
|
-
<meta charset="utf-8" />
|
|
10263
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
10264
|
-
<title>${escapeHtml11(input.title ?? "AbsoluteJS Voice Resilience")}</title>
|
|
10265
|
-
<style>
|
|
10266
|
-
:root { color-scheme: dark; }
|
|
10267
|
-
body { background: radial-gradient(circle at top left, #172554, #09090b 36%, #050505); color: #f4f4f5; font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; padding: 24px; }
|
|
10268
|
-
main { display: grid; gap: 16px; margin: 0 auto; max-width: 1180px; }
|
|
10269
|
-
section, .card { background: rgba(19, 22, 27, 0.92); border: 1px solid #27272a; border-radius: 20px; padding: 20px; }
|
|
10270
|
-
.hero { background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(245, 158, 11, 0.12)); }
|
|
10271
|
-
.grid, .provider-grid { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
10272
|
-
.timeline { display: grid; gap: 12px; }
|
|
10273
|
-
.card-header { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
|
|
10274
|
-
.card-header strong { font-size: 1.05rem; }
|
|
10275
|
-
.metric strong { display: block; font-size: 2rem; margin-top: 6px; }
|
|
10276
|
-
.muted, dt, span { color: #a1a1aa; }
|
|
10277
|
-
dl { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
10278
|
-
dl div { background: #0f1217; border: 1px solid #27272a; border-radius: 12px; padding: 10px; }
|
|
10279
|
-
dd { font-weight: 800; margin: 4px 0 0; }
|
|
10280
|
-
.pill { background: #0f1217; border: 1px solid #3f3f46; border-radius: 999px; color: #d4d4d8; display: inline-flex; margin: 3px 4px 3px 0; padding: 5px 9px; }
|
|
10281
|
-
.danger { border-color: rgba(239, 68, 68, 0.75); color: #fecaca; }
|
|
10282
|
-
.event.error { border-color: rgba(239, 68, 68, 0.7); }
|
|
10283
|
-
.event.fallback { border-color: rgba(245, 158, 11, 0.7); }
|
|
10284
|
-
.event.success, .provider.healthy { border-color: rgba(34, 197, 94, 0.5); }
|
|
10285
|
-
.provider.suppressed, .provider.degraded, .provider.rate-limited { border-color: rgba(239, 68, 68, 0.7); }
|
|
10286
|
-
.provider.recoverable { border-color: rgba(59, 130, 246, 0.7); }
|
|
10287
|
-
button { background: #f59e0b; border: 0; border-radius: 999px; color: #111827; cursor: pointer; font-weight: 800; padding: 10px 14px; }
|
|
10288
|
-
button:disabled { cursor: not-allowed; opacity: 0.45; }
|
|
10289
|
-
.simulate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
|
|
10290
|
-
.simulate-output { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #d4d4d8; overflow: auto; padding: 12px; white-space: pre-wrap; }
|
|
10291
|
-
a { color: #f59e0b; }
|
|
10292
|
-
@media (max-width: 850px) { .grid, .provider-grid, dl { grid-template-columns: 1fr; } }
|
|
10293
|
-
</style>
|
|
10294
|
-
</head>
|
|
10295
|
-
<body>
|
|
10296
|
-
<main>
|
|
10297
|
-
<section class="hero">
|
|
10298
|
-
<h1>Provider routing and resilience</h1>
|
|
10299
|
-
<p>One view for the production reliability story: LLM failover, STT/TTS routing, latency budgets, timeouts, and fallback decisions.</p>
|
|
10300
|
-
${links ? `<p>${links}</p>` : ""}
|
|
10301
|
-
<p>${kindCounts || '<span class="pill">No routing events yet</span>'}</p>
|
|
10302
|
-
</section>
|
|
10303
|
-
<section class="grid">
|
|
10304
|
-
<article class="card metric"><span>Total routing events</span><strong>${summary.total}</strong></article>
|
|
10305
|
-
<article class="card metric"><span>Fallbacks</span><strong>${summary.fallbacks}</strong></article>
|
|
10306
|
-
<article class="card metric"><span>Errors</span><strong>${summary.errors}</strong></article>
|
|
10307
|
-
<article class="card metric"><span>Timeouts</span><strong>${summary.timeouts}</strong></article>
|
|
10308
|
-
</section>
|
|
10309
|
-
<section>
|
|
10310
|
-
<h2>LLM provider health</h2>
|
|
10311
|
-
${renderProviderCards("LLM", input.llmProviderHealth)}
|
|
10312
|
-
</section>
|
|
10313
|
-
<section>
|
|
10314
|
-
<h2>STT provider health</h2>
|
|
10315
|
-
${renderSimulationControls("stt", input.sttSimulation)}
|
|
10316
|
-
${renderProviderCards("STT", input.sttProviderHealth)}
|
|
10317
|
-
</section>
|
|
10318
|
-
<section>
|
|
10319
|
-
<h2>TTS provider health</h2>
|
|
10320
|
-
${renderSimulationControls("tts", input.ttsSimulation)}
|
|
10321
|
-
${renderProviderCards("TTS", input.ttsProviderHealth)}
|
|
10322
|
-
</section>
|
|
10323
|
-
<section>
|
|
10324
|
-
<h2>Routing timeline</h2>
|
|
10325
|
-
${renderTimeline2(input.routingEvents)}
|
|
10326
|
-
</section>
|
|
10327
|
-
</main>
|
|
10328
|
-
<script>
|
|
10329
|
-
const showResult = (panel, result) => {
|
|
10330
|
-
const output = panel.querySelector(".simulate-output");
|
|
10331
|
-
if (!output) return;
|
|
10332
|
-
output.hidden = false;
|
|
10333
|
-
output.textContent = JSON.stringify(result, null, 2);
|
|
10334
|
-
};
|
|
10335
|
-
document.querySelectorAll("[data-sim-prefix]").forEach((panel) => {
|
|
10336
|
-
const prefix = panel.getAttribute("data-sim-prefix");
|
|
10337
|
-
panel.querySelectorAll("[data-provider-fail]").forEach((button) => {
|
|
10338
|
-
button.addEventListener("click", async () => {
|
|
10339
|
-
const provider = button.getAttribute("data-provider-fail");
|
|
10340
|
-
const response = await fetch(prefix + "/failure?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
|
|
10341
|
-
showResult(panel, await response.json());
|
|
10342
|
-
if (response.ok) window.setTimeout(() => window.location.reload(), 450);
|
|
10343
|
-
});
|
|
10344
|
-
});
|
|
10345
|
-
panel.querySelectorAll("[data-provider-recover]").forEach((button) => {
|
|
10346
|
-
button.addEventListener("click", async () => {
|
|
10347
|
-
const provider = button.getAttribute("data-provider-recover");
|
|
10348
|
-
const response = await fetch(prefix + "/recovery?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
|
|
10349
|
-
showResult(panel, await response.json());
|
|
10350
|
-
if (response.ok) window.setTimeout(() => window.location.reload(), 450);
|
|
10351
|
-
});
|
|
10352
|
-
});
|
|
10353
|
-
});
|
|
10354
|
-
</script>
|
|
10355
|
-
</body>
|
|
10356
|
-
</html>`;
|
|
10626
|
+
const parts = content.parts;
|
|
10627
|
+
return Array.isArray(parts) ? parts : [];
|
|
10357
10628
|
};
|
|
10358
|
-
var
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
const provider = providerFromQuery(query.provider, simulation.providers);
|
|
10366
|
-
if (!provider) {
|
|
10367
|
-
set.status = 400;
|
|
10368
|
-
return {
|
|
10369
|
-
error: "Provider is not configured for simulation."
|
|
10370
|
-
};
|
|
10371
|
-
}
|
|
10372
|
-
if (simulation.failureProviders && !simulation.failureProviders.includes(provider)) {
|
|
10373
|
-
set.status = 400;
|
|
10374
|
-
return {
|
|
10375
|
-
error: `${provider} is not configured for failure simulation.`
|
|
10376
|
-
};
|
|
10629
|
+
var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
10630
|
+
`);
|
|
10631
|
+
var extractGeminiToolCalls = (response) => {
|
|
10632
|
+
const toolCalls = [];
|
|
10633
|
+
for (const part of extractGeminiCandidateParts(response)) {
|
|
10634
|
+
if (!part || typeof part !== "object") {
|
|
10635
|
+
continue;
|
|
10377
10636
|
}
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
error: simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} before simulating fallback.`
|
|
10382
|
-
};
|
|
10637
|
+
const functionCall = part.functionCall;
|
|
10638
|
+
if (!functionCall || typeof functionCall !== "object") {
|
|
10639
|
+
continue;
|
|
10383
10640
|
}
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
const provider = providerFromQuery(query.provider, simulation.providers);
|
|
10388
|
-
if (!provider) {
|
|
10389
|
-
set.status = 400;
|
|
10390
|
-
return {
|
|
10391
|
-
error: "Provider is not configured for simulation."
|
|
10392
|
-
};
|
|
10641
|
+
const record = functionCall;
|
|
10642
|
+
if (typeof record.name !== "string") {
|
|
10643
|
+
continue;
|
|
10393
10644
|
}
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
var createVoiceResilienceRoutes = (options) => {
|
|
10399
|
-
const path = options.path ?? "/resilience";
|
|
10400
|
-
const routes = new Elysia9({
|
|
10401
|
-
name: options.name ?? "absolutejs-voice-resilience"
|
|
10402
|
-
}).get(path, async () => {
|
|
10403
|
-
const events = await options.store.list();
|
|
10404
|
-
const sttEvents = events.filter((event) => event.payload.kind === "stt");
|
|
10405
|
-
const ttsEvents = events.filter((event) => event.payload.kind === "tts");
|
|
10406
|
-
const data = {
|
|
10407
|
-
links: options.links,
|
|
10408
|
-
llmProviderHealth: await summarizeVoiceProviderHealth({
|
|
10409
|
-
events,
|
|
10410
|
-
providers: options.llmProviders ?? []
|
|
10411
|
-
}),
|
|
10412
|
-
routingEvents: listVoiceRoutingEvents(events),
|
|
10413
|
-
sttProviderHealth: await summarizeVoiceProviderHealth({
|
|
10414
|
-
events: sttEvents,
|
|
10415
|
-
providers: options.sttProviders ?? []
|
|
10416
|
-
}),
|
|
10417
|
-
sttSimulation: options.sttSimulation,
|
|
10418
|
-
title: options.title,
|
|
10419
|
-
ttsProviderHealth: await summarizeVoiceProviderHealth({
|
|
10420
|
-
events: ttsEvents,
|
|
10421
|
-
providers: options.ttsProviders ?? []
|
|
10422
|
-
}),
|
|
10423
|
-
ttsSimulation: options.ttsSimulation
|
|
10424
|
-
};
|
|
10425
|
-
const body = await (options.render ?? renderVoiceResilienceHTML)(data);
|
|
10426
|
-
return new Response(body, {
|
|
10427
|
-
headers: {
|
|
10428
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
10429
|
-
...options.headers
|
|
10430
|
-
}
|
|
10645
|
+
toolCalls.push({
|
|
10646
|
+
args: record.args && typeof record.args === "object" ? record.args : {},
|
|
10647
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
10648
|
+
name: record.name
|
|
10431
10649
|
});
|
|
10432
|
-
});
|
|
10433
|
-
registerSimulationRoutes(routes, options.sttSimulation, "/api/stt-simulate");
|
|
10434
|
-
registerSimulationRoutes(routes, options.ttsSimulation, "/api/tts-simulate");
|
|
10435
|
-
return routes;
|
|
10436
|
-
};
|
|
10437
|
-
|
|
10438
|
-
// src/opsConsoleRoutes.ts
|
|
10439
|
-
var DEFAULT_LINKS = [
|
|
10440
|
-
{
|
|
10441
|
-
description: "Quality gates for CI, deploy checks, and production readiness.",
|
|
10442
|
-
href: "/quality",
|
|
10443
|
-
label: "Quality",
|
|
10444
|
-
statusHref: "/quality/status"
|
|
10445
|
-
},
|
|
10446
|
-
{
|
|
10447
|
-
description: "Replay stored sessions against acceptance gates over time.",
|
|
10448
|
-
href: "/evals",
|
|
10449
|
-
label: "Evals",
|
|
10450
|
-
statusHref: "/evals/status"
|
|
10451
|
-
},
|
|
10452
|
-
{
|
|
10453
|
-
description: "Provider health, fallback paths, and failure simulation.",
|
|
10454
|
-
href: "/resilience",
|
|
10455
|
-
label: "Resilience"
|
|
10456
|
-
},
|
|
10457
|
-
{
|
|
10458
|
-
description: "Redacted trace exports for debugging and support handoffs.",
|
|
10459
|
-
href: "/diagnostics",
|
|
10460
|
-
label: "Diagnostics"
|
|
10461
|
-
},
|
|
10462
|
-
{
|
|
10463
|
-
description: "Recent sessions with replay links.",
|
|
10464
|
-
href: "/sessions",
|
|
10465
|
-
label: "Sessions"
|
|
10466
|
-
},
|
|
10467
|
-
{
|
|
10468
|
-
description: "Transfer and webhook delivery health.",
|
|
10469
|
-
href: "/handoffs",
|
|
10470
|
-
label: "Handoffs"
|
|
10471
10650
|
}
|
|
10472
|
-
|
|
10473
|
-
var escapeHtml12 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10474
|
-
var countProviderStatuses = (providers) => {
|
|
10475
|
-
const degradedStatuses = new Set(["degraded", "rate-limited", "suppressed"]);
|
|
10476
|
-
const healthy = providers.filter((provider) => provider.status === "healthy").length;
|
|
10477
|
-
const degraded = providers.filter((provider) => degradedStatuses.has(provider.status)).length;
|
|
10478
|
-
return {
|
|
10479
|
-
degraded,
|
|
10480
|
-
healthy,
|
|
10481
|
-
total: providers.length
|
|
10482
|
-
};
|
|
10651
|
+
return toolCalls;
|
|
10483
10652
|
};
|
|
10484
|
-
var
|
|
10485
|
-
const
|
|
10486
|
-
const
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
providers: options.llmProviders
|
|
10490
|
-
}),
|
|
10491
|
-
...await summarizeVoiceProviderHealth({
|
|
10492
|
-
events,
|
|
10493
|
-
providers: options.sttProviders
|
|
10494
|
-
}),
|
|
10495
|
-
...await summarizeVoiceProviderHealth({
|
|
10496
|
-
events,
|
|
10497
|
-
providers: options.ttsProviders
|
|
10498
|
-
})
|
|
10499
|
-
];
|
|
10500
|
-
const handoffs = await summarizeVoiceHandoffHealth({ events });
|
|
10501
|
-
const sessions = await summarizeVoiceSessions({
|
|
10502
|
-
events,
|
|
10503
|
-
limit: 8,
|
|
10504
|
-
status: "all"
|
|
10505
|
-
});
|
|
10506
|
-
const quality = await evaluateVoiceQuality({ events });
|
|
10507
|
-
const routingEvents = listVoiceRoutingEvents(events).slice(0, 10);
|
|
10508
|
-
const trace = summarizeVoiceTrace(events);
|
|
10653
|
+
var createGeminiVoiceAssistantModel = (options) => {
|
|
10654
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
10655
|
+
const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
10656
|
+
const model = options.model ?? "gemini-2.5-flash";
|
|
10657
|
+
const maxRetries = Math.max(0, options.maxRetries ?? 2);
|
|
10509
10658
|
return {
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10659
|
+
generate: async (input) => {
|
|
10660
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
|
|
10661
|
+
let response;
|
|
10662
|
+
for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
|
|
10663
|
+
response = await fetchImpl(endpoint, {
|
|
10664
|
+
body: JSON.stringify({
|
|
10665
|
+
contents: input.messages.map(messageToGeminiContent).filter(Boolean),
|
|
10666
|
+
generationConfig: {
|
|
10667
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
10668
|
+
...input.tools.length ? {} : {
|
|
10669
|
+
responseMimeType: "application/json",
|
|
10670
|
+
responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
|
|
10671
|
+
},
|
|
10672
|
+
temperature: options.temperature
|
|
10673
|
+
},
|
|
10674
|
+
systemInstruction: {
|
|
10675
|
+
parts: [
|
|
10676
|
+
{
|
|
10677
|
+
text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
10678
|
+
|
|
10679
|
+
`)
|
|
10680
|
+
}
|
|
10681
|
+
]
|
|
10682
|
+
},
|
|
10683
|
+
tools: input.tools.length ? [
|
|
10684
|
+
{
|
|
10685
|
+
functionDeclarations: input.tools.map((tool) => ({
|
|
10686
|
+
description: tool.description,
|
|
10687
|
+
name: tool.name,
|
|
10688
|
+
parameters: toGeminiSchema(tool.parameters ?? {
|
|
10689
|
+
additionalProperties: true,
|
|
10690
|
+
type: "object"
|
|
10691
|
+
})
|
|
10692
|
+
}))
|
|
10693
|
+
}
|
|
10694
|
+
] : undefined
|
|
10695
|
+
}),
|
|
10696
|
+
headers: {
|
|
10697
|
+
"content-type": "application/json"
|
|
10698
|
+
},
|
|
10699
|
+
method: "POST"
|
|
10700
|
+
});
|
|
10701
|
+
if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
|
|
10702
|
+
break;
|
|
10703
|
+
}
|
|
10704
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
10705
|
+
await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
|
|
10552
10706
|
}
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10707
|
+
if (!response) {
|
|
10708
|
+
throw new Error("Gemini voice assistant model failed: no response");
|
|
10709
|
+
}
|
|
10710
|
+
if (!response.ok) {
|
|
10711
|
+
throw createHTTPError("Gemini", response);
|
|
10712
|
+
}
|
|
10713
|
+
const body = await response.json();
|
|
10714
|
+
if (body.usageMetadata && typeof body.usageMetadata === "object") {
|
|
10715
|
+
await options.onUsage?.(body.usageMetadata);
|
|
10716
|
+
}
|
|
10717
|
+
const toolCalls = extractGeminiToolCalls(body);
|
|
10718
|
+
if (toolCalls.length) {
|
|
10719
|
+
return {
|
|
10720
|
+
assistantText: extractGeminiText(body) || undefined,
|
|
10721
|
+
toolCalls
|
|
10722
|
+
};
|
|
10723
|
+
}
|
|
10724
|
+
return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
|
|
10725
|
+
}
|
|
10726
|
+
};
|
|
10557
10727
|
};
|
|
10558
10728
|
// src/providerAdapters.ts
|
|
10559
10729
|
class VoiceIOProviderTimeoutError extends Error {
|
|
@@ -11399,7 +11569,7 @@ var createVoiceMemoryStore = () => {
|
|
|
11399
11569
|
return { get, getOrCreate, list, remove, set };
|
|
11400
11570
|
};
|
|
11401
11571
|
// src/opsWebhook.ts
|
|
11402
|
-
import { Elysia as
|
|
11572
|
+
import { Elysia as Elysia12 } from "elysia";
|
|
11403
11573
|
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
11404
11574
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
11405
11575
|
const encoder = new TextEncoder;
|
|
@@ -11529,7 +11699,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
|
11529
11699
|
};
|
|
11530
11700
|
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
11531
11701
|
const path = options.path ?? "/api/voice-ops/webhook";
|
|
11532
|
-
return new
|
|
11702
|
+
return new Elysia12().post(path, async ({ body, request, set }) => {
|
|
11533
11703
|
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
11534
11704
|
if (options.signingSecret) {
|
|
11535
11705
|
const verification = await verifyVoiceOpsWebhookSignature({
|
|
@@ -13866,6 +14036,8 @@ export {
|
|
|
13866
14036
|
createVoiceAssistantHealthJSONHandler,
|
|
13867
14037
|
createVoiceAssistantHealthHTMLHandler,
|
|
13868
14038
|
createVoiceAssistant,
|
|
14039
|
+
createVoiceAppKitRoutes,
|
|
14040
|
+
createVoiceAppKit,
|
|
13869
14041
|
createVoiceAgentTool,
|
|
13870
14042
|
createVoiceAgentSquad,
|
|
13871
14043
|
createVoiceAgent,
|