@absolutejs/voice 0.0.22-beta.385 → 0.0.22-beta.387
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/client/index.js +4362 -3271
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2720 -2621
- package/dist/proofTrends.d.ts +37 -1
- package/dist/react/index.js +2020 -172
- package/dist/vue/index.js +1969 -121
- package/package.json +1 -1
package/dist/react/index.js
CHANGED
|
@@ -1494,7 +1494,1760 @@ var useVoiceProofTrends = (path = "/api/voice/proof-trends", options = {}) => {
|
|
|
1494
1494
|
};
|
|
1495
1495
|
|
|
1496
1496
|
// src/proofTrends.ts
|
|
1497
|
+
import { Elysia as Elysia4 } from "elysia";
|
|
1498
|
+
|
|
1499
|
+
// src/providerDecisionTraces.ts
|
|
1500
|
+
import { Elysia as Elysia3 } from "elysia";
|
|
1501
|
+
|
|
1502
|
+
// src/resilienceRoutes.ts
|
|
1503
|
+
import { Elysia as Elysia2 } from "elysia";
|
|
1504
|
+
|
|
1505
|
+
// src/providerHealth.ts
|
|
1497
1506
|
import { Elysia } from "elysia";
|
|
1507
|
+
var getString = (value) => typeof value === "string" ? value : undefined;
|
|
1508
|
+
var getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1509
|
+
var isProviderStatus = (value) => value === "success" || value === "fallback" || value === "error";
|
|
1510
|
+
var summarizeVoiceProviderHealth = async (input) => {
|
|
1511
|
+
const options = Array.isArray(input) ? { events: input } : input;
|
|
1512
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
1513
|
+
const providers = options.providers ?? [];
|
|
1514
|
+
const providerSet = new Set(providers);
|
|
1515
|
+
const now = options.now ?? Date.now();
|
|
1516
|
+
const entries = new Map;
|
|
1517
|
+
const isAllowedProvider = (value) => typeof value === "string" && (providerSet.size === 0 || providerSet.has(value));
|
|
1518
|
+
const getEntry = (provider) => {
|
|
1519
|
+
const existing = entries.get(provider);
|
|
1520
|
+
if (existing) {
|
|
1521
|
+
return existing;
|
|
1522
|
+
}
|
|
1523
|
+
const entry = {
|
|
1524
|
+
elapsedCount: 0,
|
|
1525
|
+
elapsedTotal: 0,
|
|
1526
|
+
errorCount: 0,
|
|
1527
|
+
fallbackCount: 0,
|
|
1528
|
+
provider,
|
|
1529
|
+
rateLimited: false,
|
|
1530
|
+
recommended: false,
|
|
1531
|
+
runCount: 0,
|
|
1532
|
+
status: "idle",
|
|
1533
|
+
timeoutCount: 0
|
|
1534
|
+
};
|
|
1535
|
+
entries.set(provider, entry);
|
|
1536
|
+
return entry;
|
|
1537
|
+
};
|
|
1538
|
+
for (const provider of providers) {
|
|
1539
|
+
getEntry(provider);
|
|
1540
|
+
}
|
|
1541
|
+
const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
|
|
1542
|
+
for (const event of events) {
|
|
1543
|
+
if (event.type === "assistant.run") {
|
|
1544
|
+
if (hasProviderRouterEvents) {
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
const provider2 = event.payload.variantId;
|
|
1548
|
+
if (!isAllowedProvider(provider2)) {
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
const entry2 = getEntry(provider2);
|
|
1552
|
+
entry2.runCount += 1;
|
|
1553
|
+
const elapsedMs = getNumber(event.payload.elapsedMs);
|
|
1554
|
+
if (elapsedMs !== undefined) {
|
|
1555
|
+
entry2.elapsedCount += 1;
|
|
1556
|
+
entry2.elapsedTotal += elapsedMs;
|
|
1557
|
+
}
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
if (event.type !== "session.error") {
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
const provider = event.payload.provider;
|
|
1564
|
+
if (!isAllowedProvider(provider)) {
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
1568
|
+
const applyProviderHealth = () => {
|
|
1569
|
+
const entry2 = getEntry(provider);
|
|
1570
|
+
const providerHealth = event.payload.providerHealth;
|
|
1571
|
+
if (providerHealth && typeof providerHealth === "object") {
|
|
1572
|
+
const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
|
|
1573
|
+
if (suppressedUntil2 !== undefined) {
|
|
1574
|
+
entry2.suppressedUntil = suppressedUntil2;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
const suppressedUntil = getNumber(event.payload.suppressedUntil);
|
|
1578
|
+
if (suppressedUntil !== undefined) {
|
|
1579
|
+
entry2.suppressedUntil = suppressedUntil;
|
|
1580
|
+
}
|
|
1581
|
+
const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
|
|
1582
|
+
if (suppressionRemainingMs !== undefined) {
|
|
1583
|
+
entry2.suppressionRemainingMs = suppressionRemainingMs;
|
|
1584
|
+
}
|
|
1585
|
+
return entry2;
|
|
1586
|
+
};
|
|
1587
|
+
if (providerStatus === "success" || providerStatus === "fallback") {
|
|
1588
|
+
const entry2 = applyProviderHealth();
|
|
1589
|
+
entry2.runCount += 1;
|
|
1590
|
+
entry2.lastSuccessAt = event.at;
|
|
1591
|
+
if (providerStatus === "success") {
|
|
1592
|
+
entry2.lastError = undefined;
|
|
1593
|
+
entry2.rateLimited = false;
|
|
1594
|
+
entry2.suppressedUntil = undefined;
|
|
1595
|
+
entry2.suppressionRemainingMs = undefined;
|
|
1596
|
+
}
|
|
1597
|
+
const elapsedMs = getNumber(event.payload.elapsedMs);
|
|
1598
|
+
if (elapsedMs !== undefined) {
|
|
1599
|
+
entry2.elapsedCount += 1;
|
|
1600
|
+
entry2.elapsedTotal += elapsedMs;
|
|
1601
|
+
}
|
|
1602
|
+
const selectedProvider = event.payload.selectedProvider;
|
|
1603
|
+
if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
|
|
1604
|
+
getEntry(selectedProvider).fallbackCount += 1;
|
|
1605
|
+
}
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
const entry = applyProviderHealth();
|
|
1609
|
+
entry.errorCount += 1;
|
|
1610
|
+
if (event.payload.timedOut === true) {
|
|
1611
|
+
entry.timeoutCount += 1;
|
|
1612
|
+
}
|
|
1613
|
+
entry.lastError = getString(event.payload.error);
|
|
1614
|
+
entry.lastErrorAt = event.at;
|
|
1615
|
+
entry.rateLimited ||= event.payload.rateLimited === true;
|
|
1616
|
+
}
|
|
1617
|
+
const summaries = [...entries.values()].map((entry) => {
|
|
1618
|
+
const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
|
|
1619
|
+
const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
|
|
1620
|
+
const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
|
|
1621
|
+
const recoverable = hadSuppression && !activeSuppression;
|
|
1622
|
+
const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
|
|
1623
|
+
const status = activeSuppression ? "suppressed" : recoverable ? "recoverable" : entry.rateLimited ? "rate-limited" : entry.errorCount > 0 && (!entry.lastSuccessAt || !entry.lastErrorAt || entry.lastErrorAt > entry.lastSuccessAt) ? "degraded" : entry.runCount > 0 ? "healthy" : "idle";
|
|
1624
|
+
return {
|
|
1625
|
+
averageElapsedMs,
|
|
1626
|
+
errorCount: entry.errorCount,
|
|
1627
|
+
fallbackCount: entry.fallbackCount,
|
|
1628
|
+
lastError: entry.lastError,
|
|
1629
|
+
lastErrorAt: entry.lastErrorAt,
|
|
1630
|
+
lastSuccessAt: entry.lastSuccessAt,
|
|
1631
|
+
provider: entry.provider,
|
|
1632
|
+
rateLimited: entry.rateLimited,
|
|
1633
|
+
recommended: false,
|
|
1634
|
+
runCount: entry.runCount,
|
|
1635
|
+
status,
|
|
1636
|
+
suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
|
|
1637
|
+
suppressedUntil: entry.suppressedUntil,
|
|
1638
|
+
timeoutCount: entry.timeoutCount
|
|
1639
|
+
};
|
|
1640
|
+
});
|
|
1641
|
+
const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
|
|
1642
|
+
if (recommended) {
|
|
1643
|
+
recommended.recommended = true;
|
|
1644
|
+
}
|
|
1645
|
+
return summaries;
|
|
1646
|
+
};
|
|
1647
|
+
var escapeHtml5 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1648
|
+
var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
|
|
1649
|
+
'<div class="voice-provider-health">',
|
|
1650
|
+
...providers.map((provider) => {
|
|
1651
|
+
const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
|
|
1652
|
+
return [
|
|
1653
|
+
`<article class="voice-provider-card ${escapeHtml5(provider.status)}">`,
|
|
1654
|
+
'<div class="voice-provider-card-header">',
|
|
1655
|
+
`<strong>${escapeHtml5(provider.provider)}</strong>`,
|
|
1656
|
+
`<span>${escapeHtml5(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
|
|
1657
|
+
"</div>",
|
|
1658
|
+
"<dl>",
|
|
1659
|
+
`<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
|
|
1660
|
+
`<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
|
|
1661
|
+
`<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
|
|
1662
|
+
`<div><dt>Timeouts</dt><dd>${String(provider.timeoutCount)}</dd></div>`,
|
|
1663
|
+
`<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
|
|
1664
|
+
"</dl>",
|
|
1665
|
+
suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
|
|
1666
|
+
provider.lastError ? `<p>${escapeHtml5(provider.lastError)}</p>` : "",
|
|
1667
|
+
"</article>"
|
|
1668
|
+
].join("");
|
|
1669
|
+
}),
|
|
1670
|
+
"</div>"
|
|
1671
|
+
].join("");
|
|
1672
|
+
var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
|
|
1673
|
+
var createVoiceProviderHealthHTMLHandler = (options) => async () => {
|
|
1674
|
+
const providers = await summarizeVoiceProviderHealth(options);
|
|
1675
|
+
const render = options.render ?? renderVoiceProviderHealthHTML;
|
|
1676
|
+
const body = await render(providers);
|
|
1677
|
+
return new Response(body, {
|
|
1678
|
+
headers: {
|
|
1679
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1680
|
+
...options.headers
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
};
|
|
1684
|
+
var createVoiceProviderHealthRoutes = (options) => {
|
|
1685
|
+
const path = options.path ?? "/api/provider-status";
|
|
1686
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
1687
|
+
const routes = new Elysia({
|
|
1688
|
+
name: options.name ?? "absolutejs-voice-provider-health"
|
|
1689
|
+
}).get(path, createVoiceProviderHealthJSONHandler(options));
|
|
1690
|
+
if (htmlPath) {
|
|
1691
|
+
routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
|
|
1692
|
+
}
|
|
1693
|
+
return routes;
|
|
1694
|
+
};
|
|
1695
|
+
|
|
1696
|
+
// src/resilienceRoutes.ts
|
|
1697
|
+
var escapeHtml6 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1698
|
+
var getString2 = (value) => typeof value === "string" ? value : undefined;
|
|
1699
|
+
var getNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1700
|
+
var getBoolean = (value) => value === true;
|
|
1701
|
+
var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
|
|
1702
|
+
var listVoiceRoutingEvents = (events) => {
|
|
1703
|
+
const routingEvents = [];
|
|
1704
|
+
for (const event of events) {
|
|
1705
|
+
if (event.type !== "session.error") {
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
const provider = getString2(event.payload.provider);
|
|
1709
|
+
const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
1710
|
+
if (!provider || !providerStatus) {
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
const kind = getString2(event.payload.kind);
|
|
1714
|
+
routingEvents.push({
|
|
1715
|
+
at: event.at,
|
|
1716
|
+
attempt: getNumber2(event.payload.attempt),
|
|
1717
|
+
elapsedMs: getNumber2(event.payload.elapsedMs),
|
|
1718
|
+
error: getString2(event.payload.error),
|
|
1719
|
+
fallbackProvider: getString2(event.payload.fallbackProvider),
|
|
1720
|
+
kind: kind === "stt" || kind === "tts" ? kind : "llm",
|
|
1721
|
+
latencyBudgetMs: getNumber2(event.payload.latencyBudgetMs),
|
|
1722
|
+
operation: getString2(event.payload.operation),
|
|
1723
|
+
provider,
|
|
1724
|
+
routing: getString2(event.payload.routing),
|
|
1725
|
+
scenarioId: event.scenarioId,
|
|
1726
|
+
selectedProvider: getString2(event.payload.selectedProvider),
|
|
1727
|
+
sessionId: event.sessionId,
|
|
1728
|
+
status: providerStatus,
|
|
1729
|
+
suppressionRemainingMs: getNumber2(event.payload.suppressionRemainingMs),
|
|
1730
|
+
timedOut: getBoolean(event.payload.timedOut),
|
|
1731
|
+
turnId: event.turnId
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
return routingEvents.sort((left, right) => right.at - left.at);
|
|
1735
|
+
};
|
|
1736
|
+
var summarizeVoiceRoutingDecision = (events, options = {}) => {
|
|
1737
|
+
const routingEvents = listVoiceRoutingEvents(events).filter((event) => {
|
|
1738
|
+
if (options.kind && event.kind !== options.kind) {
|
|
1739
|
+
return false;
|
|
1740
|
+
}
|
|
1741
|
+
if (options.sessionId && event.sessionId !== options.sessionId) {
|
|
1742
|
+
return false;
|
|
1743
|
+
}
|
|
1744
|
+
return true;
|
|
1745
|
+
});
|
|
1746
|
+
const limited = typeof options.limit === "number" && options.limit >= 0 ? routingEvents.slice(0, options.limit) : routingEvents;
|
|
1747
|
+
return limited[0] ?? null;
|
|
1748
|
+
};
|
|
1749
|
+
var createEmptyKindSummary = () => ({
|
|
1750
|
+
errorCount: 0,
|
|
1751
|
+
fallbackCount: 0,
|
|
1752
|
+
providers: [],
|
|
1753
|
+
runCount: 0,
|
|
1754
|
+
timeoutCount: 0
|
|
1755
|
+
});
|
|
1756
|
+
var summarizeVoiceRoutingSessions = (events, options = {}) => {
|
|
1757
|
+
const routingEvents = (events.some((event) => ("payload" in event)) ? listVoiceRoutingEvents(events) : [...events]).filter((event) => !options.sessionId || event.sessionId === options.sessionId);
|
|
1758
|
+
const sessions = new Map;
|
|
1759
|
+
for (const event of routingEvents) {
|
|
1760
|
+
const existing = sessions.get(event.sessionId);
|
|
1761
|
+
const summary = existing ?? {
|
|
1762
|
+
errorCount: 0,
|
|
1763
|
+
eventCount: 0,
|
|
1764
|
+
fallbackCount: 0,
|
|
1765
|
+
kinds: {
|
|
1766
|
+
llm: createEmptyKindSummary(),
|
|
1767
|
+
stt: createEmptyKindSummary(),
|
|
1768
|
+
tts: createEmptyKindSummary()
|
|
1769
|
+
},
|
|
1770
|
+
lastEventAt: event.at,
|
|
1771
|
+
sessionId: event.sessionId,
|
|
1772
|
+
startedAt: event.at,
|
|
1773
|
+
status: "healthy",
|
|
1774
|
+
timeoutCount: 0
|
|
1775
|
+
};
|
|
1776
|
+
summary.eventCount += 1;
|
|
1777
|
+
summary.startedAt = Math.min(summary.startedAt, event.at);
|
|
1778
|
+
summary.lastEventAt = Math.max(summary.lastEventAt, event.at);
|
|
1779
|
+
if (event.status === "error") {
|
|
1780
|
+
summary.errorCount += 1;
|
|
1781
|
+
}
|
|
1782
|
+
if (event.status === "fallback") {
|
|
1783
|
+
summary.fallbackCount += 1;
|
|
1784
|
+
}
|
|
1785
|
+
if (event.timedOut) {
|
|
1786
|
+
summary.timeoutCount += 1;
|
|
1787
|
+
}
|
|
1788
|
+
const kind = summary.kinds[event.kind];
|
|
1789
|
+
kind.runCount += 1;
|
|
1790
|
+
if (event.status === "error") {
|
|
1791
|
+
kind.errorCount += 1;
|
|
1792
|
+
}
|
|
1793
|
+
if (event.status === "fallback") {
|
|
1794
|
+
kind.fallbackCount += 1;
|
|
1795
|
+
}
|
|
1796
|
+
if (event.timedOut) {
|
|
1797
|
+
kind.timeoutCount += 1;
|
|
1798
|
+
}
|
|
1799
|
+
if (event.provider && !kind.providers.includes(event.provider)) {
|
|
1800
|
+
kind.providers.push(event.provider);
|
|
1801
|
+
}
|
|
1802
|
+
if (!kind.latest || event.at > kind.latest.at) {
|
|
1803
|
+
kind.latest = event;
|
|
1804
|
+
}
|
|
1805
|
+
summary.status = summary.errorCount > 0 || summary.timeoutCount > 0 ? "degraded" : summary.fallbackCount > 0 ? "fallback" : "healthy";
|
|
1806
|
+
sessions.set(event.sessionId, summary);
|
|
1807
|
+
}
|
|
1808
|
+
const sorted = [...sessions.values()].sort((left, right) => right.lastEventAt - left.lastEventAt);
|
|
1809
|
+
return typeof options.limit === "number" && options.limit >= 0 ? sorted.slice(0, options.limit) : sorted;
|
|
1810
|
+
};
|
|
1811
|
+
var createVoiceRoutingDecisionSummary = async (options) => {
|
|
1812
|
+
const events = await options.store.list({
|
|
1813
|
+
sessionId: options.sessionId,
|
|
1814
|
+
type: "session.error"
|
|
1815
|
+
});
|
|
1816
|
+
return summarizeVoiceRoutingDecision(events, options);
|
|
1817
|
+
};
|
|
1818
|
+
var summarizeRoutingEvents = (events) => {
|
|
1819
|
+
const byKind = new Map;
|
|
1820
|
+
let errors = 0;
|
|
1821
|
+
let fallbacks = 0;
|
|
1822
|
+
let timeouts = 0;
|
|
1823
|
+
for (const event of events) {
|
|
1824
|
+
byKind.set(event.kind, (byKind.get(event.kind) ?? 0) + 1);
|
|
1825
|
+
if (event.status === "error") {
|
|
1826
|
+
errors += 1;
|
|
1827
|
+
}
|
|
1828
|
+
if (event.status === "fallback") {
|
|
1829
|
+
fallbacks += 1;
|
|
1830
|
+
}
|
|
1831
|
+
if (event.timedOut) {
|
|
1832
|
+
timeouts += 1;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return {
|
|
1836
|
+
byKind,
|
|
1837
|
+
errors,
|
|
1838
|
+
fallbacks,
|
|
1839
|
+
timeouts,
|
|
1840
|
+
total: events.length
|
|
1841
|
+
};
|
|
1842
|
+
};
|
|
1843
|
+
var renderProviderCards = (title, providers) => {
|
|
1844
|
+
if (providers.length === 0) {
|
|
1845
|
+
return `<p class="muted">No ${escapeHtml6(title)} provider health yet.</p>`;
|
|
1846
|
+
}
|
|
1847
|
+
return `<div class="provider-grid">${providers.map((provider) => `
|
|
1848
|
+
<article class="card provider ${escapeHtml6(provider.status)}">
|
|
1849
|
+
<div class="card-header">
|
|
1850
|
+
<strong>${escapeHtml6(provider.provider)}</strong>
|
|
1851
|
+
<span>${escapeHtml6(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
|
|
1852
|
+
</div>
|
|
1853
|
+
<dl>
|
|
1854
|
+
<div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
|
|
1855
|
+
<div><dt>Avg latency</dt><dd>${provider.averageElapsedMs ?? 0}ms</dd></div>
|
|
1856
|
+
<div><dt>Errors</dt><dd>${provider.errorCount}</dd></div>
|
|
1857
|
+
<div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
|
|
1858
|
+
<div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
|
|
1859
|
+
</dl>
|
|
1860
|
+
${provider.lastError ? `<p class="muted">${escapeHtml6(provider.lastError)}</p>` : ""}
|
|
1861
|
+
</article>
|
|
1862
|
+
`).join("")}</div>`;
|
|
1863
|
+
};
|
|
1864
|
+
var renderTimeline = (events) => {
|
|
1865
|
+
if (events.length === 0) {
|
|
1866
|
+
return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
|
|
1867
|
+
}
|
|
1868
|
+
return `<div class="timeline">${events.slice(0, 40).map((event) => `
|
|
1869
|
+
<article class="card event ${escapeHtml6(event.status ?? "unknown")}">
|
|
1870
|
+
<div class="card-header">
|
|
1871
|
+
<strong>${escapeHtml6(event.kind.toUpperCase())} ${escapeHtml6(event.operation ?? "generate")}</strong>
|
|
1872
|
+
<span>${new Date(event.at).toLocaleString()}</span>
|
|
1873
|
+
</div>
|
|
1874
|
+
<p>
|
|
1875
|
+
<span class="pill">${escapeHtml6(event.status ?? "unknown")}</span>
|
|
1876
|
+
<span class="pill">provider: ${escapeHtml6(event.provider ?? "unknown")}</span>
|
|
1877
|
+
${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml6(event.fallbackProvider)}</span>` : ""}
|
|
1878
|
+
${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
|
|
1879
|
+
</p>
|
|
1880
|
+
<dl>
|
|
1881
|
+
<div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
|
|
1882
|
+
<div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
|
|
1883
|
+
<div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
|
|
1884
|
+
<div><dt>Session</dt><dd>${escapeHtml6(event.sessionId)}</dd></div>
|
|
1885
|
+
</dl>
|
|
1886
|
+
${event.error ? `<p class="muted">${escapeHtml6(event.error)}</p>` : ""}
|
|
1887
|
+
</article>
|
|
1888
|
+
`).join("")}</div>`;
|
|
1889
|
+
};
|
|
1890
|
+
var renderSessionKind = (kind, summary) => {
|
|
1891
|
+
const latest = summary.latest;
|
|
1892
|
+
const provider = latest?.provider ?? summary.providers[0] ?? "none";
|
|
1893
|
+
const status = latest?.status ?? "idle";
|
|
1894
|
+
const fallback = latest?.fallbackProvider && latest.fallbackProvider !== provider ? ` -> ${latest.fallbackProvider}` : "";
|
|
1895
|
+
return `<div>
|
|
1896
|
+
<dt>${escapeHtml6(kind.toUpperCase())}</dt>
|
|
1897
|
+
<dd>${escapeHtml6(provider)}${escapeHtml6(fallback)}</dd>
|
|
1898
|
+
<small>${escapeHtml6(status)} \xB7 ${summary.runCount} event${summary.runCount === 1 ? "" : "s"} \xB7 ${summary.errorCount} error${summary.errorCount === 1 ? "" : "s"} \xB7 ${summary.fallbackCount} fallback${summary.fallbackCount === 1 ? "" : "s"}</small>
|
|
1899
|
+
</div>`;
|
|
1900
|
+
};
|
|
1901
|
+
var renderSessionSummaries = (sessions) => {
|
|
1902
|
+
if (sessions.length === 0) {
|
|
1903
|
+
return '<p class="muted">No call-level routing summaries yet. Run a voice session or provider simulation.</p>';
|
|
1904
|
+
}
|
|
1905
|
+
return `<div class="session-grid">${sessions.slice(0, 12).map((session) => `
|
|
1906
|
+
<article class="card session ${escapeHtml6(session.status)}">
|
|
1907
|
+
<div class="card-header">
|
|
1908
|
+
<strong>${escapeHtml6(session.sessionId)}</strong>
|
|
1909
|
+
<span>${escapeHtml6(session.status)}</span>
|
|
1910
|
+
</div>
|
|
1911
|
+
<p>
|
|
1912
|
+
<span class="pill">${session.eventCount} routing events</span>
|
|
1913
|
+
<span class="pill">${session.fallbackCount} fallbacks</span>
|
|
1914
|
+
<span class="pill">${session.errorCount} errors</span>
|
|
1915
|
+
<span class="pill">${session.timeoutCount} timeouts</span>
|
|
1916
|
+
</p>
|
|
1917
|
+
<dl>
|
|
1918
|
+
${renderSessionKind("llm", session.kinds.llm)}
|
|
1919
|
+
${renderSessionKind("stt", session.kinds.stt)}
|
|
1920
|
+
${renderSessionKind("tts", session.kinds.tts)}
|
|
1921
|
+
</dl>
|
|
1922
|
+
</article>
|
|
1923
|
+
`).join("")}</div>`;
|
|
1924
|
+
};
|
|
1925
|
+
var renderSimulationControls = (kind, simulation) => {
|
|
1926
|
+
if (!simulation) {
|
|
1927
|
+
return "";
|
|
1928
|
+
}
|
|
1929
|
+
const configuredProviders = simulation.providers.filter((provider) => provider.configured !== false);
|
|
1930
|
+
if (configuredProviders.length === 0) {
|
|
1931
|
+
return `<p class="muted">No ${kind.toUpperCase()} providers are configured for simulation.</p>`;
|
|
1932
|
+
}
|
|
1933
|
+
const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
|
|
1934
|
+
const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
|
|
1935
|
+
const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
|
|
1936
|
+
return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml6(pathPrefix)}">
|
|
1937
|
+
<p class="muted">${escapeHtml6(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
|
|
1938
|
+
<div class="simulate-actions">
|
|
1939
|
+
${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml6(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml6(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
|
|
1940
|
+
${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml6(provider.provider)}">Mark ${escapeHtml6(provider.provider)} recovered</button>`).join("")}
|
|
1941
|
+
</div>
|
|
1942
|
+
${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml6(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
|
|
1943
|
+
<pre class="simulate-output" hidden></pre>
|
|
1944
|
+
</div>`;
|
|
1945
|
+
};
|
|
1946
|
+
var renderVoiceResilienceHTML = (input) => {
|
|
1947
|
+
const summary = summarizeRoutingEvents(input.routingEvents);
|
|
1948
|
+
const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml6(kind)}: ${String(count)}</span>`).join("");
|
|
1949
|
+
const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml6(link.href)}">${escapeHtml6(link.label)}</a>`).join(" \xB7 ") : "";
|
|
1950
|
+
const snippet = escapeHtml6(`const sttSimulator = createVoiceIOProviderFailureSimulator({
|
|
1951
|
+
kind: 'stt',
|
|
1952
|
+
providers: ['deepgram', 'assemblyai'],
|
|
1953
|
+
fallback: ['deepgram', 'assemblyai'],
|
|
1954
|
+
onProviderEvent: async (event, input) => {
|
|
1955
|
+
await traceStore.append({
|
|
1956
|
+
at: event.at,
|
|
1957
|
+
payload: { ...event, providerStatus: event.status },
|
|
1958
|
+
sessionId: input.sessionId,
|
|
1959
|
+
type: 'session.error'
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
app.use(
|
|
1965
|
+
createVoiceResilienceRoutes({
|
|
1966
|
+
store: traceStore,
|
|
1967
|
+
sttProviders: ['deepgram', 'assemblyai'],
|
|
1968
|
+
sttSimulation: {
|
|
1969
|
+
failureProviders: ['deepgram'],
|
|
1970
|
+
fallbackRequiredProvider: 'assemblyai',
|
|
1971
|
+
providers: [{ provider: 'deepgram' }, { provider: 'assemblyai' }],
|
|
1972
|
+
run: sttSimulator.run
|
|
1973
|
+
}
|
|
1974
|
+
})
|
|
1975
|
+
);
|
|
1976
|
+
|
|
1977
|
+
app.use(
|
|
1978
|
+
createVoiceProductionReadinessRoutes({
|
|
1979
|
+
links: { resilience: '/resilience' },
|
|
1980
|
+
store: traceStore
|
|
1981
|
+
})
|
|
1982
|
+
);`);
|
|
1983
|
+
return `<!doctype html>
|
|
1984
|
+
<html lang="en">
|
|
1985
|
+
<head>
|
|
1986
|
+
<meta charset="utf-8" />
|
|
1987
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1988
|
+
<title>${escapeHtml6(input.title ?? "AbsoluteJS Voice Resilience")}</title>
|
|
1989
|
+
<style>
|
|
1990
|
+
:root { color-scheme: dark; }
|
|
1991
|
+
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; }
|
|
1992
|
+
main { display: grid; gap: 16px; margin: 0 auto; max-width: 1180px; }
|
|
1993
|
+
section, .card { background: rgba(19, 22, 27, 0.92); border: 1px solid #27272a; border-radius: 20px; padding: 20px; }
|
|
1994
|
+
.hero { background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(245, 158, 11, 0.12)); }
|
|
1995
|
+
.grid, .provider-grid { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
1996
|
+
.session-grid { display: grid; gap: 14px; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1997
|
+
.timeline { display: grid; gap: 12px; }
|
|
1998
|
+
.card-header { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
|
|
1999
|
+
.card-header strong { font-size: 1.05rem; }
|
|
2000
|
+
.metric strong { display: block; font-size: 2rem; margin-top: 6px; }
|
|
2001
|
+
.muted, dt, span { color: #a1a1aa; }
|
|
2002
|
+
dl { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
2003
|
+
dl div { background: #0f1217; border: 1px solid #27272a; border-radius: 12px; padding: 10px; }
|
|
2004
|
+
dd { font-weight: 800; margin: 4px 0 0; }
|
|
2005
|
+
.pill { background: #0f1217; border: 1px solid #3f3f46; border-radius: 999px; color: #d4d4d8; display: inline-flex; margin: 3px 4px 3px 0; padding: 5px 9px; }
|
|
2006
|
+
.danger { border-color: rgba(239, 68, 68, 0.75); color: #fecaca; }
|
|
2007
|
+
.event.error { border-color: rgba(239, 68, 68, 0.7); }
|
|
2008
|
+
.event.fallback, .session.fallback { border-color: rgba(245, 158, 11, 0.7); }
|
|
2009
|
+
.event.success, .provider.healthy, .session.healthy { border-color: rgba(34, 197, 94, 0.5); }
|
|
2010
|
+
.session.degraded { border-color: rgba(239, 68, 68, 0.7); }
|
|
2011
|
+
.provider.suppressed, .provider.degraded, .provider.rate-limited { border-color: rgba(239, 68, 68, 0.7); }
|
|
2012
|
+
.provider.recoverable { border-color: rgba(59, 130, 246, 0.7); }
|
|
2013
|
+
button { background: #f59e0b; border: 0; border-radius: 999px; color: #111827; cursor: pointer; font-weight: 800; padding: 10px 14px; }
|
|
2014
|
+
button:disabled { cursor: not-allowed; opacity: 0.45; }
|
|
2015
|
+
.simulate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
|
|
2016
|
+
.simulate-output { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #d4d4d8; overflow: auto; padding: 12px; white-space: pre-wrap; }
|
|
2017
|
+
.primitive { border-color: rgba(245, 158, 11, 0.45); }
|
|
2018
|
+
.primitive pre { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #fef3c7; overflow: auto; padding: 14px; }
|
|
2019
|
+
.primitive code { color: #fef3c7; }
|
|
2020
|
+
a { color: #f59e0b; }
|
|
2021
|
+
@media (max-width: 850px) { .grid, .provider-grid, .session-grid, dl { grid-template-columns: 1fr; } }
|
|
2022
|
+
</style>
|
|
2023
|
+
</head>
|
|
2024
|
+
<body>
|
|
2025
|
+
<main>
|
|
2026
|
+
<section class="hero">
|
|
2027
|
+
<h1>Provider routing and resilience</h1>
|
|
2028
|
+
<p>One view for the production reliability story: LLM failover, STT/TTS routing, latency budgets, timeouts, and fallback decisions.</p>
|
|
2029
|
+
${links ? `<p>${links}</p>` : ""}
|
|
2030
|
+
<p>${kindCounts || '<span class="pill">No routing events yet</span>'}</p>
|
|
2031
|
+
</section>
|
|
2032
|
+
<section class="primitive">
|
|
2033
|
+
<p class="muted">Copy into your app</p>
|
|
2034
|
+
<h2><code>createVoiceResilienceRoutes(...)</code> builds this failover proof surface</h2>
|
|
2035
|
+
<p class="muted">Mount one route group for provider health, routing traces, and failure simulation. Feed the same trace store into production readiness so unresolved provider errors fail the deploy gate while recovered fallback stays visible.</p>
|
|
2036
|
+
<pre><code>${snippet}</code></pre>
|
|
2037
|
+
</section>
|
|
2038
|
+
<section class="grid">
|
|
2039
|
+
<article class="card metric"><span>Total routing events</span><strong>${summary.total}</strong></article>
|
|
2040
|
+
<article class="card metric"><span>Fallbacks</span><strong>${summary.fallbacks}</strong></article>
|
|
2041
|
+
<article class="card metric"><span>Errors</span><strong>${summary.errors}</strong></article>
|
|
2042
|
+
<article class="card metric"><span>Timeouts</span><strong>${summary.timeouts}</strong></article>
|
|
2043
|
+
</section>
|
|
2044
|
+
<section>
|
|
2045
|
+
<h2>Call-level routing summaries</h2>
|
|
2046
|
+
<p class="muted">A compact per-call view of which LLM, STT, and TTS providers handled the session, including fallback and timeout counts.</p>
|
|
2047
|
+
${renderSessionSummaries(input.routingSessions)}
|
|
2048
|
+
</section>
|
|
2049
|
+
<section>
|
|
2050
|
+
<h2>LLM provider health</h2>
|
|
2051
|
+
${renderProviderCards("LLM", input.llmProviderHealth)}
|
|
2052
|
+
</section>
|
|
2053
|
+
<section>
|
|
2054
|
+
<h2>STT provider health</h2>
|
|
2055
|
+
${renderSimulationControls("stt", input.sttSimulation)}
|
|
2056
|
+
${renderProviderCards("STT", input.sttProviderHealth)}
|
|
2057
|
+
</section>
|
|
2058
|
+
<section>
|
|
2059
|
+
<h2>TTS provider health</h2>
|
|
2060
|
+
${renderSimulationControls("tts", input.ttsSimulation)}
|
|
2061
|
+
${renderProviderCards("TTS", input.ttsProviderHealth)}
|
|
2062
|
+
</section>
|
|
2063
|
+
<section>
|
|
2064
|
+
<h2>Routing timeline</h2>
|
|
2065
|
+
${renderTimeline(input.routingEvents)}
|
|
2066
|
+
</section>
|
|
2067
|
+
</main>
|
|
2068
|
+
<script>
|
|
2069
|
+
const showResult = (panel, result) => {
|
|
2070
|
+
const output = panel.querySelector(".simulate-output");
|
|
2071
|
+
if (!output) return;
|
|
2072
|
+
output.hidden = false;
|
|
2073
|
+
output.textContent = JSON.stringify(result, null, 2);
|
|
2074
|
+
};
|
|
2075
|
+
document.querySelectorAll("[data-sim-prefix]").forEach((panel) => {
|
|
2076
|
+
const prefix = panel.getAttribute("data-sim-prefix");
|
|
2077
|
+
panel.querySelectorAll("[data-provider-fail]").forEach((button) => {
|
|
2078
|
+
button.addEventListener("click", async () => {
|
|
2079
|
+
const provider = button.getAttribute("data-provider-fail");
|
|
2080
|
+
const response = await fetch(prefix + "/failure?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
|
|
2081
|
+
showResult(panel, await response.json());
|
|
2082
|
+
if (response.ok) window.setTimeout(() => window.location.reload(), 450);
|
|
2083
|
+
});
|
|
2084
|
+
});
|
|
2085
|
+
panel.querySelectorAll("[data-provider-recover]").forEach((button) => {
|
|
2086
|
+
button.addEventListener("click", async () => {
|
|
2087
|
+
const provider = button.getAttribute("data-provider-recover");
|
|
2088
|
+
const response = await fetch(prefix + "/recovery?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
|
|
2089
|
+
showResult(panel, await response.json());
|
|
2090
|
+
if (response.ok) window.setTimeout(() => window.location.reload(), 450);
|
|
2091
|
+
});
|
|
2092
|
+
});
|
|
2093
|
+
});
|
|
2094
|
+
</script>
|
|
2095
|
+
</body>
|
|
2096
|
+
</html>`;
|
|
2097
|
+
};
|
|
2098
|
+
var providerFromQuery = (value, providers) => typeof value === "string" && providers.some((provider) => provider.provider === value && provider.configured !== false) ? value : undefined;
|
|
2099
|
+
var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
|
|
2100
|
+
if (!simulation) {
|
|
2101
|
+
return routes;
|
|
2102
|
+
}
|
|
2103
|
+
const pathPrefix = simulation.pathPrefix ?? defaultPathPrefix;
|
|
2104
|
+
routes.post(`${pathPrefix}/failure`, async ({ query, set }) => {
|
|
2105
|
+
const provider = providerFromQuery(query.provider, simulation.providers);
|
|
2106
|
+
if (!provider) {
|
|
2107
|
+
set.status = 400;
|
|
2108
|
+
return {
|
|
2109
|
+
error: "Provider is not configured for simulation."
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
if (simulation.failureProviders && !simulation.failureProviders.includes(provider)) {
|
|
2113
|
+
set.status = 400;
|
|
2114
|
+
return {
|
|
2115
|
+
error: `${provider} is not configured for failure simulation.`
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
if (simulation.fallbackRequiredProvider && !simulation.providers.some((entry) => entry.provider === simulation.fallbackRequiredProvider && entry.configured !== false)) {
|
|
2119
|
+
set.status = 400;
|
|
2120
|
+
return {
|
|
2121
|
+
error: simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} before simulating fallback.`
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
return simulation.run(provider, "failure");
|
|
2125
|
+
});
|
|
2126
|
+
routes.post(`${pathPrefix}/recovery`, async ({ query, set }) => {
|
|
2127
|
+
const provider = providerFromQuery(query.provider, simulation.providers);
|
|
2128
|
+
if (!provider) {
|
|
2129
|
+
set.status = 400;
|
|
2130
|
+
return {
|
|
2131
|
+
error: "Provider is not configured for simulation."
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
return simulation.run(provider, "recovery");
|
|
2135
|
+
});
|
|
2136
|
+
return routes;
|
|
2137
|
+
};
|
|
2138
|
+
var createVoiceResilienceRoutes = (options) => {
|
|
2139
|
+
const path = options.path ?? "/resilience";
|
|
2140
|
+
const routes = new Elysia2({
|
|
2141
|
+
name: options.name ?? "absolutejs-voice-resilience"
|
|
2142
|
+
}).get(path, async () => {
|
|
2143
|
+
const events = await options.store.list();
|
|
2144
|
+
const sttEvents = events.filter((event) => event.payload.kind === "stt");
|
|
2145
|
+
const ttsEvents = events.filter((event) => event.payload.kind === "tts");
|
|
2146
|
+
const routingEvents = listVoiceRoutingEvents(events);
|
|
2147
|
+
const data = {
|
|
2148
|
+
links: options.links,
|
|
2149
|
+
llmProviderHealth: await summarizeVoiceProviderHealth({
|
|
2150
|
+
events,
|
|
2151
|
+
providers: options.llmProviders ?? []
|
|
2152
|
+
}),
|
|
2153
|
+
routingEvents,
|
|
2154
|
+
routingSessions: summarizeVoiceRoutingSessions(routingEvents),
|
|
2155
|
+
sttProviderHealth: await summarizeVoiceProviderHealth({
|
|
2156
|
+
events: sttEvents,
|
|
2157
|
+
providers: options.sttProviders ?? []
|
|
2158
|
+
}),
|
|
2159
|
+
sttSimulation: options.sttSimulation,
|
|
2160
|
+
title: options.title,
|
|
2161
|
+
ttsProviderHealth: await summarizeVoiceProviderHealth({
|
|
2162
|
+
events: ttsEvents,
|
|
2163
|
+
providers: options.ttsProviders ?? []
|
|
2164
|
+
}),
|
|
2165
|
+
ttsSimulation: options.ttsSimulation
|
|
2166
|
+
};
|
|
2167
|
+
const body = await (options.render ?? renderVoiceResilienceHTML)(data);
|
|
2168
|
+
return new Response(body, {
|
|
2169
|
+
headers: {
|
|
2170
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
2171
|
+
...options.headers
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
});
|
|
2175
|
+
registerSimulationRoutes(routes, options.sttSimulation, "/api/stt-simulate");
|
|
2176
|
+
registerSimulationRoutes(routes, options.ttsSimulation, "/api/tts-simulate");
|
|
2177
|
+
return routes;
|
|
2178
|
+
};
|
|
2179
|
+
|
|
2180
|
+
// src/providerDecisionTraces.ts
|
|
2181
|
+
var escapeHtml7 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2182
|
+
var getString3 = (value) => typeof value === "string" ? value : undefined;
|
|
2183
|
+
var getNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2184
|
+
var isDecisionTrace = (event) => Boolean(event && typeof event === "object" && "provider" in event && "reason" in event && "sessionId" in event && "status" in event && "surface" in event);
|
|
2185
|
+
var surfaceForKind = (kind) => {
|
|
2186
|
+
switch (kind) {
|
|
2187
|
+
case "stt":
|
|
2188
|
+
return "live-stt";
|
|
2189
|
+
case "tts":
|
|
2190
|
+
return "telephony-tts";
|
|
2191
|
+
case "llm":
|
|
2192
|
+
default:
|
|
2193
|
+
return "live-call";
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
var statusRank = { fail: 2, pass: 0, warn: 1 };
|
|
2197
|
+
var reportStatus = (issues) => issues.reduce((status, issue) => statusRank[issue.status] > statusRank[status] ? issue.status : status, "pass");
|
|
2198
|
+
var uniqueSorted = (values) => [
|
|
2199
|
+
...new Set(values.filter((value) => typeof value === "string"))
|
|
2200
|
+
].sort();
|
|
2201
|
+
var createVoiceProviderDecisionTraceEvent = (input) => {
|
|
2202
|
+
const surface = input.surface ?? surfaceForKind(input.kind);
|
|
2203
|
+
const reason = input.reason ?? (input.status === "degraded" ? `Provider ${input.provider} degraded to ${input.fallbackProvider ?? input.selectedProvider ?? "lower-fidelity fallback"}.` : input.status === "fallback" ? `Fallback from ${input.provider} to ${input.fallbackProvider ?? input.selectedProvider ?? "next provider"}.` : input.status === "error" ? `Provider ${input.provider} errored before recovery.` : input.status === "skipped" ? `Provider ${input.provider} was skipped by policy.` : `Provider ${input.selectedProvider ?? input.provider} selected by policy.`);
|
|
2204
|
+
return {
|
|
2205
|
+
at: input.at ?? Date.now(),
|
|
2206
|
+
payload: {
|
|
2207
|
+
...input,
|
|
2208
|
+
providerDecision: true,
|
|
2209
|
+
reason,
|
|
2210
|
+
surface
|
|
2211
|
+
},
|
|
2212
|
+
scenarioId: input.scenarioId,
|
|
2213
|
+
sessionId: input.sessionId ?? `${surface}-provider-decision`,
|
|
2214
|
+
turnId: input.turnId,
|
|
2215
|
+
type: "provider.decision"
|
|
2216
|
+
};
|
|
2217
|
+
};
|
|
2218
|
+
var listVoiceProviderDecisionTraces = (events) => {
|
|
2219
|
+
if (events.every(isDecisionTrace)) {
|
|
2220
|
+
return [...events].sort((left, right) => right.at - left.at);
|
|
2221
|
+
}
|
|
2222
|
+
const traceEvents = events;
|
|
2223
|
+
const explicit = traceEvents.filter((event) => event.type === "provider.decision").map((event) => {
|
|
2224
|
+
const provider = getString3(event.payload.provider);
|
|
2225
|
+
const status = getString3(event.payload.status);
|
|
2226
|
+
const surface = getString3(event.payload.surface);
|
|
2227
|
+
if (!provider || !surface || status !== "error" && status !== "fallback" && status !== "degraded" && status !== "selected" && status !== "skipped" && status !== "success") {
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
return {
|
|
2231
|
+
at: event.at,
|
|
2232
|
+
elapsedMs: getNumber3(event.payload.elapsedMs),
|
|
2233
|
+
error: getString3(event.payload.error),
|
|
2234
|
+
fallbackProvider: getString3(event.payload.fallbackProvider),
|
|
2235
|
+
kind: event.payload.kind === "llm" || event.payload.kind === "stt" || event.payload.kind === "tts" ? event.payload.kind : undefined,
|
|
2236
|
+
latencyBudgetMs: getNumber3(event.payload.latencyBudgetMs),
|
|
2237
|
+
provider,
|
|
2238
|
+
reason: getString3(event.payload.reason) ?? `Provider ${provider} emitted ${status}.`,
|
|
2239
|
+
scenarioId: event.scenarioId,
|
|
2240
|
+
selectedProvider: getString3(event.payload.selectedProvider),
|
|
2241
|
+
sessionId: event.sessionId,
|
|
2242
|
+
status,
|
|
2243
|
+
surface,
|
|
2244
|
+
turnId: event.turnId
|
|
2245
|
+
};
|
|
2246
|
+
}).filter((event) => Boolean(event));
|
|
2247
|
+
const routing = listVoiceRoutingEvents(traceEvents).map((event) => ({
|
|
2248
|
+
at: event.at,
|
|
2249
|
+
elapsedMs: event.elapsedMs,
|
|
2250
|
+
error: event.error,
|
|
2251
|
+
fallbackProvider: event.fallbackProvider,
|
|
2252
|
+
kind: event.kind,
|
|
2253
|
+
latencyBudgetMs: event.latencyBudgetMs,
|
|
2254
|
+
provider: event.provider ?? event.selectedProvider ?? "unknown",
|
|
2255
|
+
reason: event.status === "fallback" ? `Fallback selected ${event.selectedProvider ?? event.fallbackProvider ?? "next provider"} after ${event.provider ?? "provider"} failed.` : event.status === "error" ? `Provider ${event.provider ?? "unknown"} errored before fallback recovery.` : `Provider ${event.selectedProvider ?? event.provider ?? "unknown"} completed successfully.`,
|
|
2256
|
+
scenarioId: event.scenarioId,
|
|
2257
|
+
selectedProvider: event.selectedProvider,
|
|
2258
|
+
sessionId: event.sessionId,
|
|
2259
|
+
status: event.status === "fallback" || event.status === "error" ? event.status : "success",
|
|
2260
|
+
surface: getString3(event.surface) ?? surfaceForKind(event.kind),
|
|
2261
|
+
turnId: event.turnId
|
|
2262
|
+
}));
|
|
2263
|
+
return [...explicit, ...routing].sort((left, right) => right.at - left.at);
|
|
2264
|
+
};
|
|
2265
|
+
var buildVoiceProviderDecisionTraceReport = async (options) => {
|
|
2266
|
+
const now = options.now ?? Date.now();
|
|
2267
|
+
const rawEvents = options.events ?? await options.store?.list() ?? [];
|
|
2268
|
+
const decisions = listVoiceProviderDecisionTraces(rawEvents).filter((decision) => {
|
|
2269
|
+
if (options.sessionId && decision.sessionId !== options.sessionId) {
|
|
2270
|
+
return false;
|
|
2271
|
+
}
|
|
2272
|
+
if (options.maxAgeMs !== undefined && now - decision.at > options.maxAgeMs) {
|
|
2273
|
+
return false;
|
|
2274
|
+
}
|
|
2275
|
+
return true;
|
|
2276
|
+
});
|
|
2277
|
+
const surfaces = new Map;
|
|
2278
|
+
const issues = [];
|
|
2279
|
+
for (const decision of decisions) {
|
|
2280
|
+
const group = surfaces.get(decision.surface) ?? [];
|
|
2281
|
+
group.push(decision);
|
|
2282
|
+
surfaces.set(decision.surface, group);
|
|
2283
|
+
}
|
|
2284
|
+
for (const surface of options.requiredSurfaces ?? []) {
|
|
2285
|
+
if (!surfaces.has(surface)) {
|
|
2286
|
+
issues.push({
|
|
2287
|
+
code: "voice.provider_decision_trace.surface_missing",
|
|
2288
|
+
message: `Surface ${surface} has no provider decision traces.`,
|
|
2289
|
+
status: "fail",
|
|
2290
|
+
surface
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
const fallbackCount = decisions.filter((decision) => decision.status === "fallback").length;
|
|
2295
|
+
const degradedCount = decisions.filter((decision) => decision.status === "degraded").length;
|
|
2296
|
+
const statuses = new Set(decisions.map((decision) => decision.status));
|
|
2297
|
+
const providers = uniqueSorted(decisions.flatMap((decision) => [
|
|
2298
|
+
decision.provider,
|
|
2299
|
+
decision.selectedProvider,
|
|
2300
|
+
decision.fallbackProvider
|
|
2301
|
+
]));
|
|
2302
|
+
const fallbackProviders = uniqueSorted(decisions.flatMap((decision) => [
|
|
2303
|
+
decision.fallbackProvider,
|
|
2304
|
+
decision.status === "fallback" || decision.status === "degraded" ? decision.selectedProvider : undefined
|
|
2305
|
+
]));
|
|
2306
|
+
if (options.minDecisions !== undefined && decisions.length < options.minDecisions) {
|
|
2307
|
+
issues.push({
|
|
2308
|
+
code: "voice.provider_decision_trace.min_decisions",
|
|
2309
|
+
message: `Found ${String(decisions.length)} provider decision trace(s); expected at least ${String(options.minDecisions)}.`,
|
|
2310
|
+
status: "fail"
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
if (options.minFallbacks !== undefined && fallbackCount < options.minFallbacks) {
|
|
2314
|
+
issues.push({
|
|
2315
|
+
code: "voice.provider_decision_trace.min_fallbacks",
|
|
2316
|
+
message: `Found ${String(fallbackCount)} provider fallback trace(s); expected at least ${String(options.minFallbacks)}.`,
|
|
2317
|
+
status: "fail"
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
if (options.minDegraded !== undefined && degradedCount < options.minDegraded) {
|
|
2321
|
+
issues.push({
|
|
2322
|
+
code: "voice.provider_decision_trace.min_degraded",
|
|
2323
|
+
message: `Found ${String(degradedCount)} provider degradation trace(s); expected at least ${String(options.minDegraded)}.`,
|
|
2324
|
+
status: "fail"
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
for (const status of options.requiredStatuses ?? []) {
|
|
2328
|
+
if (!statuses.has(status)) {
|
|
2329
|
+
issues.push({
|
|
2330
|
+
code: "voice.provider_decision_trace.status_missing",
|
|
2331
|
+
message: `Missing provider decision status: ${status}.`,
|
|
2332
|
+
status: "fail"
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
for (const provider of options.requiredProviders ?? []) {
|
|
2337
|
+
if (!providers.includes(provider)) {
|
|
2338
|
+
issues.push({
|
|
2339
|
+
code: "voice.provider_decision_trace.provider_missing",
|
|
2340
|
+
message: `Missing provider decision provider: ${provider}.`,
|
|
2341
|
+
status: "fail"
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
for (const provider of options.requiredFallbackProviders ?? []) {
|
|
2346
|
+
if (!fallbackProviders.includes(provider)) {
|
|
2347
|
+
issues.push({
|
|
2348
|
+
code: "voice.provider_decision_trace.fallback_provider_missing",
|
|
2349
|
+
message: `Missing provider decision fallback provider: ${provider}.`,
|
|
2350
|
+
status: "fail"
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
for (const phrase of options.requiredReasonIncludes ?? []) {
|
|
2355
|
+
if (!decisions.some((decision) => decision.reason.includes(phrase))) {
|
|
2356
|
+
issues.push({
|
|
2357
|
+
code: "voice.provider_decision_trace.reason_missing",
|
|
2358
|
+
message: `Missing provider decision reason containing: ${phrase}.`,
|
|
2359
|
+
status: "fail"
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
const surfaceReports = [...surfaces.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([surface, surfaceDecisions]) => {
|
|
2364
|
+
const surfaceIssues = issues.filter((issue) => issue.surface === surface);
|
|
2365
|
+
return {
|
|
2366
|
+
degraded: surfaceDecisions.filter((decision) => decision.status === "degraded").length,
|
|
2367
|
+
decisions: surfaceDecisions.length,
|
|
2368
|
+
errors: surfaceDecisions.filter((decision) => decision.status === "error").length,
|
|
2369
|
+
fallbacks: surfaceDecisions.filter((decision) => decision.status === "fallback").length,
|
|
2370
|
+
issues: surfaceIssues,
|
|
2371
|
+
latestAt: Math.max(...surfaceDecisions.map((decision) => decision.at)),
|
|
2372
|
+
providers: uniqueSorted(surfaceDecisions.flatMap((decision) => [
|
|
2373
|
+
decision.provider,
|
|
2374
|
+
decision.selectedProvider,
|
|
2375
|
+
decision.fallbackProvider
|
|
2376
|
+
])),
|
|
2377
|
+
reasons: uniqueSorted(surfaceDecisions.map((decision) => decision.reason)),
|
|
2378
|
+
selected: surfaceDecisions.filter((decision) => decision.status === "selected" || decision.status === "success").length,
|
|
2379
|
+
status: reportStatus(surfaceIssues),
|
|
2380
|
+
surface
|
|
2381
|
+
};
|
|
2382
|
+
});
|
|
2383
|
+
return {
|
|
2384
|
+
checkedAt: now,
|
|
2385
|
+
decisions,
|
|
2386
|
+
issues,
|
|
2387
|
+
status: reportStatus(issues),
|
|
2388
|
+
summary: {
|
|
2389
|
+
degraded: degradedCount,
|
|
2390
|
+
decisions: decisions.length,
|
|
2391
|
+
errors: decisions.filter((decision) => decision.status === "error").length,
|
|
2392
|
+
fallbacks: fallbackCount,
|
|
2393
|
+
providers: providers.length,
|
|
2394
|
+
selected: decisions.filter((decision) => decision.status === "selected" || decision.status === "success").length,
|
|
2395
|
+
surfaces: surfaces.size
|
|
2396
|
+
},
|
|
2397
|
+
surfaces: surfaceReports
|
|
2398
|
+
};
|
|
2399
|
+
};
|
|
2400
|
+
var renderVoiceProviderDecisionTraceMarkdown = (report) => [
|
|
2401
|
+
"# Voice Provider Decision Traces",
|
|
2402
|
+
"",
|
|
2403
|
+
`Status: **${report.status}**`,
|
|
2404
|
+
`Decisions: ${String(report.summary.decisions)}`,
|
|
2405
|
+
`Providers: ${String(report.summary.providers)}`,
|
|
2406
|
+
`Fallbacks: ${String(report.summary.fallbacks)}`,
|
|
2407
|
+
`Degraded: ${String(report.summary.degraded)}`,
|
|
2408
|
+
`Errors: ${String(report.summary.errors)}`,
|
|
2409
|
+
"",
|
|
2410
|
+
"| Surface | Status | Decisions | Selected | Fallbacks | Degraded | Errors | Providers |",
|
|
2411
|
+
"| --- | --- | ---: | ---: | ---: | ---: | ---: | --- |",
|
|
2412
|
+
...report.surfaces.map((surface) => `| ${surface.surface} | ${surface.status} | ${String(surface.decisions)} | ${String(surface.selected)} | ${String(surface.fallbacks)} | ${String(surface.degraded)} | ${String(surface.errors)} | ${surface.providers.join(", ")} |`),
|
|
2413
|
+
"",
|
|
2414
|
+
...report.issues.map((issue) => `- ${issue.status}: ${issue.message}`)
|
|
2415
|
+
].join(`
|
|
2416
|
+
`);
|
|
2417
|
+
var renderVoiceProviderDecisionTraceHTML = (report, title = "Provider Decision Traces") => `<!doctype html>
|
|
2418
|
+
<html lang="en">
|
|
2419
|
+
<head>
|
|
2420
|
+
<meta charset="utf-8" />
|
|
2421
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2422
|
+
<title>${escapeHtml7(title)}</title>
|
|
2423
|
+
<style>
|
|
2424
|
+
body{font-family:ui-sans-serif,system-ui,sans-serif;margin:0;background:#f8fafc;color:#0f172a}
|
|
2425
|
+
main{max-width:1100px;margin:0 auto;padding:32px}
|
|
2426
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px}
|
|
2427
|
+
.card,.surface{background:white;border:1px solid #e2e8f0;border-radius:16px;padding:16px;box-shadow:0 12px 30px rgba(15,23,42,.06)}
|
|
2428
|
+
.status{display:inline-flex;border-radius:999px;padding:4px 10px;font-weight:700;background:#dcfce7;color:#166534}
|
|
2429
|
+
.status.fail{background:#fee2e2;color:#991b1b}.status.warn{background:#fef3c7;color:#92400e}
|
|
2430
|
+
.surfaces{display:grid;gap:14px;margin-top:18px}.muted{color:#64748b}
|
|
2431
|
+
code{background:#e2e8f0;border-radius:8px;padding:2px 6px}
|
|
2432
|
+
</style>
|
|
2433
|
+
</head>
|
|
2434
|
+
<body>
|
|
2435
|
+
<main>
|
|
2436
|
+
<p class="status ${report.status}">${escapeHtml7(report.status)}</p>
|
|
2437
|
+
<h1>${escapeHtml7(title)}</h1>
|
|
2438
|
+
<p class="muted">Runtime proof for why providers were selected, skipped, failed, or recovered by fallback.</p>
|
|
2439
|
+
<section class="grid">
|
|
2440
|
+
<article class="card"><strong>${String(report.summary.decisions)}</strong><p>decisions</p></article>
|
|
2441
|
+
<article class="card"><strong>${String(report.summary.providers)}</strong><p>providers</p></article>
|
|
2442
|
+
<article class="card"><strong>${String(report.summary.fallbacks)}</strong><p>fallbacks</p></article>
|
|
2443
|
+
<article class="card"><strong>${String(report.summary.degraded)}</strong><p>degraded</p></article>
|
|
2444
|
+
<article class="card"><strong>${String(report.summary.errors)}</strong><p>errors</p></article>
|
|
2445
|
+
</section>
|
|
2446
|
+
<section class="surfaces">
|
|
2447
|
+
${report.surfaces.map((surface) => `<article class="surface">
|
|
2448
|
+
<header><strong>${escapeHtml7(surface.surface)}</strong> <span class="status ${surface.status}">${escapeHtml7(surface.status)}</span></header>
|
|
2449
|
+
<p>${String(surface.decisions)} decision(s), ${String(surface.fallbacks)} fallback(s), ${String(surface.degraded)} degraded decision(s), ${String(surface.errors)} error(s).</p>
|
|
2450
|
+
<p class="muted">Providers: ${escapeHtml7(surface.providers.join(", ") || "none")}</p>
|
|
2451
|
+
<p>${surface.reasons.map((reason) => `<code>${escapeHtml7(reason)}</code>`).join(" ")}</p>
|
|
2452
|
+
</article>`).join(`
|
|
2453
|
+
`)}
|
|
2454
|
+
</section>
|
|
2455
|
+
</main>
|
|
2456
|
+
</body>
|
|
2457
|
+
</html>`;
|
|
2458
|
+
var createVoiceProviderDecisionTraceRoutes = (options) => {
|
|
2459
|
+
const path = options.path ?? "/api/voice/provider-decisions";
|
|
2460
|
+
const htmlPath = options.htmlPath ?? "/voice/provider-decisions";
|
|
2461
|
+
const markdownPath = options.markdownPath ?? "/voice/provider-decisions.md";
|
|
2462
|
+
const headers = options.headers ?? {};
|
|
2463
|
+
const title = options.title ?? "Provider Decision Traces";
|
|
2464
|
+
const report = () => buildVoiceProviderDecisionTraceReport(options);
|
|
2465
|
+
const app = new Elysia3({ name: options.name ?? "voice-provider-decisions" }).get(path, async () => new Response(JSON.stringify(await report(), null, 2), {
|
|
2466
|
+
headers: {
|
|
2467
|
+
"content-type": "application/json; charset=utf-8",
|
|
2468
|
+
...headers
|
|
2469
|
+
}
|
|
2470
|
+
}));
|
|
2471
|
+
if (htmlPath !== false) {
|
|
2472
|
+
app.get(htmlPath, async () => {
|
|
2473
|
+
const body = options.render ? await options.render(await report()) : renderVoiceProviderDecisionTraceHTML(await report(), title);
|
|
2474
|
+
return new Response(body, {
|
|
2475
|
+
headers: {
|
|
2476
|
+
"content-type": "text/html; charset=utf-8",
|
|
2477
|
+
...headers
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
if (markdownPath !== false) {
|
|
2483
|
+
app.get(markdownPath, async () => new Response(renderVoiceProviderDecisionTraceMarkdown(await report()), {
|
|
2484
|
+
headers: {
|
|
2485
|
+
"content-type": "text/markdown; charset=utf-8",
|
|
2486
|
+
...headers
|
|
2487
|
+
}
|
|
2488
|
+
}));
|
|
2489
|
+
}
|
|
2490
|
+
return app;
|
|
2491
|
+
};
|
|
2492
|
+
|
|
2493
|
+
// src/trace.ts
|
|
2494
|
+
var createVoiceTraceEventId = (event) => [
|
|
2495
|
+
event.sessionId,
|
|
2496
|
+
event.turnId ?? "session",
|
|
2497
|
+
event.type,
|
|
2498
|
+
String(event.at ?? Date.now()),
|
|
2499
|
+
crypto.randomUUID()
|
|
2500
|
+
].map(encodeURIComponent).join(":");
|
|
2501
|
+
var createVoiceTraceEvent = (event) => ({
|
|
2502
|
+
...event,
|
|
2503
|
+
at: event.at,
|
|
2504
|
+
id: event.id ?? createVoiceTraceEventId({
|
|
2505
|
+
at: event.at,
|
|
2506
|
+
sessionId: event.sessionId,
|
|
2507
|
+
turnId: event.turnId,
|
|
2508
|
+
type: event.type
|
|
2509
|
+
})
|
|
2510
|
+
});
|
|
2511
|
+
var createVoiceTraceSinkDeliveryId = (events) => {
|
|
2512
|
+
const firstEvent = events[0];
|
|
2513
|
+
return [
|
|
2514
|
+
firstEvent?.sessionId ?? "trace",
|
|
2515
|
+
firstEvent?.traceId ?? "sink",
|
|
2516
|
+
String(firstEvent?.at ?? Date.now()),
|
|
2517
|
+
crypto.randomUUID()
|
|
2518
|
+
].map(encodeURIComponent).join(":");
|
|
2519
|
+
};
|
|
2520
|
+
var createVoiceTraceSinkDeliveryRecord = (input) => {
|
|
2521
|
+
const createdAt = input.createdAt ?? Date.now();
|
|
2522
|
+
return {
|
|
2523
|
+
createdAt,
|
|
2524
|
+
deliveredAt: input.deliveredAt,
|
|
2525
|
+
deliveryAttempts: input.deliveryAttempts,
|
|
2526
|
+
deliveryError: input.deliveryError,
|
|
2527
|
+
deliveryStatus: input.deliveryStatus ?? "pending",
|
|
2528
|
+
events: input.events,
|
|
2529
|
+
id: input.id ?? createVoiceTraceSinkDeliveryId(input.events),
|
|
2530
|
+
sinkDeliveries: input.sinkDeliveries,
|
|
2531
|
+
updatedAt: input.updatedAt ?? createdAt
|
|
2532
|
+
};
|
|
2533
|
+
};
|
|
2534
|
+
var matchesTraceFilter = (event, filter) => {
|
|
2535
|
+
if (filter.sessionId !== undefined && event.sessionId !== filter.sessionId) {
|
|
2536
|
+
return false;
|
|
2537
|
+
}
|
|
2538
|
+
if (filter.turnId !== undefined && event.turnId !== filter.turnId) {
|
|
2539
|
+
return false;
|
|
2540
|
+
}
|
|
2541
|
+
if (filter.scenarioId !== undefined && event.scenarioId !== filter.scenarioId) {
|
|
2542
|
+
return false;
|
|
2543
|
+
}
|
|
2544
|
+
if (filter.traceId !== undefined && event.traceId !== filter.traceId) {
|
|
2545
|
+
return false;
|
|
2546
|
+
}
|
|
2547
|
+
if (filter.type !== undefined) {
|
|
2548
|
+
const types = Array.isArray(filter.type) ? filter.type : [filter.type];
|
|
2549
|
+
if (!types.includes(event.type)) {
|
|
2550
|
+
return false;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
return true;
|
|
2554
|
+
};
|
|
2555
|
+
var filterVoiceTraceEvents = (events, filter = {}) => {
|
|
2556
|
+
const sorted = events.filter((event) => matchesTraceFilter(event, filter)).sort((left, right) => left.at - right.at || left.id.localeCompare(right.id));
|
|
2557
|
+
return typeof filter.limit === "number" && filter.limit >= 0 ? sorted.slice(0, filter.limit) : sorted;
|
|
2558
|
+
};
|
|
2559
|
+
var isPruneTimeMatch = (event, options) => {
|
|
2560
|
+
if (typeof options.before === "number" && event.at >= options.before) {
|
|
2561
|
+
return false;
|
|
2562
|
+
}
|
|
2563
|
+
if (typeof options.beforeOrAt === "number" && event.at > options.beforeOrAt) {
|
|
2564
|
+
return false;
|
|
2565
|
+
}
|
|
2566
|
+
return true;
|
|
2567
|
+
};
|
|
2568
|
+
var selectVoiceTraceEventsForPrune = (events, options = {}) => {
|
|
2569
|
+
let candidates = filterVoiceTraceEvents(events, options.filter).filter((event) => isPruneTimeMatch(event, options));
|
|
2570
|
+
if (typeof options.keepNewest === "number" && options.keepNewest >= 0) {
|
|
2571
|
+
const newestIds = new Set([...candidates].sort((left, right) => right.at - left.at || right.id.localeCompare(left.id)).slice(0, options.keepNewest).map((event) => event.id));
|
|
2572
|
+
candidates = candidates.filter((event) => !newestIds.has(event.id));
|
|
2573
|
+
}
|
|
2574
|
+
return typeof options.limit === "number" && options.limit >= 0 ? candidates.slice(0, options.limit) : candidates;
|
|
2575
|
+
};
|
|
2576
|
+
var pruneVoiceTraceEvents = async (options) => {
|
|
2577
|
+
const events = await options.store.list(options.filter);
|
|
2578
|
+
const deleted = selectVoiceTraceEventsForPrune(events, options);
|
|
2579
|
+
if (!options.dryRun) {
|
|
2580
|
+
await Promise.all(deleted.map((event) => options.store.remove(event.id)));
|
|
2581
|
+
}
|
|
2582
|
+
return {
|
|
2583
|
+
deleted,
|
|
2584
|
+
deletedCount: deleted.length,
|
|
2585
|
+
dryRun: Boolean(options.dryRun),
|
|
2586
|
+
scannedCount: events.length
|
|
2587
|
+
};
|
|
2588
|
+
};
|
|
2589
|
+
var sleep = async (delayMs) => {
|
|
2590
|
+
if (delayMs <= 0) {
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2594
|
+
};
|
|
2595
|
+
var toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2596
|
+
var signVoiceTraceSinkBody = async (input) => {
|
|
2597
|
+
const encoder = new TextEncoder;
|
|
2598
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
2599
|
+
hash: "SHA-256",
|
|
2600
|
+
name: "HMAC"
|
|
2601
|
+
}, false, ["sign"]);
|
|
2602
|
+
const payload = encoder.encode(`${input.timestamp}.${input.body}`);
|
|
2603
|
+
const signature = await crypto.subtle.sign("HMAC", key, payload);
|
|
2604
|
+
return `sha256=${toHex(new Uint8Array(signature))}`;
|
|
2605
|
+
};
|
|
2606
|
+
var createVoiceTraceSinkDeliveryError = (input) => {
|
|
2607
|
+
if (input.response) {
|
|
2608
|
+
const statusText = input.response.statusText?.trim();
|
|
2609
|
+
return `Attempt ${input.attempt} failed with trace sink response ${input.response.status}${statusText ? ` ${statusText}` : ""}.`;
|
|
2610
|
+
}
|
|
2611
|
+
if (input.error instanceof Error) {
|
|
2612
|
+
return `Attempt ${input.attempt} failed: ${input.error.message}`;
|
|
2613
|
+
}
|
|
2614
|
+
return `Attempt ${input.attempt} failed: ${String(input.error)}`;
|
|
2615
|
+
};
|
|
2616
|
+
var normalizeVoiceTraceS3KeyPrefix = (prefix) => prefix?.trim().replace(/^\/+|\/+$/g, "") ?? "voice/trace-deliveries";
|
|
2617
|
+
var createVoiceTraceS3ObjectKey = (prefix, events) => {
|
|
2618
|
+
const firstEvent = events[0];
|
|
2619
|
+
const safeSessionId = encodeURIComponent(firstEvent?.sessionId ?? "trace");
|
|
2620
|
+
const safeEventId = encodeURIComponent(firstEvent?.id ?? crypto.randomUUID());
|
|
2621
|
+
return `${prefix}/${safeSessionId}/${Date.now()}-${safeEventId}.json`;
|
|
2622
|
+
};
|
|
2623
|
+
var resolveVoiceS3DeliveredTo = (options, key) => {
|
|
2624
|
+
const bucket = options.bucket;
|
|
2625
|
+
return bucket ? `s3://${bucket}/${key}` : `s3://${key}`;
|
|
2626
|
+
};
|
|
2627
|
+
var aggregateVoiceTraceSinkDeliveryStatus = (deliveries) => {
|
|
2628
|
+
const statuses = Object.values(deliveries).map((delivery) => delivery.status);
|
|
2629
|
+
if (statuses.length === 0 || statuses.every((status) => status === "skipped")) {
|
|
2630
|
+
return "skipped";
|
|
2631
|
+
}
|
|
2632
|
+
if (statuses.some((status) => status === "failed")) {
|
|
2633
|
+
return "failed";
|
|
2634
|
+
}
|
|
2635
|
+
return "delivered";
|
|
2636
|
+
};
|
|
2637
|
+
var createVoiceTraceHTTPSink = (options) => ({
|
|
2638
|
+
deliver: async ({ events }) => {
|
|
2639
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2640
|
+
if (typeof fetchImpl !== "function") {
|
|
2641
|
+
return {
|
|
2642
|
+
attempts: 0,
|
|
2643
|
+
deliveredTo: options.url,
|
|
2644
|
+
error: "Trace sink delivery failed: fetch is not available in this runtime.",
|
|
2645
|
+
eventCount: events.length,
|
|
2646
|
+
status: "failed"
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
const maxRetries = Math.max(0, options.retries ?? 0);
|
|
2650
|
+
const backoffMs = Math.max(0, options.backoffMs ?? 250);
|
|
2651
|
+
const timeoutMs = Math.max(0, options.timeoutMs ?? 1e4);
|
|
2652
|
+
const payload = options.body ? await options.body({ events }) : {
|
|
2653
|
+
eventCount: events.length,
|
|
2654
|
+
events,
|
|
2655
|
+
source: "absolutejs-voice"
|
|
2656
|
+
};
|
|
2657
|
+
const body = JSON.stringify(payload);
|
|
2658
|
+
let lastError = "Trace sink delivery failed.";
|
|
2659
|
+
for (let attempt = 1;attempt <= maxRetries + 1; attempt += 1) {
|
|
2660
|
+
let controller;
|
|
2661
|
+
let timeout;
|
|
2662
|
+
try {
|
|
2663
|
+
const headers = {
|
|
2664
|
+
"content-type": "application/json",
|
|
2665
|
+
...options.headers
|
|
2666
|
+
};
|
|
2667
|
+
if (options.signingSecret) {
|
|
2668
|
+
const timestamp = String(Date.now());
|
|
2669
|
+
headers["x-absolutejs-timestamp"] = timestamp;
|
|
2670
|
+
headers["x-absolutejs-signature"] = await signVoiceTraceSinkBody({
|
|
2671
|
+
body,
|
|
2672
|
+
secret: options.signingSecret,
|
|
2673
|
+
timestamp
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
controller = timeoutMs > 0 ? new AbortController : undefined;
|
|
2677
|
+
if (controller && timeoutMs > 0) {
|
|
2678
|
+
timeout = setTimeout(() => controller?.abort(), timeoutMs);
|
|
2679
|
+
}
|
|
2680
|
+
const response = await fetchImpl(options.url, {
|
|
2681
|
+
body,
|
|
2682
|
+
headers,
|
|
2683
|
+
method: options.method ?? "POST",
|
|
2684
|
+
signal: controller?.signal
|
|
2685
|
+
});
|
|
2686
|
+
if (response.ok) {
|
|
2687
|
+
let responseBody;
|
|
2688
|
+
try {
|
|
2689
|
+
responseBody = await response.clone().json();
|
|
2690
|
+
} catch {
|
|
2691
|
+
responseBody = undefined;
|
|
2692
|
+
}
|
|
2693
|
+
return {
|
|
2694
|
+
attempts: attempt,
|
|
2695
|
+
deliveredAt: Date.now(),
|
|
2696
|
+
deliveredTo: options.url,
|
|
2697
|
+
eventCount: events.length,
|
|
2698
|
+
responseBody,
|
|
2699
|
+
status: "delivered"
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
lastError = createVoiceTraceSinkDeliveryError({
|
|
2703
|
+
attempt,
|
|
2704
|
+
response
|
|
2705
|
+
});
|
|
2706
|
+
} catch (error) {
|
|
2707
|
+
lastError = createVoiceTraceSinkDeliveryError({
|
|
2708
|
+
attempt,
|
|
2709
|
+
error
|
|
2710
|
+
});
|
|
2711
|
+
} finally {
|
|
2712
|
+
if (timeout) {
|
|
2713
|
+
clearTimeout(timeout);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
if (attempt <= maxRetries) {
|
|
2717
|
+
await sleep(backoffMs * attempt);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
return {
|
|
2721
|
+
attempts: maxRetries + 1,
|
|
2722
|
+
deliveredTo: options.url,
|
|
2723
|
+
error: lastError,
|
|
2724
|
+
eventCount: events.length,
|
|
2725
|
+
status: "failed"
|
|
2726
|
+
};
|
|
2727
|
+
},
|
|
2728
|
+
eventTypes: options.eventTypes,
|
|
2729
|
+
id: options.id,
|
|
2730
|
+
kind: options.kind ?? "http"
|
|
2731
|
+
});
|
|
2732
|
+
var createVoiceTraceS3Sink = (options) => {
|
|
2733
|
+
const client = options.client ?? new Bun.S3Client(options);
|
|
2734
|
+
const keyPrefix = normalizeVoiceTraceS3KeyPrefix(options.keyPrefix);
|
|
2735
|
+
return {
|
|
2736
|
+
deliver: async ({ events }) => {
|
|
2737
|
+
const key = createVoiceTraceS3ObjectKey(keyPrefix, events);
|
|
2738
|
+
const payload = options.body ? await options.body({ events, key }) : {
|
|
2739
|
+
eventCount: events.length,
|
|
2740
|
+
events,
|
|
2741
|
+
key,
|
|
2742
|
+
source: "absolutejs-voice"
|
|
2743
|
+
};
|
|
2744
|
+
try {
|
|
2745
|
+
const file = client.file(key, options);
|
|
2746
|
+
await file.write(JSON.stringify(payload), {
|
|
2747
|
+
type: options.contentType ?? "application/json"
|
|
2748
|
+
});
|
|
2749
|
+
return {
|
|
2750
|
+
attempts: 1,
|
|
2751
|
+
deliveredAt: Date.now(),
|
|
2752
|
+
deliveredTo: resolveVoiceS3DeliveredTo(options, key),
|
|
2753
|
+
eventCount: events.length,
|
|
2754
|
+
responseBody: { key },
|
|
2755
|
+
status: "delivered"
|
|
2756
|
+
};
|
|
2757
|
+
} catch (error) {
|
|
2758
|
+
return {
|
|
2759
|
+
attempts: 1,
|
|
2760
|
+
deliveredTo: resolveVoiceS3DeliveredTo(options, key),
|
|
2761
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2762
|
+
eventCount: events.length,
|
|
2763
|
+
status: "failed"
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
},
|
|
2767
|
+
eventTypes: options.eventTypes,
|
|
2768
|
+
id: options.id,
|
|
2769
|
+
kind: options.kind ?? "s3"
|
|
2770
|
+
};
|
|
2771
|
+
};
|
|
2772
|
+
var deliverVoiceTraceEventsToSinks = async (input) => {
|
|
2773
|
+
const events = input.redact ? redactVoiceTraceEvents(input.events, input.redact) : input.events;
|
|
2774
|
+
const sinkDeliveries = {};
|
|
2775
|
+
for (const sink of input.sinks) {
|
|
2776
|
+
const sinkEvents = sink.eventTypes?.length ? events.filter((event) => sink.eventTypes?.includes(event.type)) : events;
|
|
2777
|
+
if (sinkEvents.length === 0) {
|
|
2778
|
+
sinkDeliveries[sink.id] = {
|
|
2779
|
+
attempts: 0,
|
|
2780
|
+
eventCount: 0,
|
|
2781
|
+
status: "skipped"
|
|
2782
|
+
};
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
try {
|
|
2786
|
+
sinkDeliveries[sink.id] = await sink.deliver({
|
|
2787
|
+
events: sinkEvents
|
|
2788
|
+
});
|
|
2789
|
+
} catch (error) {
|
|
2790
|
+
sinkDeliveries[sink.id] = {
|
|
2791
|
+
attempts: 1,
|
|
2792
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2793
|
+
eventCount: sinkEvents.length,
|
|
2794
|
+
status: "failed"
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
return {
|
|
2799
|
+
deliveredAt: Date.now(),
|
|
2800
|
+
eventCount: events.length,
|
|
2801
|
+
sinkDeliveries,
|
|
2802
|
+
status: aggregateVoiceTraceSinkDeliveryStatus(sinkDeliveries)
|
|
2803
|
+
};
|
|
2804
|
+
};
|
|
2805
|
+
var createVoiceTraceSinkStore = (options) => {
|
|
2806
|
+
const deliver = async (event) => {
|
|
2807
|
+
const result = await deliverVoiceTraceEventsToSinks({
|
|
2808
|
+
events: [event],
|
|
2809
|
+
redact: options.redact,
|
|
2810
|
+
sinks: options.sinks
|
|
2811
|
+
});
|
|
2812
|
+
await options.onDelivery?.(result);
|
|
2813
|
+
};
|
|
2814
|
+
return {
|
|
2815
|
+
append: async (event) => {
|
|
2816
|
+
const stored = await options.store.append(event);
|
|
2817
|
+
if (options.deliveryQueue) {
|
|
2818
|
+
const delivery2 = createVoiceTraceSinkDeliveryRecord({
|
|
2819
|
+
events: [stored]
|
|
2820
|
+
});
|
|
2821
|
+
await options.deliveryQueue.set(delivery2.id, delivery2);
|
|
2822
|
+
return stored;
|
|
2823
|
+
}
|
|
2824
|
+
const delivery = deliver(stored);
|
|
2825
|
+
if (options.awaitDelivery) {
|
|
2826
|
+
await delivery;
|
|
2827
|
+
} else {
|
|
2828
|
+
delivery.catch((error) => {
|
|
2829
|
+
options.onError?.(error);
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2832
|
+
return stored;
|
|
2833
|
+
},
|
|
2834
|
+
get: (id) => options.store.get(id),
|
|
2835
|
+
list: (filter) => options.store.list(filter),
|
|
2836
|
+
remove: (id) => options.store.remove(id)
|
|
2837
|
+
};
|
|
2838
|
+
};
|
|
2839
|
+
var normalizeVoiceProfileTraceTaggerProfile = (profile) => typeof profile === "string" ? { id: profile } : profile?.id ? profile : undefined;
|
|
2840
|
+
var createVoiceProfileTraceTagger = (options) => {
|
|
2841
|
+
const profiles = new Map((options.profiles ?? []).map((profile) => [profile.id, profile]));
|
|
2842
|
+
const defaultProfile = normalizeVoiceProfileTraceTaggerProfile(options.defaultProfile);
|
|
2843
|
+
const resolveProfile = async (event) => {
|
|
2844
|
+
const resolved = normalizeVoiceProfileTraceTaggerProfile(await options.resolveProfile?.(event));
|
|
2845
|
+
const profile = resolved ?? defaultProfile;
|
|
2846
|
+
return profile ? profiles.get(profile.id) ?? profile : undefined;
|
|
2847
|
+
};
|
|
2848
|
+
return {
|
|
2849
|
+
append: async (event) => {
|
|
2850
|
+
const profile = await resolveProfile(event);
|
|
2851
|
+
if (!profile) {
|
|
2852
|
+
return options.store.append(event);
|
|
2853
|
+
}
|
|
2854
|
+
const metadata = {
|
|
2855
|
+
...event.metadata ?? {},
|
|
2856
|
+
benchmarkProfileId: profile.id,
|
|
2857
|
+
profileDescription: event.metadata?.profileDescription ?? profile.description,
|
|
2858
|
+
profileId: profile.id,
|
|
2859
|
+
profileLabel: event.metadata?.profileLabel ?? profile.label
|
|
2860
|
+
};
|
|
2861
|
+
const payload = event.payload && typeof event.payload === "object" ? {
|
|
2862
|
+
...event.payload,
|
|
2863
|
+
benchmarkProfileId: event.payload.benchmarkProfileId ?? profile.id,
|
|
2864
|
+
profileDescription: event.payload.profileDescription ?? profile.description,
|
|
2865
|
+
profileId: event.payload.profileId ?? profile.id,
|
|
2866
|
+
profileLabel: event.payload.profileLabel ?? profile.label
|
|
2867
|
+
} : event.payload;
|
|
2868
|
+
return options.store.append({
|
|
2869
|
+
...event,
|
|
2870
|
+
metadata,
|
|
2871
|
+
payload
|
|
2872
|
+
});
|
|
2873
|
+
},
|
|
2874
|
+
get: (id) => options.store.get(id),
|
|
2875
|
+
list: (filter) => options.store.list(filter),
|
|
2876
|
+
remove: (id) => options.store.remove(id)
|
|
2877
|
+
};
|
|
2878
|
+
};
|
|
2879
|
+
var createVoiceMemoryTraceSinkDeliveryStore = () => {
|
|
2880
|
+
const deliveries = new Map;
|
|
2881
|
+
return {
|
|
2882
|
+
get: async (id) => deliveries.get(id),
|
|
2883
|
+
list: async () => [...deliveries.values()].sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)),
|
|
2884
|
+
remove: async (id) => {
|
|
2885
|
+
deliveries.delete(id);
|
|
2886
|
+
},
|
|
2887
|
+
set: async (id, delivery) => {
|
|
2888
|
+
deliveries.set(id, delivery);
|
|
2889
|
+
}
|
|
2890
|
+
};
|
|
2891
|
+
};
|
|
2892
|
+
var createVoiceMemoryTraceEventStore = () => {
|
|
2893
|
+
const events = new Map;
|
|
2894
|
+
const append = async (event) => {
|
|
2895
|
+
const stored = createVoiceTraceEvent(event);
|
|
2896
|
+
events.set(stored.id, stored);
|
|
2897
|
+
return stored;
|
|
2898
|
+
};
|
|
2899
|
+
const get = async (id) => events.get(id);
|
|
2900
|
+
const list = async (filter) => filterVoiceTraceEvents([...events.values()], filter);
|
|
2901
|
+
const remove = async (id) => {
|
|
2902
|
+
events.delete(id);
|
|
2903
|
+
};
|
|
2904
|
+
return { append, get, list, remove };
|
|
2905
|
+
};
|
|
2906
|
+
var exportVoiceTrace = async (input) => {
|
|
2907
|
+
const events = await input.store.list(input.filter);
|
|
2908
|
+
return {
|
|
2909
|
+
exportedAt: Date.now(),
|
|
2910
|
+
events: input.redact ? redactVoiceTraceEvents(events, input.redact) : events,
|
|
2911
|
+
filter: input.filter,
|
|
2912
|
+
redacted: Boolean(input.redact)
|
|
2913
|
+
};
|
|
2914
|
+
};
|
|
2915
|
+
var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2916
|
+
var escapeHtml8 = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2917
|
+
var formatTraceValue = (value) => {
|
|
2918
|
+
if (value === undefined || value === null) {
|
|
2919
|
+
return "";
|
|
2920
|
+
}
|
|
2921
|
+
if (typeof value === "string") {
|
|
2922
|
+
return value;
|
|
2923
|
+
}
|
|
2924
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
2925
|
+
return String(value);
|
|
2926
|
+
}
|
|
2927
|
+
try {
|
|
2928
|
+
return JSON.stringify(value);
|
|
2929
|
+
} catch {
|
|
2930
|
+
return String(value);
|
|
2931
|
+
}
|
|
2932
|
+
};
|
|
2933
|
+
var DEFAULT_REDACTION_KEYS = [
|
|
2934
|
+
"apiKey",
|
|
2935
|
+
"authorization",
|
|
2936
|
+
"creditCard",
|
|
2937
|
+
"email",
|
|
2938
|
+
"externalId",
|
|
2939
|
+
"password",
|
|
2940
|
+
"phone",
|
|
2941
|
+
"secret",
|
|
2942
|
+
"ssn",
|
|
2943
|
+
"token"
|
|
2944
|
+
];
|
|
2945
|
+
var DEFAULT_REDACTION_TEXT_KEYS = [
|
|
2946
|
+
"assistantText",
|
|
2947
|
+
"content",
|
|
2948
|
+
"error",
|
|
2949
|
+
"reason",
|
|
2950
|
+
"summary",
|
|
2951
|
+
"text"
|
|
2952
|
+
];
|
|
2953
|
+
var EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
|
2954
|
+
var PHONE_PATTERN = /(?<!\d)(?:\+?1[\s.-]?)?(?:\(?\d{3}\)?[\s.-]?)\d{3}[\s.-]?\d{4}(?!\d)/g;
|
|
2955
|
+
var normalizeRedactionKey = (key) => key.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2956
|
+
var resolveVoiceTraceRedactionOptions = (options = {}) => ({
|
|
2957
|
+
keys: typeof options === "boolean" ? DEFAULT_REDACTION_KEYS : options.keys ?? DEFAULT_REDACTION_KEYS,
|
|
2958
|
+
redactEmails: typeof options === "boolean" ? true : options.redactEmails ?? true,
|
|
2959
|
+
redactPhoneNumbers: typeof options === "boolean" ? true : options.redactPhoneNumbers ?? true,
|
|
2960
|
+
redactText: typeof options === "boolean" ? true : options.redactText ?? true,
|
|
2961
|
+
replacement: typeof options === "boolean" ? "[redacted]" : options.replacement ?? "[redacted]",
|
|
2962
|
+
textKeys: typeof options === "boolean" ? DEFAULT_REDACTION_TEXT_KEYS : options.textKeys ?? DEFAULT_REDACTION_TEXT_KEYS
|
|
2963
|
+
});
|
|
2964
|
+
var resolveReplacement = (input) => typeof input.options.replacement === "function" ? input.options.replacement({
|
|
2965
|
+
key: input.key,
|
|
2966
|
+
path: input.path,
|
|
2967
|
+
value: input.value
|
|
2968
|
+
}) : input.options.replacement;
|
|
2969
|
+
var redactVoiceTraceText = (value, options = {}, input = {}) => {
|
|
2970
|
+
const resolved = resolveVoiceTraceRedactionOptions(options);
|
|
2971
|
+
let redacted = value;
|
|
2972
|
+
const replacement = resolveReplacement({
|
|
2973
|
+
key: input.key,
|
|
2974
|
+
options: resolved,
|
|
2975
|
+
path: input.path ?? [],
|
|
2976
|
+
value
|
|
2977
|
+
});
|
|
2978
|
+
if (resolved.redactEmails) {
|
|
2979
|
+
redacted = redacted.replace(EMAIL_PATTERN, replacement);
|
|
2980
|
+
}
|
|
2981
|
+
if (resolved.redactPhoneNumbers) {
|
|
2982
|
+
redacted = redacted.replace(PHONE_PATTERN, replacement);
|
|
2983
|
+
}
|
|
2984
|
+
return redacted;
|
|
2985
|
+
};
|
|
2986
|
+
var redactTraceValue = (value, options, path) => {
|
|
2987
|
+
const key = path.at(-1);
|
|
2988
|
+
const normalizedKey = key ? normalizeRedactionKey(key) : undefined;
|
|
2989
|
+
const sensitiveKeys = new Set(options.keys.map(normalizeRedactionKey));
|
|
2990
|
+
const textKeys = new Set(options.textKeys.map(normalizeRedactionKey));
|
|
2991
|
+
if (normalizedKey && sensitiveKeys.has(normalizedKey) && (value === null || ["boolean", "number", "string", "undefined"].includes(typeof value))) {
|
|
2992
|
+
return resolveReplacement({
|
|
2993
|
+
key,
|
|
2994
|
+
options,
|
|
2995
|
+
path,
|
|
2996
|
+
value: String(value ?? "")
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
if (typeof value === "string") {
|
|
3000
|
+
const shouldRedactText = options.redactText && (!normalizedKey || textKeys.has(normalizedKey) || path.length === 0);
|
|
3001
|
+
return shouldRedactText ? redactVoiceTraceText(value, options, {
|
|
3002
|
+
key,
|
|
3003
|
+
path
|
|
3004
|
+
}) : value;
|
|
3005
|
+
}
|
|
3006
|
+
if (Array.isArray(value)) {
|
|
3007
|
+
return value.map((item, index) => redactTraceValue(item, options, [...path, String(index)]));
|
|
3008
|
+
}
|
|
3009
|
+
if (typeof value === "object" && value) {
|
|
3010
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
3011
|
+
entryKey,
|
|
3012
|
+
redactTraceValue(entryValue, options, [...path, entryKey])
|
|
3013
|
+
]));
|
|
3014
|
+
}
|
|
3015
|
+
return value;
|
|
3016
|
+
};
|
|
3017
|
+
var redactVoiceTraceEvent = (event, options = {}) => {
|
|
3018
|
+
const resolved = resolveVoiceTraceRedactionOptions(options);
|
|
3019
|
+
return {
|
|
3020
|
+
...event,
|
|
3021
|
+
metadata: redactTraceValue(event.metadata, resolved, ["metadata"]),
|
|
3022
|
+
payload: redactTraceValue(event.payload, resolved, ["payload"])
|
|
3023
|
+
};
|
|
3024
|
+
};
|
|
3025
|
+
var redactVoiceTraceEvents = (events, options = {}) => events.map((event) => redactVoiceTraceEvent(event, options));
|
|
3026
|
+
var summarizeVoiceTrace = (events) => {
|
|
3027
|
+
const sorted = filterVoiceTraceEvents(events);
|
|
3028
|
+
const firstEvent = sorted[0];
|
|
3029
|
+
const lastEvent = sorted.at(-1);
|
|
3030
|
+
const lifecycleEvents = sorted.filter((event) => event.type === "call.lifecycle");
|
|
3031
|
+
const startEvent = lifecycleEvents.find((event) => event.payload.type === "start");
|
|
3032
|
+
const endEvent = lifecycleEvents.toReversed().find((event) => event.payload.type === "end");
|
|
3033
|
+
const costEvents = sorted.filter((event) => event.type === "turn.cost");
|
|
3034
|
+
const toolEvents = sorted.filter((event) => event.type === "agent.tool");
|
|
3035
|
+
const startedAt = startEvent?.at ?? firstEvent?.at;
|
|
3036
|
+
const endedAt = endEvent?.at ?? lastEvent?.at;
|
|
3037
|
+
const failed = sorted.some((event) => event.type === "session.error") || endEvent?.payload.disposition === "failed";
|
|
3038
|
+
return {
|
|
3039
|
+
assistantReplyCount: sorted.filter((event) => event.type === "turn.assistant").length,
|
|
3040
|
+
callDurationMs: startedAt !== undefined && endedAt !== undefined ? Math.max(0, endedAt - startedAt) : undefined,
|
|
3041
|
+
cost: {
|
|
3042
|
+
estimatedRelativeCostUnits: costEvents.reduce((total, event) => total + toNumber(event.payload.estimatedRelativeCostUnits), 0),
|
|
3043
|
+
totalBillableAudioMs: costEvents.reduce((total, event) => total + toNumber(event.payload.totalBillableAudioMs), 0)
|
|
3044
|
+
},
|
|
3045
|
+
endedAt,
|
|
3046
|
+
errorCount: sorted.filter((event) => event.type === "session.error").length,
|
|
3047
|
+
eventCount: sorted.length,
|
|
3048
|
+
failed,
|
|
3049
|
+
handoffCount: sorted.filter((event) => event.type === "agent.handoff").length,
|
|
3050
|
+
modelCallCount: sorted.filter((event) => event.type === "agent.model").length,
|
|
3051
|
+
sessionId: firstEvent?.sessionId,
|
|
3052
|
+
startedAt,
|
|
3053
|
+
toolCallCount: toolEvents.length,
|
|
3054
|
+
toolErrorCount: toolEvents.filter((event) => event.payload.status === "error").length,
|
|
3055
|
+
traceId: firstEvent?.traceId,
|
|
3056
|
+
transcriptCount: sorted.filter((event) => event.type === "turn.transcript").length,
|
|
3057
|
+
turnCount: sorted.filter((event) => event.type === "turn.committed").length
|
|
3058
|
+
};
|
|
3059
|
+
};
|
|
3060
|
+
var evaluateVoiceTrace = (events, options = {}) => {
|
|
3061
|
+
const summary = summarizeVoiceTrace(events);
|
|
3062
|
+
const issues = [];
|
|
3063
|
+
const maxHandoffs = options.maxHandoffs ?? 3;
|
|
3064
|
+
const maxToolErrors = options.maxToolErrors ?? 0;
|
|
3065
|
+
const maxModelCallsPerTurn = options.maxModelCallsPerTurn ?? 6;
|
|
3066
|
+
const turnCountForRatio = Math.max(1, summary.turnCount);
|
|
3067
|
+
if (options.requireCompletedCall !== false && !summary.endedAt) {
|
|
3068
|
+
issues.push({
|
|
3069
|
+
code: "call-not-ended",
|
|
3070
|
+
message: "Trace does not include a call end lifecycle event.",
|
|
3071
|
+
severity: "warning"
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
if (summary.failed) {
|
|
3075
|
+
issues.push({
|
|
3076
|
+
code: "session-error",
|
|
3077
|
+
message: "Trace contains a session error or failed call disposition.",
|
|
3078
|
+
severity: "error"
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
3081
|
+
if (options.requireTranscript !== false && summary.transcriptCount === 0) {
|
|
3082
|
+
issues.push({
|
|
3083
|
+
code: "missing-transcript",
|
|
3084
|
+
message: "Trace does not include any transcript events.",
|
|
3085
|
+
severity: "error"
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
if (options.requireTurn !== false && summary.turnCount === 0) {
|
|
3089
|
+
issues.push({
|
|
3090
|
+
code: "missing-turn",
|
|
3091
|
+
message: "Trace does not include any committed turns.",
|
|
3092
|
+
severity: "error"
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
if (options.requireAssistantReply !== false && summary.turnCount > 0 && summary.assistantReplyCount === 0) {
|
|
3096
|
+
issues.push({
|
|
3097
|
+
code: "missing-assistant-reply",
|
|
3098
|
+
message: "Trace has committed turns but no assistant replies.",
|
|
3099
|
+
severity: "warning"
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
if (summary.toolErrorCount > maxToolErrors) {
|
|
3103
|
+
issues.push({
|
|
3104
|
+
code: "tool-errors",
|
|
3105
|
+
message: `Trace has ${summary.toolErrorCount} tool error(s), above the allowed ${maxToolErrors}.`,
|
|
3106
|
+
severity: "error"
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
if (summary.handoffCount > maxHandoffs) {
|
|
3110
|
+
issues.push({
|
|
3111
|
+
code: "too-many-handoffs",
|
|
3112
|
+
message: `Trace has ${summary.handoffCount} handoff(s), above the allowed ${maxHandoffs}.`,
|
|
3113
|
+
severity: "warning"
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
if (summary.modelCallCount / turnCountForRatio > maxModelCallsPerTurn) {
|
|
3117
|
+
issues.push({
|
|
3118
|
+
code: "too-many-model-calls",
|
|
3119
|
+
message: `Trace averages more than ${maxModelCallsPerTurn} model calls per committed turn.`,
|
|
3120
|
+
severity: "warning"
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
return {
|
|
3124
|
+
issues,
|
|
3125
|
+
pass: !issues.some((issue) => issue.severity === "error"),
|
|
3126
|
+
summary
|
|
3127
|
+
};
|
|
3128
|
+
};
|
|
3129
|
+
var renderTraceEventMarkdown = (event, startedAt) => {
|
|
3130
|
+
const offset = startedAt === undefined ? `${event.at}` : `+${Math.max(0, event.at - startedAt)}ms`;
|
|
3131
|
+
const label = `- ${offset} [${event.type}]`;
|
|
3132
|
+
switch (event.type) {
|
|
3133
|
+
case "turn.transcript":
|
|
3134
|
+
return `${label} ${event.payload.isFinal ? "final" : "partial"} "${formatTraceValue(event.payload.text)}"`;
|
|
3135
|
+
case "turn.committed":
|
|
3136
|
+
return `${label} committed "${formatTraceValue(event.payload.text)}"`;
|
|
3137
|
+
case "turn.assistant":
|
|
3138
|
+
return event.payload.text ? `${label} assistant "${formatTraceValue(event.payload.text)}"` : `${label} ${formatTraceValue(event.payload.status)}`;
|
|
3139
|
+
case "agent.tool":
|
|
3140
|
+
return `${label} ${formatTraceValue(event.payload.toolName)} ${formatTraceValue(event.payload.status)}`;
|
|
3141
|
+
case "agent.context":
|
|
3142
|
+
return `${label} ${formatTraceValue(event.payload.fromAgentId)} -> ${formatTraceValue(event.payload.targetAgentId)} ${formatTraceValue(event.payload.status)}`;
|
|
3143
|
+
case "agent.handoff":
|
|
3144
|
+
return `${label} ${formatTraceValue(event.payload.fromAgentId)} -> ${formatTraceValue(event.payload.targetAgentId)}`;
|
|
3145
|
+
case "session.error":
|
|
3146
|
+
return `${label} ${formatTraceValue(event.payload.error)}`;
|
|
3147
|
+
case "call.lifecycle":
|
|
3148
|
+
return `${label} ${formatTraceValue(event.payload.type)} ${formatTraceValue(event.payload.disposition)}`.trim();
|
|
3149
|
+
default:
|
|
3150
|
+
return `${label} ${formatTraceValue(event.payload)}`;
|
|
3151
|
+
}
|
|
3152
|
+
};
|
|
3153
|
+
var renderVoiceTraceMarkdown = (events, options = {}) => {
|
|
3154
|
+
const sorted = filterVoiceTraceEvents(options.redact ? redactVoiceTraceEvents(events, options.redact) : events);
|
|
3155
|
+
const summary = summarizeVoiceTrace(sorted);
|
|
3156
|
+
const evaluation = evaluateVoiceTrace(sorted, options.evaluation);
|
|
3157
|
+
const lines = [
|
|
3158
|
+
`# ${options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim()}`,
|
|
3159
|
+
"",
|
|
3160
|
+
`Pass: ${evaluation.pass ? "yes" : "no"}`,
|
|
3161
|
+
`Session: ${summary.sessionId ?? "unknown"}`,
|
|
3162
|
+
`Events: ${summary.eventCount}`,
|
|
3163
|
+
`Turns: ${summary.turnCount}`,
|
|
3164
|
+
`Transcripts: ${summary.transcriptCount}`,
|
|
3165
|
+
`Assistant replies: ${summary.assistantReplyCount}`,
|
|
3166
|
+
`Model calls: ${summary.modelCallCount}`,
|
|
3167
|
+
`Tool calls: ${summary.toolCallCount}`,
|
|
3168
|
+
`Handoffs: ${summary.handoffCount}`,
|
|
3169
|
+
`Errors: ${summary.errorCount}`,
|
|
3170
|
+
`Estimated cost units: ${summary.cost.estimatedRelativeCostUnits}`,
|
|
3171
|
+
""
|
|
3172
|
+
];
|
|
3173
|
+
if (evaluation.issues.length > 0) {
|
|
3174
|
+
lines.push("## Issues", "");
|
|
3175
|
+
for (const issue of evaluation.issues) {
|
|
3176
|
+
lines.push(`- [${issue.severity}] ${issue.code}: ${issue.message}`);
|
|
3177
|
+
}
|
|
3178
|
+
lines.push("");
|
|
3179
|
+
}
|
|
3180
|
+
lines.push("## Timeline", "");
|
|
3181
|
+
for (const event of sorted) {
|
|
3182
|
+
lines.push(renderTraceEventMarkdown(event, summary.startedAt));
|
|
3183
|
+
}
|
|
3184
|
+
return lines.join(`
|
|
3185
|
+
`);
|
|
3186
|
+
};
|
|
3187
|
+
var renderVoiceTraceHTML = (events, options = {}) => {
|
|
3188
|
+
const markdown = renderVoiceTraceMarkdown(events, options);
|
|
3189
|
+
const renderEvents = options.redact ? redactVoiceTraceEvents(events, options.redact) : events;
|
|
3190
|
+
const summary = summarizeVoiceTrace(renderEvents);
|
|
3191
|
+
const evaluation = evaluateVoiceTrace(renderEvents, options.evaluation);
|
|
3192
|
+
const eventRows = filterVoiceTraceEvents(renderEvents).map((event) => {
|
|
3193
|
+
const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
|
|
3194
|
+
return [
|
|
3195
|
+
"<tr>",
|
|
3196
|
+
`<td>${escapeHtml8(String(offset))}</td>`,
|
|
3197
|
+
`<td>${escapeHtml8(event.type)}</td>`,
|
|
3198
|
+
`<td>${escapeHtml8(event.turnId ?? "")}</td>`,
|
|
3199
|
+
`<td><code>${escapeHtml8(JSON.stringify(event.payload))}</code></td>`,
|
|
3200
|
+
"</tr>"
|
|
3201
|
+
].join("");
|
|
3202
|
+
}).join(`
|
|
3203
|
+
`);
|
|
3204
|
+
return [
|
|
3205
|
+
"<!doctype html>",
|
|
3206
|
+
'<html lang="en">',
|
|
3207
|
+
"<head>",
|
|
3208
|
+
'<meta charset="utf-8" />',
|
|
3209
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
3210
|
+
`<title>${escapeHtml8(options.title ?? "Voice Trace")}</title>`,
|
|
3211
|
+
"<style>",
|
|
3212
|
+
"body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
|
|
3213
|
+
"main{max-width:1100px;margin:auto}",
|
|
3214
|
+
".summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:.75rem;margin:1rem 0}",
|
|
3215
|
+
".card{background:white;border:1px solid #ded9cc;border-radius:12px;padding:1rem}",
|
|
3216
|
+
".pass{color:#126b3a}.fail{color:#9d2222}",
|
|
3217
|
+
"table{border-collapse:collapse;width:100%;background:white;border:1px solid #ded9cc}",
|
|
3218
|
+
"th,td{border-bottom:1px solid #eee8dc;padding:.65rem;text-align:left;vertical-align:top}",
|
|
3219
|
+
"code{white-space:pre-wrap;word-break:break-word}",
|
|
3220
|
+
"pre{background:#181713;color:#f8f7f2;padding:1rem;border-radius:12px;overflow:auto}",
|
|
3221
|
+
"</style>",
|
|
3222
|
+
"</head>",
|
|
3223
|
+
"<body><main>",
|
|
3224
|
+
`<h1>${escapeHtml8(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
|
|
3225
|
+
`<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
|
|
3226
|
+
'<section class="summary">',
|
|
3227
|
+
`<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
|
|
3228
|
+
`<div class="card"><strong>Turns</strong><br>${summary.turnCount}</div>`,
|
|
3229
|
+
`<div class="card"><strong>Transcripts</strong><br>${summary.transcriptCount}</div>`,
|
|
3230
|
+
`<div class="card"><strong>Tool errors</strong><br>${summary.toolErrorCount}</div>`,
|
|
3231
|
+
`<div class="card"><strong>Cost units</strong><br>${summary.cost.estimatedRelativeCostUnits}</div>`,
|
|
3232
|
+
"</section>",
|
|
3233
|
+
"<h2>Timeline</h2>",
|
|
3234
|
+
"<table><thead><tr><th>Offset ms</th><th>Type</th><th>Turn</th><th>Payload</th></tr></thead><tbody>",
|
|
3235
|
+
eventRows,
|
|
3236
|
+
"</tbody></table>",
|
|
3237
|
+
"<h2>Markdown Export</h2>",
|
|
3238
|
+
`<pre>${escapeHtml8(markdown)}</pre>`,
|
|
3239
|
+
"</main></body></html>"
|
|
3240
|
+
].join(`
|
|
3241
|
+
`);
|
|
3242
|
+
};
|
|
3243
|
+
var buildVoiceTraceReplay = (events, options = {}) => ({
|
|
3244
|
+
evaluation: evaluateVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events, options.evaluation),
|
|
3245
|
+
html: renderVoiceTraceHTML(events, options),
|
|
3246
|
+
markdown: renderVoiceTraceMarkdown(events, options),
|
|
3247
|
+
summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
|
|
3248
|
+
});
|
|
3249
|
+
|
|
3250
|
+
// src/proofTrends.ts
|
|
1498
3251
|
var DEFAULT_VOICE_PROOF_TRENDS_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
1499
3252
|
var DEFAULT_VOICE_PROOF_TREND_PROFILE_DEFINITIONS = [
|
|
1500
3253
|
{
|
|
@@ -1827,6 +3580,9 @@ var buildVoiceRealCallProfileEvidenceFromTraceEvents = (events, options = {}) =>
|
|
|
1827
3580
|
const providerP95Ms = maxNumber(providers.map((provider) => provider.p95Ms));
|
|
1828
3581
|
const runtimeChannel = summarizeRuntimeChannelTraceEvidence(sessionEvents);
|
|
1829
3582
|
const surfaces = readRealCallProfileTraceSurfaces(sessionEvents);
|
|
3583
|
+
if (providers.length === 0 && runtimeChannel === undefined && liveLatencies.length === 0 && surfaces.length === 0) {
|
|
3584
|
+
return;
|
|
3585
|
+
}
|
|
1830
3586
|
return {
|
|
1831
3587
|
generatedAt: new Date(Math.max(...sessionEvents.map((event) => event.at))).toISOString(),
|
|
1832
3588
|
liveP95Ms: percentile(liveLatencies, 95),
|
|
@@ -2291,6 +4047,98 @@ var uniqueVoiceRealCallProfileRecoveryLoopActions = (actions) => {
|
|
|
2291
4047
|
return true;
|
|
2292
4048
|
});
|
|
2293
4049
|
};
|
|
4050
|
+
var normalizeRealCallProfileRecoveryProvider = (role, provider) => {
|
|
4051
|
+
if (!provider) {
|
|
4052
|
+
return;
|
|
4053
|
+
}
|
|
4054
|
+
if (typeof provider === "string") {
|
|
4055
|
+
return {
|
|
4056
|
+
provider,
|
|
4057
|
+
selectedProvider: provider,
|
|
4058
|
+
status: "selected"
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
return {
|
|
4062
|
+
...provider,
|
|
4063
|
+
selectedProvider: provider.selectedProvider ?? provider.provider,
|
|
4064
|
+
status: provider.status ?? "selected"
|
|
4065
|
+
};
|
|
4066
|
+
};
|
|
4067
|
+
var profileRealCallRecoveryEvent = (event, profileId, metadata = {}) => ({
|
|
4068
|
+
...event,
|
|
4069
|
+
metadata: {
|
|
4070
|
+
...metadata,
|
|
4071
|
+
...event.metadata ?? {},
|
|
4072
|
+
benchmarkProfileId: event.metadata?.benchmarkProfileId ?? profileId,
|
|
4073
|
+
profileId: event.metadata?.profileId ?? profileId
|
|
4074
|
+
},
|
|
4075
|
+
payload: {
|
|
4076
|
+
...event.payload ?? {},
|
|
4077
|
+
benchmarkProfileId: event.payload.benchmarkProfileId ?? profileId,
|
|
4078
|
+
profileId: event.payload.profileId ?? profileId
|
|
4079
|
+
}
|
|
4080
|
+
});
|
|
4081
|
+
var appendVoiceRealCallProfileRecoveryEvidence = async (options) => {
|
|
4082
|
+
const at = options.at ?? Date.now();
|
|
4083
|
+
const scenarioId = options.scenarioId ?? "real-call-profile-recovery";
|
|
4084
|
+
const sessionId = options.sessionId ?? `real-call-profile-recovery-${options.profileId}-${new Date(at).toISOString()}`;
|
|
4085
|
+
const browser = options.browser ?? {};
|
|
4086
|
+
const live = options.live ?? {};
|
|
4087
|
+
const providerEvents = [
|
|
4088
|
+
["llm", 320, "live-call"],
|
|
4089
|
+
["stt", 82, "live-stt"],
|
|
4090
|
+
["tts", 45, "live-tts"]
|
|
4091
|
+
].flatMap(([role, defaultElapsedMs, defaultSurface], index) => {
|
|
4092
|
+
const provider = normalizeRealCallProfileRecoveryProvider(role, options.providers?.[role]);
|
|
4093
|
+
if (!provider) {
|
|
4094
|
+
return [];
|
|
4095
|
+
}
|
|
4096
|
+
return [
|
|
4097
|
+
profileRealCallRecoveryEvent(createVoiceProviderDecisionTraceEvent({
|
|
4098
|
+
at: at + index,
|
|
4099
|
+
elapsedMs: provider.elapsedMs ?? defaultElapsedMs,
|
|
4100
|
+
kind: role,
|
|
4101
|
+
provider: provider.provider,
|
|
4102
|
+
reason: provider.reason ?? `Real-call profile recovery selected ${provider.provider} for ${role.toUpperCase()} evidence.`,
|
|
4103
|
+
scenarioId,
|
|
4104
|
+
selectedProvider: provider.selectedProvider,
|
|
4105
|
+
sessionId,
|
|
4106
|
+
status: provider.status,
|
|
4107
|
+
surface: provider.surface ?? defaultSurface
|
|
4108
|
+
}), options.profileId, options.metadata)
|
|
4109
|
+
];
|
|
4110
|
+
});
|
|
4111
|
+
const browserEvents = browser === false ? [] : [
|
|
4112
|
+
profileRealCallRecoveryEvent(createVoiceTraceEvent({
|
|
4113
|
+
at: at + 3,
|
|
4114
|
+
payload: {
|
|
4115
|
+
firstAudioLatencyMs: browser.firstAudioLatencyMs ?? 420,
|
|
4116
|
+
messageCount: browser.messageCount,
|
|
4117
|
+
openSockets: browser.openSockets,
|
|
4118
|
+
receivedBytes: browser.receivedBytes,
|
|
4119
|
+
sentBytes: browser.sentBytes,
|
|
4120
|
+
status: browser.status ?? "pass"
|
|
4121
|
+
},
|
|
4122
|
+
scenarioId,
|
|
4123
|
+
sessionId,
|
|
4124
|
+
type: "client.browser_media"
|
|
4125
|
+
}), options.profileId, options.metadata)
|
|
4126
|
+
];
|
|
4127
|
+
const liveEvents = live === false ? [] : [
|
|
4128
|
+
profileRealCallRecoveryEvent(createVoiceTraceEvent({
|
|
4129
|
+
at: at + 4,
|
|
4130
|
+
payload: {
|
|
4131
|
+
latencyMs: live.latencyMs ?? 420,
|
|
4132
|
+
status: live.status ?? "pass"
|
|
4133
|
+
},
|
|
4134
|
+
scenarioId,
|
|
4135
|
+
sessionId,
|
|
4136
|
+
type: "client.live_latency"
|
|
4137
|
+
}), options.profileId, options.metadata)
|
|
4138
|
+
];
|
|
4139
|
+
const events = await Promise.all([...providerEvents, ...browserEvents, ...liveEvents].map((event) => options.store.append(event)));
|
|
4140
|
+
return { events, sessionId };
|
|
4141
|
+
};
|
|
2294
4142
|
var runVoiceRealCallProfileRecoveryLoop = async (options) => {
|
|
2295
4143
|
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
2296
4144
|
const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
|
|
@@ -3119,7 +4967,7 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
|
|
|
3119
4967
|
}
|
|
3120
4968
|
};
|
|
3121
4969
|
};
|
|
3122
|
-
var
|
|
4970
|
+
var escapeHtml9 = (value) => String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3123
4971
|
var escapeMarkdown = (value) => value.replaceAll("|", "\\|");
|
|
3124
4972
|
var renderVoiceProofTrendRecommendationMarkdown = (report, title = "Voice Provider Runtime Recommendations") => [
|
|
3125
4973
|
`# ${title}`,
|
|
@@ -3152,11 +5000,11 @@ var renderVoiceProofTrendRecommendationMarkdown = (report, title = "Voice Provid
|
|
|
3152
5000
|
].join(`
|
|
3153
5001
|
`);
|
|
3154
5002
|
var renderVoiceProofTrendRecommendationHTML = (report, title = "Voice Provider Runtime Recommendations") => {
|
|
3155
|
-
const cards = report.recommendations.map((recommendation) => `<article class="${
|
|
3156
|
-
const issues = report.issues.length === 0 ? "<li>None</li>" : report.issues.map((issue) => `<li>${
|
|
3157
|
-
const providerRows = report.providers.length === 0 ? "<li>No provider-specific samples were present.</li>" : report.providers.map((provider) => `<li><strong>#${String(provider.rank)} ${
|
|
3158
|
-
const profileRows = report.profiles.length === 0 ? "<li>No benchmark profiles were present.</li>" : report.profiles.map((profile) => `<li><strong>${
|
|
3159
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>${
|
|
5003
|
+
const cards = report.recommendations.map((recommendation) => `<article class="${escapeHtml9(recommendation.status)}"><p class="eyebrow">${escapeHtml9(recommendation.surface)} \xB7 ${escapeHtml9(recommendation.status)}</p><h2>${escapeHtml9(recommendation.recommendation)}</h2><p>${escapeHtml9(recommendation.nextMove)}</p><pre>${escapeHtml9(JSON.stringify(recommendation.evidence, null, 2))}</pre></article>`).join("");
|
|
5004
|
+
const issues = report.issues.length === 0 ? "<li>None</li>" : report.issues.map((issue) => `<li>${escapeHtml9(issue)}</li>`).join("");
|
|
5005
|
+
const providerRows = report.providers.length === 0 ? "<li>No provider-specific samples were present.</li>" : report.providers.map((provider) => `<li><strong>#${String(provider.rank)} ${escapeHtml9(provider.label ?? provider.id)}</strong><span>${escapeHtml9(provider.role ?? "provider")} \xB7 ${escapeHtml9(provider.status)} \xB7 p95 ${escapeHtml9(provider.p95Ms ?? "n/a")}ms \xB7 ${escapeHtml9(provider.samples ?? "n/a")} sample(s)</span><small>${escapeHtml9(provider.nextMove)}</small></li>`).join("");
|
|
5006
|
+
const profileRows = report.profiles.length === 0 ? "<li>No benchmark profiles were present.</li>" : report.profiles.map((profile) => `<li><strong>${escapeHtml9(profile.label ?? profile.id)}</strong><span>${escapeHtml9(profile.status)} \xB7 ${escapeHtml9(formatProviderMix(profile.bestProviders))}</span><small>${escapeHtml9(profile.nextMove)}</small></li>`).join("");
|
|
5007
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>${escapeHtml9(title)}</title><style>body{background:#101418;color:#f7f3e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero,article{background:#17201d;border:1px solid #2e3d36;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(20,184,166,.18),rgba(245,158,11,.12))}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.2rem,6vw,4.7rem);letter-spacing:-.06em;line-height:.92;margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{border:1px solid #42534a;border-radius:999px;padding:8px 12px}.pass{border-color:rgba(34,197,94,.55)}.warn{border-color:rgba(245,158,11,.7)}.fail{border-color:rgba(239,68,68,.75)}pre{background:#0b1110;border-radius:14px;overflow:auto;padding:12px}a{color:#5eead4}li{margin:.45rem 0}li span,li small{display:block;color:#c9d3ca}</style></head><body><main><section class="hero"><p class="eyebrow">Sustained proof recommendations</p><h1>${escapeHtml9(title)}</h1><p>Generated ${escapeHtml9(report.generatedAt)} from ${escapeHtml9(report.source)}.</p><div class="summary"><span class="pill">Status ${escapeHtml9(report.status)}</span><span class="pill">Provider ${report.summary.keepCurrentProviderPath ? "keep" : "change"}</span><span class="pill">Best mix ${escapeHtml9(formatProviderMix(report.bestProviders))}</span><span class="pill">Profiles ${String(report.profiles.length)}</span><span class="pill">Runtime ${report.summary.keepCurrentRuntimeChannel ? "keep" : "tune"}</span><span class="pill">${String(report.summary.recommendedActions)} action(s)</span></div></section>${cards}<section class="hero"><h2>Benchmark Profiles</h2><ul>${profileRows}</ul></section><section class="hero"><h2>Provider Comparison</h2><ul>${providerRows}</ul></section><section class="hero"><h2>Issues</h2><ul>${issues}</ul></section></main></body></html>`;
|
|
3160
5008
|
};
|
|
3161
5009
|
var renderVoiceRealCallProfileHistoryMarkdown = (report, title = "Voice Real-Call Profile History") => [
|
|
3162
5010
|
`# ${title}`,
|
|
@@ -3190,18 +5038,18 @@ var renderVoiceRealCallProfileHistoryMarkdown = (report, title = "Voice Real-Cal
|
|
|
3190
5038
|
].join(`
|
|
3191
5039
|
`);
|
|
3192
5040
|
var renderVoiceRealCallProfileHistoryHTML = (report, title = "Voice Real-Call Profile History") => {
|
|
3193
|
-
const profileRows = report.summary.profiles?.length ? report.summary.profiles.map((profile) => `<tr><td>${
|
|
3194
|
-
const defaultRows = report.defaults.profiles.length > 0 ? report.defaults.profiles.map((profile) => `<tr><td>${
|
|
3195
|
-
const recommendations = report.recommendations.recommendations.map((recommendation) => `<article class="${
|
|
3196
|
-
const issues = report.issues.length === 0 ? "<li>None</li>" : report.issues.map((issue) => `<li>${
|
|
3197
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>${
|
|
5041
|
+
const profileRows = report.summary.profiles?.length ? report.summary.profiles.map((profile) => `<tr><td>${escapeHtml9(profile.label ?? profile.id)}</td><td>${escapeHtml9(profile.status ?? "unknown")}</td><td>${escapeHtml9(profile.maxLiveP95Ms ?? "n/a")}</td><td>${escapeHtml9(profile.maxProviderP95Ms ?? "n/a")}</td><td>${escapeHtml9(profile.maxTurnP95Ms ?? "n/a")}</td><td>${escapeHtml9(formatProviderMix(profile.providers ?? []))}</td></tr>`).join("") : '<tr><td colspan="6">No profiles present.</td></tr>';
|
|
5042
|
+
const defaultRows = report.defaults.profiles.length > 0 ? report.defaults.profiles.map((profile) => `<tr><td>${escapeHtml9(profile.label ?? profile.profileId)}</td><td>${escapeHtml9(profile.status)}</td><td>${escapeHtml9(Object.entries(profile.providerRoutes).map(([role, provider]) => `${role}: ${provider}`).join(", ") || "n/a")}</td><td>${escapeHtml9(profile.latencyBudgets.maxLiveP95Ms ?? "n/a")}</td><td>${escapeHtml9(profile.latencyBudgets.maxProviderP95Ms ?? "n/a")}</td><td>${escapeHtml9(profile.latencyBudgets.maxTurnP95Ms ?? "n/a")}</td></tr>`).join("") : '<tr><td colspan="6">No actionable defaults present.</td></tr>';
|
|
5043
|
+
const recommendations = report.recommendations.recommendations.map((recommendation) => `<article class="${escapeHtml9(recommendation.status)}"><p class="eyebrow">${escapeHtml9(recommendation.surface)} \xB7 ${escapeHtml9(recommendation.status)}</p><h2>${escapeHtml9(recommendation.recommendation)}</h2><p>${escapeHtml9(recommendation.nextMove)}</p></article>`).join("");
|
|
5044
|
+
const issues = report.issues.length === 0 ? "<li>None</li>" : report.issues.map((issue) => `<li>${escapeHtml9(issue)}</li>`).join("");
|
|
5045
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>${escapeHtml9(title)}</title><style>body{background:#111510;color:#f6f0dd;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero,article,.card{background:#182117;border:1px solid #32412d;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(132,204,22,.16),rgba(20,184,166,.12))}.eyebrow{color:#bef264;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.2rem,6vw,4.7rem);letter-spacing:-.06em;line-height:.92;margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{border:1px solid #52624b;border-radius:999px;padding:8px 12px}.pass{border-color:rgba(34,197,94,.55)}.warn{border-color:rgba(245,158,11,.7)}.fail{border-color:rgba(239,68,68,.75)}table{border-collapse:collapse;width:100%}td,th{border-bottom:1px solid #32412d;padding:10px;text-align:left}</style></head><body><main><section class="hero"><p class="eyebrow">Real-call benchmark history</p><h1>${escapeHtml9(title)}</h1><p>Generated ${escapeHtml9(report.generatedAt)} from ${escapeHtml9(report.source)}.</p><div class="summary"><span class="pill">Status ${escapeHtml9(report.status)}</span><span class="pill">Reports ${String(report.reports)}</span><span class="pill">Profiles ${String(report.summary.profileCount)}</span><span class="pill">Defaults ${String(report.defaults.summary.actionableProfiles)}/${String(report.defaults.summary.profileCount)}</span><span class="pill">Cycles ${String(report.summary.cycles ?? 0)}</span><span class="pill">Best mix ${escapeHtml9(formatProviderMix(report.recommendations.bestProviders))}</span></div></section><section class="card"><h2>Profiles</h2><table><thead><tr><th>Profile</th><th>Status</th><th>Live p95</th><th>Provider p95</th><th>Turn p95</th><th>Provider mix</th></tr></thead><tbody>${profileRows}</tbody></table></section><section class="card"><h2>Actionable Defaults</h2><table><thead><tr><th>Profile</th><th>Status</th><th>Provider routes</th><th>Live budget</th><th>Provider budget</th><th>Turn budget</th></tr></thead><tbody>${defaultRows}</tbody></table></section>${recommendations}<section class="card"><h2>Issues</h2><ul>${issues}</ul></section></main></body></html>`;
|
|
3198
5046
|
};
|
|
3199
5047
|
var createVoiceProofTrendRecommendationRoutes = (options) => {
|
|
3200
5048
|
const path = options.path ?? "/api/voice/proof-trend-recommendations";
|
|
3201
5049
|
const htmlPath = options.htmlPath === undefined ? "/voice/proof-trend-recommendations" : options.htmlPath;
|
|
3202
5050
|
const markdownPath = options.markdownPath === undefined ? "/voice/proof-trend-recommendations.md" : options.markdownPath;
|
|
3203
5051
|
const title = options.title ?? "Voice Provider Runtime Recommendations";
|
|
3204
|
-
const routes = new
|
|
5052
|
+
const routes = new Elysia4({
|
|
3205
5053
|
name: options.name ?? "absolutejs-voice-proof-trend-recommendations"
|
|
3206
5054
|
});
|
|
3207
5055
|
const loadReport = async () => {
|
|
@@ -3243,7 +5091,7 @@ var createVoiceRealCallProfileHistoryRoutes = (options = {}) => {
|
|
|
3243
5091
|
const htmlPath = options.htmlPath === undefined ? "/voice/real-call-profile-history" : options.htmlPath;
|
|
3244
5092
|
const markdownPath = options.markdownPath === undefined ? "/voice/real-call-profile-history.md" : options.markdownPath;
|
|
3245
5093
|
const title = options.title ?? "Voice Real-Call Profile History";
|
|
3246
|
-
const routes = new
|
|
5094
|
+
const routes = new Elysia4({
|
|
3247
5095
|
name: options.name ?? "absolutejs-voice-real-call-profile-history"
|
|
3248
5096
|
});
|
|
3249
5097
|
const loadReport = async () => {
|
|
@@ -3295,7 +5143,7 @@ var loadVoiceRealCallProfileHistoryRouteReport = async (options) => {
|
|
|
3295
5143
|
};
|
|
3296
5144
|
var createVoiceRealCallProfileRecoveryActionRoutes = (options = {}) => {
|
|
3297
5145
|
const path = options.path ?? "/api/voice/real-call-profile-history";
|
|
3298
|
-
const routes = new
|
|
5146
|
+
const routes = new Elysia4({
|
|
3299
5147
|
name: options.name ?? "absolutejs-voice-real-call-profile-recovery-actions"
|
|
3300
5148
|
});
|
|
3301
5149
|
const actionPath = (actionId) => `${path}${realCallProfileActionPaths[actionId]}`;
|
|
@@ -3470,7 +5318,7 @@ var createVoiceRealCallProfileRecoveryActionRoutes = (options = {}) => {
|
|
|
3470
5318
|
};
|
|
3471
5319
|
var createVoiceProofTrendRoutes = (options) => {
|
|
3472
5320
|
const path = options.path ?? "/api/voice/proof-trends";
|
|
3473
|
-
const routes = new
|
|
5321
|
+
const routes = new Elysia4({
|
|
3474
5322
|
name: options.name ?? "absolutejs-voice-proof-trends"
|
|
3475
5323
|
});
|
|
3476
5324
|
routes.get(path, async () => {
|
|
@@ -3510,7 +5358,7 @@ var DEFAULT_LINKS2 = [
|
|
|
3510
5358
|
{ href: "/voice/proof-trends", label: "Trend page" },
|
|
3511
5359
|
{ href: "/api/voice/proof-trends", label: "Trend JSON" }
|
|
3512
5360
|
];
|
|
3513
|
-
var
|
|
5361
|
+
var escapeHtml10 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3514
5362
|
var formatMs = (value) => typeof value === "number" && Number.isFinite(value) ? `${Math.round(value)}ms` : "n/a";
|
|
3515
5363
|
var statusLabel = (report) => {
|
|
3516
5364
|
if (!report) {
|
|
@@ -3560,19 +5408,19 @@ var createVoiceProofTrendsViewModel = (snapshot, options = {}) => {
|
|
|
3560
5408
|
var renderVoiceProofTrendsHTML = (snapshot, options = {}) => {
|
|
3561
5409
|
const model = createVoiceProofTrendsViewModel(snapshot, options);
|
|
3562
5410
|
const metrics = model.metrics.length ? `<div class="absolute-voice-proof-trends__metrics">${model.metrics.map((metric) => `<article>
|
|
3563
|
-
<span>${
|
|
3564
|
-
<strong>${
|
|
3565
|
-
</article>`).join("")}</div>` : `<p class="absolute-voice-proof-trends__empty">${model.error ?
|
|
3566
|
-
const links = model.links.length ? `<p class="absolute-voice-proof-trends__links">${model.links.map((link) => `<a href="${
|
|
3567
|
-
return `<section class="absolute-voice-proof-trends absolute-voice-proof-trends--${
|
|
5411
|
+
<span>${escapeHtml10(metric.label)}</span>
|
|
5412
|
+
<strong>${escapeHtml10(metric.value)}</strong>
|
|
5413
|
+
</article>`).join("")}</div>` : `<p class="absolute-voice-proof-trends__empty">${model.error ? escapeHtml10(model.error) : "Run the sustained proof trends script to populate evidence."}</p>`;
|
|
5414
|
+
const links = model.links.length ? `<p class="absolute-voice-proof-trends__links">${model.links.map((link) => `<a href="${escapeHtml10(link.href)}">${escapeHtml10(link.label)}</a>`).join("")}</p>` : "";
|
|
5415
|
+
return `<section class="absolute-voice-proof-trends absolute-voice-proof-trends--${escapeHtml10(model.status)}">
|
|
3568
5416
|
<header class="absolute-voice-proof-trends__header">
|
|
3569
|
-
<span class="absolute-voice-proof-trends__eyebrow">${
|
|
3570
|
-
<strong class="absolute-voice-proof-trends__label">${
|
|
5417
|
+
<span class="absolute-voice-proof-trends__eyebrow">${escapeHtml10(model.title)}</span>
|
|
5418
|
+
<strong class="absolute-voice-proof-trends__label">${escapeHtml10(model.label)}</strong>
|
|
3571
5419
|
</header>
|
|
3572
|
-
<p class="absolute-voice-proof-trends__description">${
|
|
5420
|
+
<p class="absolute-voice-proof-trends__description">${escapeHtml10(model.description)}</p>
|
|
3573
5421
|
${metrics}
|
|
3574
5422
|
${links}
|
|
3575
|
-
${model.error ? `<p class="absolute-voice-proof-trends__error">${
|
|
5423
|
+
${model.error ? `<p class="absolute-voice-proof-trends__error">${escapeHtml10(model.error)}</p>` : ""}
|
|
3576
5424
|
</section>`;
|
|
3577
5425
|
};
|
|
3578
5426
|
var getVoiceProofTrendsCSS = () => `.absolute-voice-proof-trends{border:1px solid #99f6e4;border-radius:20px;background:#f0fdfa;color:#0f172a;padding:18px;box-shadow:0 18px 40px rgba(13,148,136,.12);font-family:inherit}.absolute-voice-proof-trends--warning,.absolute-voice-proof-trends--error{border-color:#f2a7a7;background:#fff7f4}.absolute-voice-proof-trends__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-proof-trends__eyebrow{color:#0f766e;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-proof-trends__label{font-size:24px;line-height:1}.absolute-voice-proof-trends__description,.absolute-voice-proof-trends__empty{color:#475569}.absolute-voice-proof-trends__metrics{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));margin-top:14px}.absolute-voice-proof-trends__metrics article{background:#fff;border:1px solid #ccfbf1;border-radius:16px;padding:12px}.absolute-voice-proof-trends__metrics span{color:#64748b;display:block;font-size:12px;font-weight:800;text-transform:uppercase}.absolute-voice-proof-trends__metrics strong{display:block;font-size:20px;margin-top:4px}.absolute-voice-proof-trends__links{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 0}.absolute-voice-proof-trends__links a{border:1px solid #99f6e4;border-radius:999px;color:#0f766e;font-weight:800;padding:6px 10px;text-decoration:none}.absolute-voice-proof-trends__error{color:#9f1239;font-weight:700}`;
|
|
@@ -3757,7 +5605,7 @@ var DEFAULT_LINKS3 = [
|
|
|
3757
5605
|
{ href: "/voice/real-call-profile-history", label: "Profile history" },
|
|
3758
5606
|
{ href: "/api/voice/real-call-profile-history", label: "JSON" }
|
|
3759
5607
|
];
|
|
3760
|
-
var
|
|
5608
|
+
var escapeHtml11 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3761
5609
|
var formatMs2 = (value) => typeof value === "number" && Number.isFinite(value) ? `${Math.round(value)}ms` : "n/a";
|
|
3762
5610
|
var formatProviderRoutes = (profile) => Object.entries(profile.providerRoutes).map(([role, provider]) => `${role}: ${provider}`).join(", ") || "No complete route yet";
|
|
3763
5611
|
var createProfileView = (profile) => ({
|
|
@@ -3788,25 +5636,25 @@ var createVoiceProfileComparisonViewModel = (snapshot, options = {}) => {
|
|
|
3788
5636
|
};
|
|
3789
5637
|
var renderVoiceProfileComparisonHTML = (snapshot, options = {}) => {
|
|
3790
5638
|
const model = createVoiceProfileComparisonViewModel(snapshot, options);
|
|
3791
|
-
const profiles = model.profiles.length ? `<div class="absolute-voice-profile-comparison__profiles">${model.profiles.map((profile) => `<article class="absolute-voice-profile-comparison__profile absolute-voice-profile-comparison__profile--${
|
|
5639
|
+
const profiles = model.profiles.length ? `<div class="absolute-voice-profile-comparison__profiles">${model.profiles.map((profile) => `<article class="absolute-voice-profile-comparison__profile absolute-voice-profile-comparison__profile--${escapeHtml11(profile.status)}">
|
|
3792
5640
|
<header>
|
|
3793
|
-
<span>${
|
|
3794
|
-
<strong>${
|
|
5641
|
+
<span>${escapeHtml11(profile.status)}</span>
|
|
5642
|
+
<strong>${escapeHtml11(profile.label)}</strong>
|
|
3795
5643
|
</header>
|
|
3796
|
-
<p>${
|
|
3797
|
-
<div>${profile.evidence.map((metric) => `<span><small>${
|
|
3798
|
-
<em>${
|
|
3799
|
-
</article>`).join("")}</div>` : `<p class="absolute-voice-profile-comparison__empty">${model.error ?
|
|
3800
|
-
const links = model.links.length ? `<p class="absolute-voice-profile-comparison__links">${model.links.map((link) => `<a href="${
|
|
3801
|
-
return `<section class="absolute-voice-profile-comparison absolute-voice-profile-comparison--${
|
|
5644
|
+
<p>${escapeHtml11(profile.providerRoutes)}</p>
|
|
5645
|
+
<div>${profile.evidence.map((metric) => `<span><small>${escapeHtml11(metric.label)}</small><b>${escapeHtml11(metric.value)}</b></span>`).join("")}</div>
|
|
5646
|
+
<em>${escapeHtml11(profile.nextMove)}</em>
|
|
5647
|
+
</article>`).join("")}</div>` : `<p class="absolute-voice-profile-comparison__empty">${model.error ? escapeHtml11(model.error) : "Run real-call profile collection to populate profile comparisons."}</p>`;
|
|
5648
|
+
const links = model.links.length ? `<p class="absolute-voice-profile-comparison__links">${model.links.map((link) => `<a href="${escapeHtml11(link.href)}">${escapeHtml11(link.label)}</a>`).join("")}</p>` : "";
|
|
5649
|
+
return `<section class="absolute-voice-profile-comparison absolute-voice-profile-comparison--${escapeHtml11(model.status)}">
|
|
3802
5650
|
<header class="absolute-voice-profile-comparison__header">
|
|
3803
|
-
<span class="absolute-voice-profile-comparison__eyebrow">${
|
|
3804
|
-
<strong class="absolute-voice-profile-comparison__label">${
|
|
5651
|
+
<span class="absolute-voice-profile-comparison__eyebrow">${escapeHtml11(model.title)}</span>
|
|
5652
|
+
<strong class="absolute-voice-profile-comparison__label">${escapeHtml11(model.label)}</strong>
|
|
3805
5653
|
</header>
|
|
3806
|
-
<p class="absolute-voice-profile-comparison__description">${
|
|
5654
|
+
<p class="absolute-voice-profile-comparison__description">${escapeHtml11(model.description)}</p>
|
|
3807
5655
|
${profiles}
|
|
3808
5656
|
${links}
|
|
3809
|
-
${model.error ? `<p class="absolute-voice-profile-comparison__error">${
|
|
5657
|
+
${model.error ? `<p class="absolute-voice-profile-comparison__error">${escapeHtml11(model.error)}</p>` : ""}
|
|
3810
5658
|
</section>`;
|
|
3811
5659
|
};
|
|
3812
5660
|
var getVoiceProfileComparisonCSS = () => `.absolute-voice-profile-comparison{border:1px solid #c7d2fe;border-radius:20px;background:#eef2ff;color:#111827;padding:18px;box-shadow:0 18px 40px rgba(79,70,229,.12);font-family:inherit}.absolute-voice-profile-comparison--warning,.absolute-voice-profile-comparison--error{border-color:#fbbf24;background:#fffbeb}.absolute-voice-profile-comparison__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-profile-comparison__eyebrow{color:#4338ca;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-profile-comparison__label{font-size:24px;line-height:1}.absolute-voice-profile-comparison__description,.absolute-voice-profile-comparison__empty{color:#4b5563}.absolute-voice-profile-comparison__profiles{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));margin-top:14px}.absolute-voice-profile-comparison__profile{background:#fff;border:1px solid #c7d2fe;border-radius:16px;padding:14px}.absolute-voice-profile-comparison__profile--warn{border-color:#fbbf24}.absolute-voice-profile-comparison__profile--fail{border-color:#f87171}.absolute-voice-profile-comparison__profile header{align-items:center;display:flex;gap:8px;justify-content:space-between}.absolute-voice-profile-comparison__profile header span{border:1px solid currentColor;border-radius:999px;color:#4338ca;font-size:11px;font-weight:900;padding:3px 7px;text-transform:uppercase}.absolute-voice-profile-comparison__profile p{color:#1f2937;font-weight:800;overflow-wrap:anywhere}.absolute-voice-profile-comparison__profile div{display:grid;gap:8px;grid-template-columns:repeat(3,minmax(0,1fr))}.absolute-voice-profile-comparison__profile small{color:#6b7280;display:block;font-size:11px}.absolute-voice-profile-comparison__profile b{display:block}.absolute-voice-profile-comparison__profile em{color:#4b5563;display:block;font-size:13px;margin-top:12px}.absolute-voice-profile-comparison__links{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 0}.absolute-voice-profile-comparison__links a{border:1px solid #a5b4fc;border-radius:999px;color:#4338ca;font-weight:800;padding:6px 10px;text-decoration:none}.absolute-voice-profile-comparison__error{color:#9f1239;font-weight:700}`;
|
|
@@ -4029,27 +5877,27 @@ var createVoiceProfileSwitchRecommendationStore = (path = "/api/voice/profile-sw
|
|
|
4029
5877
|
// src/client/profileSwitchRecommendationWidget.ts
|
|
4030
5878
|
var DEFAULT_TITLE7 = "Profile Switch Recommendation";
|
|
4031
5879
|
var DEFAULT_DESCRIPTION7 = "Compares the current session against measured profile evidence and recommends whether to switch stacks.";
|
|
4032
|
-
var
|
|
5880
|
+
var escapeHtml12 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4033
5881
|
var formatRoute = (routes) => routes ? Object.entries(routes).map(([role, provider]) => `${role}: ${provider}`).join(", ") : "No route";
|
|
4034
5882
|
var renderVoiceProfileSwitchRecommendationHTML = (snapshot, options = {}) => {
|
|
4035
5883
|
const recommendation = snapshot.recommendation;
|
|
4036
5884
|
const status = snapshot.error ? "error" : recommendation ? recommendation.status : snapshot.isLoading ? "loading" : "empty";
|
|
4037
5885
|
const label = snapshot.error ? "Unavailable" : recommendation ? recommendation.status === "switch" ? `Switch to ${recommendation.recommendedProfile?.label ?? recommendation.recommendedProfile?.profileId ?? "recommended profile"}` : recommendation.status === "stay" ? "Keep current profile" : "Needs evidence" : snapshot.isLoading ? "Checking" : "No recommendation";
|
|
4038
5886
|
const body = recommendation ? `<div class="absolute-voice-profile-switch__body">
|
|
4039
|
-
<p><strong>Current:</strong> ${
|
|
4040
|
-
<p><strong>Recommended:</strong> ${
|
|
4041
|
-
<p><strong>Routes:</strong> ${
|
|
4042
|
-
<ul>${recommendation.reasons.map((reason) => `<li>${
|
|
4043
|
-
<em>${
|
|
4044
|
-
</div>` : `<p class="absolute-voice-profile-switch__empty">${
|
|
4045
|
-
return `<section class="absolute-voice-profile-switch absolute-voice-profile-switch--${
|
|
5887
|
+
<p><strong>Current:</strong> ${escapeHtml12(recommendation.currentProfile?.label ?? recommendation.currentProfile?.profileId ?? "Unknown")}</p>
|
|
5888
|
+
<p><strong>Recommended:</strong> ${escapeHtml12(recommendation.recommendedProfile?.label ?? recommendation.recommendedProfile?.profileId ?? "None")}</p>
|
|
5889
|
+
<p><strong>Routes:</strong> ${escapeHtml12(formatRoute(recommendation.recommendedProfile?.providerRoutes))}</p>
|
|
5890
|
+
<ul>${recommendation.reasons.map((reason) => `<li>${escapeHtml12(reason)}</li>`).join("")}</ul>
|
|
5891
|
+
<em>${escapeHtml12(recommendation.nextMove)}</em>
|
|
5892
|
+
</div>` : `<p class="absolute-voice-profile-switch__empty">${escapeHtml12(snapshot.error ?? "Run session traffic to populate a recommendation.")}</p>`;
|
|
5893
|
+
return `<section class="absolute-voice-profile-switch absolute-voice-profile-switch--${escapeHtml12(status)}">
|
|
4046
5894
|
<header class="absolute-voice-profile-switch__header">
|
|
4047
|
-
<span class="absolute-voice-profile-switch__eyebrow">${
|
|
4048
|
-
<strong class="absolute-voice-profile-switch__label">${
|
|
5895
|
+
<span class="absolute-voice-profile-switch__eyebrow">${escapeHtml12(options.title ?? DEFAULT_TITLE7)}</span>
|
|
5896
|
+
<strong class="absolute-voice-profile-switch__label">${escapeHtml12(label)}</strong>
|
|
4049
5897
|
</header>
|
|
4050
|
-
<p class="absolute-voice-profile-switch__description">${
|
|
5898
|
+
<p class="absolute-voice-profile-switch__description">${escapeHtml12(options.description ?? DEFAULT_DESCRIPTION7)}</p>
|
|
4051
5899
|
${body}
|
|
4052
|
-
${snapshot.error ? `<p class="absolute-voice-profile-switch__error">${
|
|
5900
|
+
${snapshot.error ? `<p class="absolute-voice-profile-switch__error">${escapeHtml12(snapshot.error)}</p>` : ""}
|
|
4053
5901
|
</section>`;
|
|
4054
5902
|
};
|
|
4055
5903
|
var getVoiceProfileSwitchRecommendationCSS = () => `.absolute-voice-profile-switch{border:1px solid #fed7aa;border-radius:20px;background:#fff7ed;color:#1c1917;padding:18px;box-shadow:0 18px 40px rgba(234,88,12,.12);font-family:inherit}.absolute-voice-profile-switch--switch{border-color:#fdba74}.absolute-voice-profile-switch--stay{border-color:#86efac;background:#f0fdf4}.absolute-voice-profile-switch--warn,.absolute-voice-profile-switch--error{border-color:#fca5a5;background:#fff1f2}.absolute-voice-profile-switch__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-profile-switch__eyebrow{color:#c2410c;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-profile-switch__label{font-size:24px;line-height:1}.absolute-voice-profile-switch__description,.absolute-voice-profile-switch__body em,.absolute-voice-profile-switch__empty{color:#57534e}.absolute-voice-profile-switch__body{background:#fff;border:1px solid #fed7aa;border-radius:16px;margin-top:14px;padding:14px}.absolute-voice-profile-switch__body p{margin:.35rem 0}.absolute-voice-profile-switch__body ul{margin:.75rem 0;padding-left:1.2rem}.absolute-voice-profile-switch__body em{display:block}.absolute-voice-profile-switch__error{color:#9f1239;font-weight:700}`;
|
|
@@ -4204,7 +6052,7 @@ var DEFAULT_LINKS4 = [
|
|
|
4204
6052
|
{ href: "/production-readiness", label: "Readiness page" },
|
|
4205
6053
|
{ href: "/voice/slo-readiness-thresholds", label: "Gate thresholds" }
|
|
4206
6054
|
];
|
|
4207
|
-
var
|
|
6055
|
+
var escapeHtml13 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4208
6056
|
var formatExplanationValue = (value, unit) => {
|
|
4209
6057
|
if (value === undefined || value === null) {
|
|
4210
6058
|
return "n/a";
|
|
@@ -4245,23 +6093,23 @@ var createVoiceReadinessFailuresViewModel = (snapshot, options = {}) => {
|
|
|
4245
6093
|
};
|
|
4246
6094
|
var renderVoiceReadinessFailuresHTML = (snapshot, options = {}) => {
|
|
4247
6095
|
const model = createVoiceReadinessFailuresViewModel(snapshot, options);
|
|
4248
|
-
const failures = model.failures.length ? `<div class="absolute-voice-readiness-failures__items">${model.failures.map((failure) => `<article class="absolute-voice-readiness-failures__item absolute-voice-readiness-failures__item--${
|
|
4249
|
-
<span>${
|
|
4250
|
-
<strong>${
|
|
4251
|
-
<p>Observed ${
|
|
4252
|
-
<p>${
|
|
4253
|
-
<p class="absolute-voice-readiness-failures__links">${failure.evidenceHref ? `<a href="${
|
|
4254
|
-
</article>`).join("")}</div>` : `<p class="absolute-voice-readiness-failures__empty">${model.error ?
|
|
4255
|
-
const links = model.links.length ? `<p class="absolute-voice-readiness-failures__links">${model.links.map((link) => `<a href="${
|
|
4256
|
-
return `<section class="absolute-voice-readiness-failures absolute-voice-readiness-failures--${
|
|
6096
|
+
const failures = model.failures.length ? `<div class="absolute-voice-readiness-failures__items">${model.failures.map((failure) => `<article class="absolute-voice-readiness-failures__item absolute-voice-readiness-failures__item--${escapeHtml13(failure.status)}">
|
|
6097
|
+
<span>${escapeHtml13(failure.status.toUpperCase())}</span>
|
|
6098
|
+
<strong>${escapeHtml13(failure.label)}</strong>
|
|
6099
|
+
<p>Observed ${escapeHtml13(failure.observed)} against ${escapeHtml13(failure.thresholdLabel)} ${escapeHtml13(failure.threshold)}.</p>
|
|
6100
|
+
<p>${escapeHtml13(failure.remediation)}</p>
|
|
6101
|
+
<p class="absolute-voice-readiness-failures__links">${failure.evidenceHref ? `<a href="${escapeHtml13(failure.evidenceHref)}">Evidence</a>` : ""}${failure.sourceHref ? `<a href="${escapeHtml13(failure.sourceHref)}">Threshold source</a>` : ""}</p>
|
|
6102
|
+
</article>`).join("")}</div>` : `<p class="absolute-voice-readiness-failures__empty">${model.error ? escapeHtml13(model.error) : "No calibrated readiness gate explanations are open."}</p>`;
|
|
6103
|
+
const links = model.links.length ? `<p class="absolute-voice-readiness-failures__links">${model.links.map((link) => `<a href="${escapeHtml13(link.href)}">${escapeHtml13(link.label)}</a>`).join("")}</p>` : "";
|
|
6104
|
+
return `<section class="absolute-voice-readiness-failures absolute-voice-readiness-failures--${escapeHtml13(model.status)}">
|
|
4257
6105
|
<header class="absolute-voice-readiness-failures__header">
|
|
4258
|
-
<span class="absolute-voice-readiness-failures__eyebrow">${
|
|
4259
|
-
<strong class="absolute-voice-readiness-failures__label">${
|
|
6106
|
+
<span class="absolute-voice-readiness-failures__eyebrow">${escapeHtml13(model.title)}</span>
|
|
6107
|
+
<strong class="absolute-voice-readiness-failures__label">${escapeHtml13(model.label)}</strong>
|
|
4260
6108
|
</header>
|
|
4261
|
-
<p class="absolute-voice-readiness-failures__description">${
|
|
6109
|
+
<p class="absolute-voice-readiness-failures__description">${escapeHtml13(model.description)}</p>
|
|
4262
6110
|
${failures}
|
|
4263
6111
|
${links}
|
|
4264
|
-
${model.error ? `<p class="absolute-voice-readiness-failures__error">${
|
|
6112
|
+
${model.error ? `<p class="absolute-voice-readiness-failures__error">${escapeHtml13(model.error)}</p>` : ""}
|
|
4265
6113
|
</section>`;
|
|
4266
6114
|
};
|
|
4267
6115
|
var getVoiceReadinessFailuresCSS = () => `.absolute-voice-readiness-failures{border:1px solid #fed7aa;border-radius:20px;background:#fff7ed;color:#1c1917;padding:18px;box-shadow:0 18px 40px rgba(234,88,12,.12);font-family:inherit}.absolute-voice-readiness-failures--ready{border-color:#86efac;background:#f0fdf4}.absolute-voice-readiness-failures--error{border-color:#fda4af;background:#fff1f2}.absolute-voice-readiness-failures__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-readiness-failures__eyebrow{color:#9a3412;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-readiness-failures__label{font-size:24px;line-height:1}.absolute-voice-readiness-failures__description,.absolute-voice-readiness-failures__empty{color:#57534e}.absolute-voice-readiness-failures__items{display:grid;gap:10px;margin-top:14px}.absolute-voice-readiness-failures__item{background:white;border:1px solid #fed7aa;border-radius:16px;padding:12px}.absolute-voice-readiness-failures__item--fail{border-color:#fb7185}.absolute-voice-readiness-failures__item span{color:#9a3412;display:block;font-size:12px;font-weight:900;text-transform:uppercase}.absolute-voice-readiness-failures__item strong{display:block;font-size:18px;margin-top:4px}.absolute-voice-readiness-failures__item p{margin:.45rem 0 0}.absolute-voice-readiness-failures__links{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 0}.absolute-voice-readiness-failures__links a{border:1px solid #fdba74;border-radius:999px;color:#9a3412;font-weight:800;padding:6px 10px;text-decoration:none}.absolute-voice-readiness-failures__error{color:#9f1239;font-weight:700}`;
|
|
@@ -4490,7 +6338,7 @@ var createVoiceProviderSimulationControlsStore = (options) => {
|
|
|
4490
6338
|
};
|
|
4491
6339
|
|
|
4492
6340
|
// src/client/providerSimulationControlsWidget.ts
|
|
4493
|
-
var
|
|
6341
|
+
var escapeHtml14 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4494
6342
|
var formatKind = (kind) => (kind ?? "stt").toUpperCase();
|
|
4495
6343
|
var createVoiceProviderSimulationControlsViewModel = (snapshot, options) => {
|
|
4496
6344
|
const configuredProviders = options.providers.filter((provider) => provider.configured !== false);
|
|
@@ -4510,18 +6358,18 @@ var createVoiceProviderSimulationControlsViewModel = (snapshot, options) => {
|
|
|
4510
6358
|
};
|
|
4511
6359
|
var renderVoiceProviderSimulationControlsHTML = (snapshot, options) => {
|
|
4512
6360
|
const model = createVoiceProviderSimulationControlsViewModel(snapshot, options);
|
|
4513
|
-
const failureButtons = model.failureProviders.map((provider) => `<button type="button" data-voice-provider-fail="${
|
|
4514
|
-
const recoveryButtons = model.providers.map((provider) => `<button type="button" data-voice-provider-recover="${
|
|
6361
|
+
const failureButtons = model.failureProviders.map((provider) => `<button type="button" data-voice-provider-fail="${escapeHtml14(provider.provider)}"${!model.canSimulateFailure || snapshot.isRunning ? " disabled" : ""}>Simulate ${escapeHtml14(provider.provider)} ${escapeHtml14(formatKind(options.kind))} failure</button>`).join("");
|
|
6362
|
+
const recoveryButtons = model.providers.map((provider) => `<button type="button" data-voice-provider-recover="${escapeHtml14(provider.provider)}"${snapshot.isRunning ? " disabled" : ""}>Mark ${escapeHtml14(provider.provider)} recovered</button>`).join("");
|
|
4515
6363
|
return `<section class="absolute-voice-provider-simulation absolute-voice-provider-simulation--${snapshot.error ? "error" : snapshot.isRunning ? "running" : "ready"}">
|
|
4516
6364
|
<header class="absolute-voice-provider-simulation__header">
|
|
4517
|
-
<span class="absolute-voice-provider-simulation__eyebrow">${
|
|
4518
|
-
<strong class="absolute-voice-provider-simulation__label">${
|
|
6365
|
+
<span class="absolute-voice-provider-simulation__eyebrow">${escapeHtml14(model.title)}</span>
|
|
6366
|
+
<strong class="absolute-voice-provider-simulation__label">${escapeHtml14(model.label)}</strong>
|
|
4519
6367
|
</header>
|
|
4520
|
-
<p class="absolute-voice-provider-simulation__description">${
|
|
4521
|
-
${model.canSimulateFailure ? "" : `<p class="absolute-voice-provider-simulation__empty">${
|
|
6368
|
+
<p class="absolute-voice-provider-simulation__description">${escapeHtml14(model.description)}</p>
|
|
6369
|
+
${model.canSimulateFailure ? "" : `<p class="absolute-voice-provider-simulation__empty">${escapeHtml14(options.fallbackRequiredMessage ?? "Configure fallback providers before simulating failure.")}</p>`}
|
|
4522
6370
|
<div class="absolute-voice-provider-simulation__actions">${failureButtons}${recoveryButtons}</div>
|
|
4523
|
-
${snapshot.error ? `<p class="absolute-voice-provider-simulation__error">${
|
|
4524
|
-
${model.resultText ? `<pre class="absolute-voice-provider-simulation__result">${
|
|
6371
|
+
${snapshot.error ? `<p class="absolute-voice-provider-simulation__error">${escapeHtml14(snapshot.error)}</p>` : ""}
|
|
6372
|
+
${model.resultText ? `<pre class="absolute-voice-provider-simulation__result">${escapeHtml14(model.resultText)}</pre>` : ""}
|
|
4525
6373
|
</section>`;
|
|
4526
6374
|
};
|
|
4527
6375
|
var bindVoiceProviderSimulationControls = (element, store) => {
|
|
@@ -4781,7 +6629,7 @@ var useVoiceProviderCapabilities = (path = "/api/provider-capabilities", options
|
|
|
4781
6629
|
// src/client/providerCapabilitiesWidget.ts
|
|
4782
6630
|
var DEFAULT_TITLE9 = "Provider Capabilities";
|
|
4783
6631
|
var DEFAULT_DESCRIPTION9 = "Configured, selected, and healthy voice providers for this deployment.";
|
|
4784
|
-
var
|
|
6632
|
+
var escapeHtml15 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4785
6633
|
var formatProvider = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
|
|
4786
6634
|
var formatKind2 = (kind) => kind.toUpperCase();
|
|
4787
6635
|
var formatStatus2 = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
@@ -4836,25 +6684,25 @@ var createVoiceProviderCapabilitiesViewModel = (snapshot, options = {}) => {
|
|
|
4836
6684
|
};
|
|
4837
6685
|
var renderVoiceProviderCapabilitiesHTML = (snapshot, options = {}) => {
|
|
4838
6686
|
const model = createVoiceProviderCapabilitiesViewModel(snapshot, options);
|
|
4839
|
-
const capabilities = model.capabilities.length ? `<div class="absolute-voice-provider-capabilities__providers">${model.capabilities.map((capability) => `<article class="absolute-voice-provider-capabilities__provider absolute-voice-provider-capabilities__provider--${
|
|
6687
|
+
const capabilities = model.capabilities.length ? `<div class="absolute-voice-provider-capabilities__providers">${model.capabilities.map((capability) => `<article class="absolute-voice-provider-capabilities__provider absolute-voice-provider-capabilities__provider--${escapeHtml15(capability.status)}">
|
|
4840
6688
|
<header>
|
|
4841
|
-
<strong>${
|
|
4842
|
-
<span>${
|
|
6689
|
+
<strong>${escapeHtml15(capability.label)}</strong>
|
|
6690
|
+
<span>${escapeHtml15(formatStatus2(capability.status))}</span>
|
|
4843
6691
|
</header>
|
|
4844
|
-
<p>${
|
|
6692
|
+
<p>${escapeHtml15(capability.detail)}</p>
|
|
4845
6693
|
<dl>${capability.rows.map((row) => `<div>
|
|
4846
|
-
<dt>${
|
|
4847
|
-
<dd>${
|
|
6694
|
+
<dt>${escapeHtml15(row.label)}</dt>
|
|
6695
|
+
<dd>${escapeHtml15(row.value)}</dd>
|
|
4848
6696
|
</div>`).join("")}</dl>
|
|
4849
6697
|
</article>`).join("")}</div>` : '<p class="absolute-voice-provider-capabilities__empty">Configure provider capabilities to see deployment coverage.</p>';
|
|
4850
|
-
return `<section class="absolute-voice-provider-capabilities absolute-voice-provider-capabilities--${
|
|
6698
|
+
return `<section class="absolute-voice-provider-capabilities absolute-voice-provider-capabilities--${escapeHtml15(model.status)}">
|
|
4851
6699
|
<header class="absolute-voice-provider-capabilities__header">
|
|
4852
|
-
<span class="absolute-voice-provider-capabilities__eyebrow">${
|
|
4853
|
-
<strong class="absolute-voice-provider-capabilities__label">${
|
|
6700
|
+
<span class="absolute-voice-provider-capabilities__eyebrow">${escapeHtml15(model.title)}</span>
|
|
6701
|
+
<strong class="absolute-voice-provider-capabilities__label">${escapeHtml15(model.label)}</strong>
|
|
4854
6702
|
</header>
|
|
4855
|
-
<p class="absolute-voice-provider-capabilities__description">${
|
|
6703
|
+
<p class="absolute-voice-provider-capabilities__description">${escapeHtml15(model.description)}</p>
|
|
4856
6704
|
${capabilities}
|
|
4857
|
-
${model.error ? `<p class="absolute-voice-provider-capabilities__error">${
|
|
6705
|
+
${model.error ? `<p class="absolute-voice-provider-capabilities__error">${escapeHtml15(model.error)}</p>` : ""}
|
|
4858
6706
|
</section>`;
|
|
4859
6707
|
};
|
|
4860
6708
|
var getVoiceProviderCapabilitiesCSS = () => `.absolute-voice-provider-capabilities{border:1px solid #bfd7ea;border-radius:20px;background:#f6fbff;color:#08131f;padding:18px;box-shadow:0 18px 40px rgba(14,51,78,.12);font-family:inherit}.absolute-voice-provider-capabilities--error,.absolute-voice-provider-capabilities--warning{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-provider-capabilities__header,.absolute-voice-provider-capabilities__provider header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-provider-capabilities__eyebrow{color:#255f85;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-provider-capabilities__label{font-size:24px;line-height:1}.absolute-voice-provider-capabilities__description,.absolute-voice-provider-capabilities__provider p,.absolute-voice-provider-capabilities__provider dt,.absolute-voice-provider-capabilities__empty{color:#405467}.absolute-voice-provider-capabilities__providers{display:grid;gap:12px;margin-top:14px}.absolute-voice-provider-capabilities__provider{background:#fff;border:1px solid #d7e7f3;border-radius:16px;padding:14px}.absolute-voice-provider-capabilities__provider--selected,.absolute-voice-provider-capabilities__provider--healthy{border-color:#86efac}.absolute-voice-provider-capabilities__provider--degraded,.absolute-voice-provider-capabilities__provider--rate-limited,.absolute-voice-provider-capabilities__provider--suppressed,.absolute-voice-provider-capabilities__provider--unconfigured{border-color:#f2a7a7}.absolute-voice-provider-capabilities__provider p{margin:10px 0}.absolute-voice-provider-capabilities__provider dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-provider-capabilities__provider div{background:#f6fbff;border:1px solid #d7e7f3;border-radius:12px;padding:8px}.absolute-voice-provider-capabilities__provider dt{font-size:12px}.absolute-voice-provider-capabilities__provider dd{font-weight:800;margin:4px 0 0}.absolute-voice-provider-capabilities__empty{margin:14px 0 0}.absolute-voice-provider-capabilities__error{color:#9f1239;font-weight:700}`;
|
|
@@ -5072,7 +6920,7 @@ var useVoiceProviderContracts = (path = "/api/provider-contracts", options = {})
|
|
|
5072
6920
|
// src/client/providerContractsWidget.ts
|
|
5073
6921
|
var DEFAULT_TITLE10 = "Provider Contracts";
|
|
5074
6922
|
var DEFAULT_DESCRIPTION10 = "Production contract coverage for provider env, latency, fallback, streaming, and capabilities.";
|
|
5075
|
-
var
|
|
6923
|
+
var escapeHtml16 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
5076
6924
|
var formatProvider2 = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
|
|
5077
6925
|
var formatStatus3 = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
5078
6926
|
var contractDetail = (row) => {
|
|
@@ -5116,26 +6964,26 @@ var createVoiceProviderContractsViewModel = (snapshot, options = {}) => {
|
|
|
5116
6964
|
};
|
|
5117
6965
|
var renderVoiceProviderContractsHTML = (snapshot, options = {}) => {
|
|
5118
6966
|
const model = createVoiceProviderContractsViewModel(snapshot, options);
|
|
5119
|
-
const rows = model.rows.length ? `<div class="absolute-voice-provider-contracts__rows">${model.rows.map((row) => `<article class="absolute-voice-provider-contracts__row absolute-voice-provider-contracts__row--${
|
|
6967
|
+
const rows = model.rows.length ? `<div class="absolute-voice-provider-contracts__rows">${model.rows.map((row) => `<article class="absolute-voice-provider-contracts__row absolute-voice-provider-contracts__row--${escapeHtml16(row.status)}">
|
|
5120
6968
|
<header>
|
|
5121
|
-
<strong>${
|
|
5122
|
-
<span>${
|
|
6969
|
+
<strong>${escapeHtml16(row.label)}</strong>
|
|
6970
|
+
<span>${escapeHtml16(formatStatus3(row.status))}</span>
|
|
5123
6971
|
</header>
|
|
5124
|
-
<p>${
|
|
5125
|
-
${row.remediations.length ? `<ul class="absolute-voice-provider-contracts__remediations">${row.remediations.map((remediation) => `<li>${remediation.href ? `<a href="${
|
|
6972
|
+
<p>${escapeHtml16(row.detail)}</p>
|
|
6973
|
+
${row.remediations.length ? `<ul class="absolute-voice-provider-contracts__remediations">${row.remediations.map((remediation) => `<li>${remediation.href ? `<a href="${escapeHtml16(remediation.href)}">${escapeHtml16(remediation.label)}</a>` : `<strong>${escapeHtml16(remediation.label)}</strong>`}<span>${escapeHtml16(remediation.detail)}</span></li>`).join("")}</ul>` : ""}
|
|
5126
6974
|
<dl>${row.rows.map((item) => `<div>
|
|
5127
|
-
<dt>${
|
|
5128
|
-
<dd>${
|
|
6975
|
+
<dt>${escapeHtml16(item.label)}</dt>
|
|
6976
|
+
<dd>${escapeHtml16(item.value)}</dd>
|
|
5129
6977
|
</div>`).join("")}</dl>
|
|
5130
6978
|
</article>`).join("")}</div>` : '<p class="absolute-voice-provider-contracts__empty">Configure provider contracts to see production coverage.</p>';
|
|
5131
|
-
return `<section class="absolute-voice-provider-contracts absolute-voice-provider-contracts--${
|
|
6979
|
+
return `<section class="absolute-voice-provider-contracts absolute-voice-provider-contracts--${escapeHtml16(model.status)}">
|
|
5132
6980
|
<header class="absolute-voice-provider-contracts__header">
|
|
5133
|
-
<span class="absolute-voice-provider-contracts__eyebrow">${
|
|
5134
|
-
<strong class="absolute-voice-provider-contracts__label">${
|
|
6981
|
+
<span class="absolute-voice-provider-contracts__eyebrow">${escapeHtml16(model.title)}</span>
|
|
6982
|
+
<strong class="absolute-voice-provider-contracts__label">${escapeHtml16(model.label)}</strong>
|
|
5135
6983
|
</header>
|
|
5136
|
-
<p class="absolute-voice-provider-contracts__description">${
|
|
6984
|
+
<p class="absolute-voice-provider-contracts__description">${escapeHtml16(model.description)}</p>
|
|
5137
6985
|
${rows}
|
|
5138
|
-
${model.error ? `<p class="absolute-voice-provider-contracts__error">${
|
|
6986
|
+
${model.error ? `<p class="absolute-voice-provider-contracts__error">${escapeHtml16(model.error)}</p>` : ""}
|
|
5139
6987
|
</section>`;
|
|
5140
6988
|
};
|
|
5141
6989
|
var getVoiceProviderContractsCSS = () => `.absolute-voice-provider-contracts{border:1px solid #b8dcc7;border-radius:20px;background:#f7fff9;color:#09140d;padding:18px;box-shadow:0 18px 40px rgba(21,83,45,.12);font-family:inherit}.absolute-voice-provider-contracts--error,.absolute-voice-provider-contracts--warning{border-color:#f2a7a7;background:#fff7f4}.absolute-voice-provider-contracts__header,.absolute-voice-provider-contracts__row header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-provider-contracts__eyebrow{color:#166534;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-provider-contracts__label{font-size:24px;line-height:1}.absolute-voice-provider-contracts__description,.absolute-voice-provider-contracts__row p,.absolute-voice-provider-contracts__row dt,.absolute-voice-provider-contracts__empty{color:#405448}.absolute-voice-provider-contracts__rows{display:grid;gap:12px;margin-top:14px}.absolute-voice-provider-contracts__row{background:#fff;border:1px solid #d6eadb;border-radius:16px;padding:14px}.absolute-voice-provider-contracts__row--pass{border-color:#86efac}.absolute-voice-provider-contracts__row--warn,.absolute-voice-provider-contracts__row--fail{border-color:#f2a7a7}.absolute-voice-provider-contracts__row p{margin:10px 0}.absolute-voice-provider-contracts__remediations{display:grid;gap:8px;list-style:none;margin:0 0 10px;padding:0}.absolute-voice-provider-contracts__remediations li{background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;display:grid;gap:3px;padding:8px}.absolute-voice-provider-contracts__remediations a,.absolute-voice-provider-contracts__remediations strong{color:#9a3412}.absolute-voice-provider-contracts__remediations span{color:#7c2d12}.absolute-voice-provider-contracts__row dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-provider-contracts__row div{background:#f7fff9;border:1px solid #d6eadb;border-radius:12px;padding:8px}.absolute-voice-provider-contracts__row dt{font-size:12px}.absolute-voice-provider-contracts__row dd{font-weight:800;margin:4px 0 0}.absolute-voice-provider-contracts__error{color:#9f1239;font-weight:700}`;
|
|
@@ -5374,7 +7222,7 @@ var useVoiceProviderStatus = (path = "/api/provider-status", options = {}) => {
|
|
|
5374
7222
|
// src/client/providerStatusWidget.ts
|
|
5375
7223
|
var DEFAULT_TITLE11 = "Voice Providers";
|
|
5376
7224
|
var DEFAULT_DESCRIPTION11 = "Live provider health, fallback counts, latency, and suppression state from your self-hosted trace store.";
|
|
5377
|
-
var
|
|
7225
|
+
var escapeHtml17 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
5378
7226
|
var formatProvider3 = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
|
|
5379
7227
|
var formatStatus4 = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
5380
7228
|
var formatLatency = (value) => typeof value === "number" ? `${value}ms` : "No samples";
|
|
@@ -5430,25 +7278,25 @@ var createVoiceProviderStatusViewModel = (snapshot, options = {}) => {
|
|
|
5430
7278
|
};
|
|
5431
7279
|
var renderVoiceProviderStatusHTML = (snapshot, options = {}) => {
|
|
5432
7280
|
const model = createVoiceProviderStatusViewModel(snapshot, options);
|
|
5433
|
-
const providers = model.providers.length ? `<div class="absolute-voice-provider-status__providers">${model.providers.map((provider) => `<article class="absolute-voice-provider-status__provider absolute-voice-provider-status__provider--${
|
|
7281
|
+
const providers = model.providers.length ? `<div class="absolute-voice-provider-status__providers">${model.providers.map((provider) => `<article class="absolute-voice-provider-status__provider absolute-voice-provider-status__provider--${escapeHtml17(provider.status)}">
|
|
5434
7282
|
<header>
|
|
5435
|
-
<strong>${
|
|
5436
|
-
<span>${
|
|
7283
|
+
<strong>${escapeHtml17(provider.label)}</strong>
|
|
7284
|
+
<span>${escapeHtml17(formatStatus4(provider.status))}</span>
|
|
5437
7285
|
</header>
|
|
5438
|
-
<p>${
|
|
7286
|
+
<p>${escapeHtml17(provider.detail)}</p>
|
|
5439
7287
|
<dl>${provider.rows.map((row) => `<div>
|
|
5440
|
-
<dt>${
|
|
5441
|
-
<dd>${
|
|
7288
|
+
<dt>${escapeHtml17(row.label)}</dt>
|
|
7289
|
+
<dd>${escapeHtml17(row.value)}</dd>
|
|
5442
7290
|
</div>`).join("")}</dl>
|
|
5443
7291
|
</article>`).join("")}</div>` : '<p class="absolute-voice-provider-status__empty">Run voice traffic to see provider health.</p>';
|
|
5444
|
-
return `<section class="absolute-voice-provider-status absolute-voice-provider-status--${
|
|
7292
|
+
return `<section class="absolute-voice-provider-status absolute-voice-provider-status--${escapeHtml17(model.status)}">
|
|
5445
7293
|
<header class="absolute-voice-provider-status__header">
|
|
5446
|
-
<span class="absolute-voice-provider-status__eyebrow">${
|
|
5447
|
-
<strong class="absolute-voice-provider-status__label">${
|
|
7294
|
+
<span class="absolute-voice-provider-status__eyebrow">${escapeHtml17(model.title)}</span>
|
|
7295
|
+
<strong class="absolute-voice-provider-status__label">${escapeHtml17(model.label)}</strong>
|
|
5448
7296
|
</header>
|
|
5449
|
-
<p class="absolute-voice-provider-status__description">${
|
|
7297
|
+
<p class="absolute-voice-provider-status__description">${escapeHtml17(model.description)}</p>
|
|
5450
7298
|
${providers}
|
|
5451
|
-
${model.error ? `<p class="absolute-voice-provider-status__error">${
|
|
7299
|
+
${model.error ? `<p class="absolute-voice-provider-status__error">${escapeHtml17(model.error)}</p>` : ""}
|
|
5452
7300
|
</section>`;
|
|
5453
7301
|
};
|
|
5454
7302
|
var getVoiceProviderStatusCSS = () => `.absolute-voice-provider-status{border:1px solid #d8d2c4;border-radius:20px;background:#fffaf0;color:#16130d;padding:18px;box-shadow:0 18px 40px rgba(47,37,18,.12);font-family:inherit}.absolute-voice-provider-status--error,.absolute-voice-provider-status--warning{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-provider-status__header,.absolute-voice-provider-status__provider header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-provider-status__eyebrow{color:#73664f;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-provider-status__label{font-size:24px;line-height:1}.absolute-voice-provider-status__description,.absolute-voice-provider-status__provider p,.absolute-voice-provider-status__provider dt,.absolute-voice-provider-status__empty{color:#514733}.absolute-voice-provider-status__providers{display:grid;gap:12px;margin-top:14px}.absolute-voice-provider-status__provider{background:#fff;border:1px solid #eee4d2;border-radius:16px;padding:14px}.absolute-voice-provider-status__provider--degraded,.absolute-voice-provider-status__provider--rate-limited,.absolute-voice-provider-status__provider--suppressed{border-color:#f2a7a7}.absolute-voice-provider-status__provider--recoverable{border-color:#fbbf24}.absolute-voice-provider-status__provider p{margin:10px 0}.absolute-voice-provider-status__provider dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-provider-status__provider div{background:#fffaf0;border:1px solid #eee4d2;border-radius:12px;padding:8px}.absolute-voice-provider-status__provider dt{font-size:12px}.absolute-voice-provider-status__provider dd{font-weight:800;margin:4px 0 0}.absolute-voice-provider-status__empty{margin:14px 0 0}.absolute-voice-provider-status__error{color:#9f1239;font-weight:700}`;
|
|
@@ -5671,7 +7519,7 @@ var useVoiceRoutingStatus = (path = "/api/routing/latest", options = {}) => {
|
|
|
5671
7519
|
// src/client/routingStatusWidget.ts
|
|
5672
7520
|
var DEFAULT_TITLE12 = "Voice Routing";
|
|
5673
7521
|
var DEFAULT_DESCRIPTION12 = "Latest provider routing decision from the self-hosted trace store.";
|
|
5674
|
-
var
|
|
7522
|
+
var escapeHtml18 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
5675
7523
|
var formatValue = (value, fallback = "None") => typeof value === "string" && value.trim() ? value : typeof value === "number" && Number.isFinite(value) ? String(value) : fallback;
|
|
5676
7524
|
var formatProviderRoutes2 = (routes) => routes && typeof routes === "object" ? Object.entries(routes).map(([role, provider]) => `${role}: ${formatValue(provider)}`).join(", ") || "None" : "None";
|
|
5677
7525
|
var getProviderRoute = (routes, role) => routes && typeof routes === "object" ? formatValue(routes[role], "Not configured") : "Not configured";
|
|
@@ -5753,22 +7601,22 @@ var createVoiceRoutingStatusViewModel = (snapshot, options = {}) => {
|
|
|
5753
7601
|
var renderVoiceRoutingStatusHTML = (snapshot, options = {}) => {
|
|
5754
7602
|
const model = createVoiceRoutingStatusViewModel(snapshot, options);
|
|
5755
7603
|
const activeStack = model.activeStack.length ? `<div class="absolute-voice-routing-status__stack" aria-label="Active voice stack">${model.activeStack.map((item) => `<div>
|
|
5756
|
-
<span>${
|
|
5757
|
-
<strong>${
|
|
7604
|
+
<span>${escapeHtml18(item.label)}</span>
|
|
7605
|
+
<strong>${escapeHtml18(item.value)}</strong>
|
|
5758
7606
|
</div>`).join("")}</div>` : "";
|
|
5759
7607
|
const rows = model.rows.length ? `<div class="absolute-voice-routing-status__grid">${model.rows.map((row) => `<div>
|
|
5760
|
-
<span>${
|
|
5761
|
-
<strong>${
|
|
7608
|
+
<span>${escapeHtml18(row.label)}</span>
|
|
7609
|
+
<strong>${escapeHtml18(row.value)}</strong>
|
|
5762
7610
|
</div>`).join("")}</div>` : '<p class="absolute-voice-routing-status__empty">Start a voice session to see the selected provider.</p>';
|
|
5763
|
-
return `<section class="absolute-voice-routing-status absolute-voice-routing-status--${
|
|
7611
|
+
return `<section class="absolute-voice-routing-status absolute-voice-routing-status--${escapeHtml18(model.status)}">
|
|
5764
7612
|
<header class="absolute-voice-routing-status__header">
|
|
5765
|
-
<span class="absolute-voice-routing-status__eyebrow">${
|
|
5766
|
-
<strong class="absolute-voice-routing-status__label">${
|
|
7613
|
+
<span class="absolute-voice-routing-status__eyebrow">${escapeHtml18(model.title)}</span>
|
|
7614
|
+
<strong class="absolute-voice-routing-status__label">${escapeHtml18(model.label)}</strong>
|
|
5767
7615
|
</header>
|
|
5768
|
-
<p class="absolute-voice-routing-status__description">${
|
|
7616
|
+
<p class="absolute-voice-routing-status__description">${escapeHtml18(model.description)}</p>
|
|
5769
7617
|
${activeStack}
|
|
5770
7618
|
${rows}
|
|
5771
|
-
${model.error ? `<p class="absolute-voice-routing-status__error">${
|
|
7619
|
+
${model.error ? `<p class="absolute-voice-routing-status__error">${escapeHtml18(model.error)}</p>` : ""}
|
|
5772
7620
|
</section>`;
|
|
5773
7621
|
};
|
|
5774
7622
|
var getVoiceRoutingStatusCSS = () => `.absolute-voice-routing-status{border:1px solid #d8d2c4;border-radius:20px;background:#fffaf0;color:#16130d;padding:18px;box-shadow:0 18px 40px rgba(47,37,18,.12);font-family:inherit}.absolute-voice-routing-status--error{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-routing-status__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-routing-status__eyebrow{color:#73664f;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-routing-status__label{font-size:24px;line-height:1}.absolute-voice-routing-status__description{color:#514733;margin:12px 0 0}.absolute-voice-routing-status__stack{background:linear-gradient(135deg,#16130d,#49391f);border-radius:18px;color:#fff;display:grid;gap:8px;grid-template-columns:repeat(5,minmax(0,1fr));margin-top:14px;padding:12px}.absolute-voice-routing-status__stack div{border-left:1px solid rgba(255,255,255,.18);padding-left:10px}.absolute-voice-routing-status__stack div:first-child{border-left:0;padding-left:0}.absolute-voice-routing-status__stack span{color:#e9d9b8;display:block;font-size:11px;font-weight:800;letter-spacing:.08em;margin-bottom:5px;text-transform:uppercase}.absolute-voice-routing-status__stack strong{display:block;font-size:13px;line-height:1.25;overflow-wrap:anywhere}.absolute-voice-routing-status__grid{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin-top:14px}.absolute-voice-routing-status__grid div{background:#fff;border:1px solid #eee4d2;border-radius:14px;padding:10px 12px}.absolute-voice-routing-status__grid span{color:#655944;display:block;font-size:12px;margin-bottom:4px}.absolute-voice-routing-status__grid strong{overflow-wrap:anywhere}.absolute-voice-routing-status__empty{color:#655944;margin:14px 0 0}.absolute-voice-routing-status__error{color:#9f1239;font-weight:700}@media (max-width:760px){.absolute-voice-routing-status__stack{grid-template-columns:repeat(2,minmax(0,1fr))}.absolute-voice-routing-status__stack div{border-left:0;border-top:1px solid rgba(255,255,255,.18);padding-left:0;padding-top:8px}.absolute-voice-routing-status__stack div:first-child{border-top:0;padding-top:0}}`;
|
|
@@ -5948,7 +7796,7 @@ var createVoiceTraceTimelineStore = (path = "/api/voice-traces", options = {}) =
|
|
|
5948
7796
|
// src/client/traceTimelineWidget.ts
|
|
5949
7797
|
var DEFAULT_TITLE13 = "Voice Traces";
|
|
5950
7798
|
var DEFAULT_DESCRIPTION13 = "Latest call timelines with provider latency, fallbacks, handoffs, and errors from your self-hosted trace store.";
|
|
5951
|
-
var
|
|
7799
|
+
var escapeHtml19 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
5952
7800
|
var formatMs3 = (value) => typeof value === "number" ? `${value}ms` : "n/a";
|
|
5953
7801
|
var formatProviders = (session) => session.providers.length ? session.providers.map((provider) => provider.provider).join(", ") : "No providers";
|
|
5954
7802
|
var createVoiceTraceTimelineViewModel = (snapshot, options = {}) => {
|
|
@@ -5978,27 +7826,27 @@ var renderVoiceTraceTimelineWidgetHTML = (snapshot, options = {}) => {
|
|
|
5978
7826
|
const model = createVoiceTraceTimelineViewModel(snapshot, options);
|
|
5979
7827
|
const sessions = model.sessions.length ? `<div class="absolute-voice-trace-timeline__sessions">${model.sessions.map((session) => {
|
|
5980
7828
|
const supportLinks = [
|
|
5981
|
-
`<a href="${
|
|
5982
|
-
session.operationsRecordHref ? `<a href="${
|
|
5983
|
-
session.incidentBundleHref ? `<a href="${
|
|
7829
|
+
`<a href="${escapeHtml19(session.detailHref)}">Open timeline</a>`,
|
|
7830
|
+
session.operationsRecordHref ? `<a href="${escapeHtml19(session.operationsRecordHref)}">Open operations record</a>` : undefined,
|
|
7831
|
+
session.incidentBundleHref ? `<a href="${escapeHtml19(session.incidentBundleHref)}">Export incident bundle</a>` : undefined
|
|
5984
7832
|
].filter(Boolean).join("");
|
|
5985
|
-
return `<article class="absolute-voice-trace-timeline__session absolute-voice-trace-timeline__session--${
|
|
7833
|
+
return `<article class="absolute-voice-trace-timeline__session absolute-voice-trace-timeline__session--${escapeHtml19(session.status)}">
|
|
5986
7834
|
<header>
|
|
5987
|
-
<strong>${
|
|
5988
|
-
<span>${
|
|
7835
|
+
<strong>${escapeHtml19(session.sessionId)}</strong>
|
|
7836
|
+
<span>${escapeHtml19(session.status)}</span>
|
|
5989
7837
|
</header>
|
|
5990
|
-
<p>${
|
|
7838
|
+
<p>${escapeHtml19(session.label)} \xB7 ${escapeHtml19(session.durationLabel)} \xB7 ${escapeHtml19(session.providerLabel)}</p>
|
|
5991
7839
|
<p class="absolute-voice-trace-timeline__actions">${supportLinks}</p>
|
|
5992
7840
|
</article>`;
|
|
5993
7841
|
}).join("")}</div>` : '<p class="absolute-voice-trace-timeline__empty">Run a voice session to see call timelines.</p>';
|
|
5994
|
-
return `<section class="absolute-voice-trace-timeline absolute-voice-trace-timeline--${
|
|
7842
|
+
return `<section class="absolute-voice-trace-timeline absolute-voice-trace-timeline--${escapeHtml19(model.status)}">
|
|
5995
7843
|
<header class="absolute-voice-trace-timeline__header">
|
|
5996
|
-
<span class="absolute-voice-trace-timeline__eyebrow">${
|
|
5997
|
-
<strong class="absolute-voice-trace-timeline__label">${
|
|
7844
|
+
<span class="absolute-voice-trace-timeline__eyebrow">${escapeHtml19(model.title)}</span>
|
|
7845
|
+
<strong class="absolute-voice-trace-timeline__label">${escapeHtml19(model.label)}</strong>
|
|
5998
7846
|
</header>
|
|
5999
|
-
<p class="absolute-voice-trace-timeline__description">${
|
|
7847
|
+
<p class="absolute-voice-trace-timeline__description">${escapeHtml19(model.description)}</p>
|
|
6000
7848
|
${sessions}
|
|
6001
|
-
${model.error ? `<p class="absolute-voice-trace-timeline__error">${
|
|
7849
|
+
${model.error ? `<p class="absolute-voice-trace-timeline__error">${escapeHtml19(model.error)}</p>` : ""}
|
|
6002
7850
|
</section>`;
|
|
6003
7851
|
};
|
|
6004
7852
|
var getVoiceTraceTimelineCSS = () => `.absolute-voice-trace-timeline{border:1px solid #bad7d3;border-radius:20px;background:#f3fffb;color:#09201c;padding:18px;box-shadow:0 18px 40px rgba(9,32,28,.12);font-family:inherit}.absolute-voice-trace-timeline--error,.absolute-voice-trace-timeline--failed{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-trace-timeline--warning{border-color:#fbbf24;background:#fffaf0}.absolute-voice-trace-timeline__header,.absolute-voice-trace-timeline__session header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-trace-timeline__eyebrow{color:#17665b;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-trace-timeline__label{font-size:24px;line-height:1}.absolute-voice-trace-timeline__description,.absolute-voice-trace-timeline__session p,.absolute-voice-trace-timeline__empty{color:#35544f}.absolute-voice-trace-timeline__sessions{display:grid;gap:12px;margin-top:14px}.absolute-voice-trace-timeline__session{background:#fff;border:1px solid #cfe7e2;border-radius:16px;padding:14px}.absolute-voice-trace-timeline__session--failed{border-color:#f2a7a7}.absolute-voice-trace-timeline__session--warning{border-color:#fbbf24}.absolute-voice-trace-timeline__session p{margin:10px 0}.absolute-voice-trace-timeline__actions{display:flex;flex-wrap:wrap;gap:10px}.absolute-voice-trace-timeline__session a{color:#0f766e;font-weight:800}.absolute-voice-trace-timeline__empty{margin:14px 0 0}.absolute-voice-trace-timeline__error{color:#9f1239;font-weight:700}`;
|
|
@@ -6157,8 +8005,8 @@ var VoiceTraceTimeline = ({
|
|
|
6157
8005
|
import { useEffect as useEffect15, useRef as useRef15, useSyncExternalStore as useSyncExternalStore15 } from "react";
|
|
6158
8006
|
|
|
6159
8007
|
// src/client/agentSquadStatus.ts
|
|
6160
|
-
var
|
|
6161
|
-
var getPayloadString = (event, key) =>
|
|
8008
|
+
var getString4 = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
8009
|
+
var getPayloadString = (event, key) => getString4(event.payload?.[key]);
|
|
6162
8010
|
var eventStatus = (event) => {
|
|
6163
8011
|
const status = getPayloadString(event, "status");
|
|
6164
8012
|
if (status === "blocked")
|
|
@@ -6250,7 +8098,7 @@ var useVoiceAgentSquadStatus = (path = "/api/voice-traces", options = {}) => {
|
|
|
6250
8098
|
// src/client/agentSquadStatusWidget.ts
|
|
6251
8099
|
var DEFAULT_TITLE14 = "Voice Agent Squad";
|
|
6252
8100
|
var DEFAULT_DESCRIPTION14 = "Current specialist and recent handoffs from your self-hosted voice traces.";
|
|
6253
|
-
var
|
|
8101
|
+
var escapeHtml20 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6254
8102
|
var labelFor = (current) => {
|
|
6255
8103
|
if (!current)
|
|
6256
8104
|
return "Waiting for specialist activity";
|
|
@@ -6277,24 +8125,24 @@ var renderVoiceAgentSquadStatusHTML = (snapshot, options = {}) => {
|
|
|
6277
8125
|
const model = createVoiceAgentSquadStatusViewModel(snapshot, options);
|
|
6278
8126
|
const current = model.current;
|
|
6279
8127
|
const rows = model.sessions.length ? model.sessions.slice(0, 5).map((session) => `<li>
|
|
6280
|
-
<span>${
|
|
6281
|
-
<strong>${
|
|
6282
|
-
<em>${
|
|
6283
|
-
${session.summary || session.reason ? `<p>${
|
|
8128
|
+
<span>${escapeHtml20(session.sessionId)}</span>
|
|
8129
|
+
<strong>${escapeHtml20(session.targetAgentId ?? "none")}</strong>
|
|
8130
|
+
<em>${escapeHtml20(session.status)}</em>
|
|
8131
|
+
${session.summary || session.reason ? `<p>${escapeHtml20(session.summary ?? session.reason ?? "")}</p>` : ""}
|
|
6284
8132
|
</li>`).join("") : "<li><span>No squad traces yet.</span><strong>Waiting</strong></li>";
|
|
6285
8133
|
return `<section class="absolute-voice-agent-squad-status">
|
|
6286
8134
|
<header>
|
|
6287
|
-
<span>${
|
|
6288
|
-
<strong>${
|
|
8135
|
+
<span>${escapeHtml20(model.title)}</span>
|
|
8136
|
+
<strong>${escapeHtml20(model.label)}</strong>
|
|
6289
8137
|
</header>
|
|
6290
|
-
<p>${
|
|
8138
|
+
<p>${escapeHtml20(model.description)}</p>
|
|
6291
8139
|
<div>
|
|
6292
|
-
<span>Session</span><strong>${
|
|
6293
|
-
<span>From</span><strong>${
|
|
6294
|
-
<span>Status</span><strong>${
|
|
8140
|
+
<span>Session</span><strong>${escapeHtml20(current?.sessionId ?? "n/a")}</strong>
|
|
8141
|
+
<span>From</span><strong>${escapeHtml20(current?.fromAgentId ?? "n/a")}</strong>
|
|
8142
|
+
<span>Status</span><strong>${escapeHtml20(current?.status ?? "idle")}</strong>
|
|
6295
8143
|
</div>
|
|
6296
8144
|
<ul>${rows}</ul>
|
|
6297
|
-
${model.error ? `<p class="absolute-voice-agent-squad-status__error">${
|
|
8145
|
+
${model.error ? `<p class="absolute-voice-agent-squad-status__error">${escapeHtml20(model.error)}</p>` : ""}
|
|
6298
8146
|
</section>`;
|
|
6299
8147
|
};
|
|
6300
8148
|
var getVoiceAgentSquadStatusCSS = () => `.absolute-voice-agent-squad-status{border:1px solid #38bdf866;border-radius:20px;background:#0f172a;color:#f8fafc;padding:18px;font-family:inherit}.absolute-voice-agent-squad-status header{display:grid;gap:4px}.absolute-voice-agent-squad-status header span{color:#7dd3fc;font-size:12px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-agent-squad-status header strong{font-size:20px}.absolute-voice-agent-squad-status p{color:#cbd5e1}.absolute-voice-agent-squad-status div{display:grid;gap:6px;grid-template-columns:max-content 1fr;margin:14px 0}.absolute-voice-agent-squad-status div span{color:#94a3b8}.absolute-voice-agent-squad-status ul{display:grid;gap:8px;list-style:none;margin:0;padding:0}.absolute-voice-agent-squad-status li{background:#020617;border:1px solid #1e293b;border-radius:14px;padding:10px}.absolute-voice-agent-squad-status li span{color:#94a3b8;display:block;font-size:12px}.absolute-voice-agent-squad-status li strong{display:block}.absolute-voice-agent-squad-status li em{color:#7dd3fc;font-style:normal}.absolute-voice-agent-squad-status__error{color:#fecaca;font-weight:800}`;
|
|
@@ -6531,7 +8379,7 @@ var useVoiceTurnLatency = (path = "/api/turn-latency", options = {}) => {
|
|
|
6531
8379
|
var DEFAULT_TITLE15 = "Turn Latency";
|
|
6532
8380
|
var DEFAULT_DESCRIPTION15 = "Per-turn timing from first transcript to commit and assistant response start.";
|
|
6533
8381
|
var DEFAULT_PROOF_LABEL = "Run latency proof";
|
|
6534
|
-
var
|
|
8382
|
+
var escapeHtml21 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6535
8383
|
var formatMs4 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
|
|
6536
8384
|
var createVoiceTurnLatencyViewModel = (snapshot, options = {}) => {
|
|
6537
8385
|
const turns = (snapshot.report?.turns ?? []).map((turn) => ({
|
|
@@ -6559,25 +8407,25 @@ var createVoiceTurnLatencyViewModel = (snapshot, options = {}) => {
|
|
|
6559
8407
|
};
|
|
6560
8408
|
var renderVoiceTurnLatencyHTML = (snapshot, options = {}) => {
|
|
6561
8409
|
const model = createVoiceTurnLatencyViewModel(snapshot, options);
|
|
6562
|
-
const turns = model.turns.length ? `<div class="absolute-voice-turn-latency__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-latency__turn absolute-voice-turn-latency__turn--${
|
|
8410
|
+
const turns = model.turns.length ? `<div class="absolute-voice-turn-latency__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-latency__turn absolute-voice-turn-latency__turn--${escapeHtml21(turn.status)}">
|
|
6563
8411
|
<header>
|
|
6564
|
-
<strong>${
|
|
6565
|
-
<span>${
|
|
8412
|
+
<strong>${escapeHtml21(turn.label)}</strong>
|
|
8413
|
+
<span>${escapeHtml21(turn.status)}</span>
|
|
6566
8414
|
</header>
|
|
6567
8415
|
<dl>${turn.rows.map((row) => `<div>
|
|
6568
|
-
<dt>${
|
|
6569
|
-
<dd>${
|
|
8416
|
+
<dt>${escapeHtml21(row.label)}</dt>
|
|
8417
|
+
<dd>${escapeHtml21(row.value)}</dd>
|
|
6570
8418
|
</div>`).join("")}</dl>
|
|
6571
8419
|
</article>`).join("")}</div>` : '<p class="absolute-voice-turn-latency__empty">Complete a voice turn to see latency diagnostics.</p>';
|
|
6572
|
-
return `<section class="absolute-voice-turn-latency absolute-voice-turn-latency--${
|
|
8420
|
+
return `<section class="absolute-voice-turn-latency absolute-voice-turn-latency--${escapeHtml21(model.status)}">
|
|
6573
8421
|
<header class="absolute-voice-turn-latency__header">
|
|
6574
|
-
<span class="absolute-voice-turn-latency__eyebrow">${
|
|
6575
|
-
<strong class="absolute-voice-turn-latency__label">${
|
|
8422
|
+
<span class="absolute-voice-turn-latency__eyebrow">${escapeHtml21(model.title)}</span>
|
|
8423
|
+
<strong class="absolute-voice-turn-latency__label">${escapeHtml21(model.label)}</strong>
|
|
6576
8424
|
</header>
|
|
6577
|
-
<p class="absolute-voice-turn-latency__description">${
|
|
6578
|
-
${model.showProofAction ? `<button class="absolute-voice-turn-latency__proof" data-absolute-voice-turn-latency-proof type="button">${
|
|
8425
|
+
<p class="absolute-voice-turn-latency__description">${escapeHtml21(model.description)}</p>
|
|
8426
|
+
${model.showProofAction ? `<button class="absolute-voice-turn-latency__proof" data-absolute-voice-turn-latency-proof type="button">${escapeHtml21(model.proofLabel ?? DEFAULT_PROOF_LABEL)}</button>` : ""}
|
|
6579
8427
|
${turns}
|
|
6580
|
-
${model.error ? `<p class="absolute-voice-turn-latency__error">${
|
|
8428
|
+
${model.error ? `<p class="absolute-voice-turn-latency__error">${escapeHtml21(model.error)}</p>` : ""}
|
|
6581
8429
|
</section>`;
|
|
6582
8430
|
};
|
|
6583
8431
|
var mountVoiceTurnLatency = (element, path = "/api/turn-latency", options = {}) => {
|
|
@@ -6813,7 +8661,7 @@ var useVoiceTurnQuality = (path = "/api/turn-quality", options = {}) => {
|
|
|
6813
8661
|
// src/client/turnQualityWidget.ts
|
|
6814
8662
|
var DEFAULT_TITLE16 = "Turn Quality";
|
|
6815
8663
|
var DEFAULT_DESCRIPTION16 = "Per-turn STT confidence, fallback selection, corrections, and transcript coverage.";
|
|
6816
|
-
var
|
|
8664
|
+
var escapeHtml22 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6817
8665
|
var formatConfidence = (value) => typeof value === "number" ? `${Math.round(value * 100)}%` : "n/a";
|
|
6818
8666
|
var formatMaybe = (value) => value === undefined || value === "" ? "n/a" : String(value);
|
|
6819
8667
|
var getTurnDetail = (turn) => {
|
|
@@ -6863,25 +8711,25 @@ var createVoiceTurnQualityViewModel = (snapshot, options = {}) => {
|
|
|
6863
8711
|
};
|
|
6864
8712
|
var renderVoiceTurnQualityHTML = (snapshot, options = {}) => {
|
|
6865
8713
|
const model = createVoiceTurnQualityViewModel(snapshot, options);
|
|
6866
|
-
const turns = model.turns.length ? `<div class="absolute-voice-turn-quality__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-quality__turn absolute-voice-turn-quality__turn--${
|
|
8714
|
+
const turns = model.turns.length ? `<div class="absolute-voice-turn-quality__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-quality__turn absolute-voice-turn-quality__turn--${escapeHtml22(turn.status)}">
|
|
6867
8715
|
<header>
|
|
6868
|
-
<strong>${
|
|
6869
|
-
<span>${
|
|
8716
|
+
<strong>${escapeHtml22(turn.label)}</strong>
|
|
8717
|
+
<span>${escapeHtml22(turn.status)}</span>
|
|
6870
8718
|
</header>
|
|
6871
|
-
<p>${
|
|
8719
|
+
<p>${escapeHtml22(turn.detail)}</p>
|
|
6872
8720
|
<dl>${turn.rows.map((row) => `<div>
|
|
6873
|
-
<dt>${
|
|
6874
|
-
<dd>${
|
|
8721
|
+
<dt>${escapeHtml22(row.label)}</dt>
|
|
8722
|
+
<dd>${escapeHtml22(row.value)}</dd>
|
|
6875
8723
|
</div>`).join("")}</dl>
|
|
6876
8724
|
</article>`).join("")}</div>` : '<p class="absolute-voice-turn-quality__empty">Complete a voice turn to see STT quality diagnostics.</p>';
|
|
6877
|
-
return `<section class="absolute-voice-turn-quality absolute-voice-turn-quality--${
|
|
8725
|
+
return `<section class="absolute-voice-turn-quality absolute-voice-turn-quality--${escapeHtml22(model.status)}">
|
|
6878
8726
|
<header class="absolute-voice-turn-quality__header">
|
|
6879
|
-
<span class="absolute-voice-turn-quality__eyebrow">${
|
|
6880
|
-
<strong class="absolute-voice-turn-quality__label">${
|
|
8727
|
+
<span class="absolute-voice-turn-quality__eyebrow">${escapeHtml22(model.title)}</span>
|
|
8728
|
+
<strong class="absolute-voice-turn-quality__label">${escapeHtml22(model.label)}</strong>
|
|
6881
8729
|
</header>
|
|
6882
|
-
<p class="absolute-voice-turn-quality__description">${
|
|
8730
|
+
<p class="absolute-voice-turn-quality__description">${escapeHtml22(model.description)}</p>
|
|
6883
8731
|
${turns}
|
|
6884
|
-
${model.error ? `<p class="absolute-voice-turn-quality__error">${
|
|
8732
|
+
${model.error ? `<p class="absolute-voice-turn-quality__error">${escapeHtml22(model.error)}</p>` : ""}
|
|
6885
8733
|
</section>`;
|
|
6886
8734
|
};
|
|
6887
8735
|
var getVoiceTurnQualityCSS = () => `.absolute-voice-turn-quality{border:1px solid #e4d1a3;border-radius:20px;background:#fff9eb;color:#17120a;padding:18px;box-shadow:0 18px 40px rgba(73,48,14,.12);font-family:inherit}.absolute-voice-turn-quality--error,.absolute-voice-turn-quality--warning{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-turn-quality__header,.absolute-voice-turn-quality__turn header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-turn-quality__eyebrow{color:#8a5a0a;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-turn-quality__label{font-size:24px;line-height:1}.absolute-voice-turn-quality__description,.absolute-voice-turn-quality__turn p,.absolute-voice-turn-quality__turn dt,.absolute-voice-turn-quality__empty{color:#5a4930}.absolute-voice-turn-quality__turns{display:grid;gap:12px;margin-top:14px}.absolute-voice-turn-quality__turn{background:#fff;border:1px solid #f0dfba;border-radius:16px;padding:14px}.absolute-voice-turn-quality__turn--pass{border-color:#86efac}.absolute-voice-turn-quality__turn--warn,.absolute-voice-turn-quality__turn--unknown{border-color:#fbbf24}.absolute-voice-turn-quality__turn--fail{border-color:#f2a7a7}.absolute-voice-turn-quality__turn p{margin:10px 0}.absolute-voice-turn-quality__turn dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-turn-quality__turn div{background:#fff9eb;border:1px solid #f0dfba;border-radius:12px;padding:8px}.absolute-voice-turn-quality__turn dt{font-size:12px}.absolute-voice-turn-quality__turn dd{font-weight:800;margin:4px 0 0}.absolute-voice-turn-quality__empty{margin:14px 0 0}.absolute-voice-turn-quality__error{color:#9f1239;font-weight:700}`;
|