@absolutejs/voice 0.0.22-beta.385 → 0.0.22-beta.386
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 +4360 -3272
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2718 -2622
- package/dist/proofTrends.d.ts +37 -1
- package/dist/react/index.js +2017 -172
- package/dist/vue/index.js +1966 -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
|
{
|
|
@@ -2291,6 +4044,98 @@ var uniqueVoiceRealCallProfileRecoveryLoopActions = (actions) => {
|
|
|
2291
4044
|
return true;
|
|
2292
4045
|
});
|
|
2293
4046
|
};
|
|
4047
|
+
var normalizeRealCallProfileRecoveryProvider = (role, provider) => {
|
|
4048
|
+
if (!provider) {
|
|
4049
|
+
return;
|
|
4050
|
+
}
|
|
4051
|
+
if (typeof provider === "string") {
|
|
4052
|
+
return {
|
|
4053
|
+
provider,
|
|
4054
|
+
selectedProvider: provider,
|
|
4055
|
+
status: "selected"
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
4058
|
+
return {
|
|
4059
|
+
...provider,
|
|
4060
|
+
selectedProvider: provider.selectedProvider ?? provider.provider,
|
|
4061
|
+
status: provider.status ?? "selected"
|
|
4062
|
+
};
|
|
4063
|
+
};
|
|
4064
|
+
var profileRealCallRecoveryEvent = (event, profileId, metadata = {}) => ({
|
|
4065
|
+
...event,
|
|
4066
|
+
metadata: {
|
|
4067
|
+
...metadata,
|
|
4068
|
+
...event.metadata ?? {},
|
|
4069
|
+
benchmarkProfileId: event.metadata?.benchmarkProfileId ?? profileId,
|
|
4070
|
+
profileId: event.metadata?.profileId ?? profileId
|
|
4071
|
+
},
|
|
4072
|
+
payload: {
|
|
4073
|
+
...event.payload ?? {},
|
|
4074
|
+
benchmarkProfileId: event.payload.benchmarkProfileId ?? profileId,
|
|
4075
|
+
profileId: event.payload.profileId ?? profileId
|
|
4076
|
+
}
|
|
4077
|
+
});
|
|
4078
|
+
var appendVoiceRealCallProfileRecoveryEvidence = async (options) => {
|
|
4079
|
+
const at = options.at ?? Date.now();
|
|
4080
|
+
const scenarioId = options.scenarioId ?? "real-call-profile-recovery";
|
|
4081
|
+
const sessionId = options.sessionId ?? `real-call-profile-recovery-${options.profileId}-${new Date(at).toISOString()}`;
|
|
4082
|
+
const browser = options.browser ?? {};
|
|
4083
|
+
const live = options.live ?? {};
|
|
4084
|
+
const providerEvents = [
|
|
4085
|
+
["llm", 320, "live-call"],
|
|
4086
|
+
["stt", 82, "live-stt"],
|
|
4087
|
+
["tts", 45, "live-tts"]
|
|
4088
|
+
].flatMap(([role, defaultElapsedMs, defaultSurface], index) => {
|
|
4089
|
+
const provider = normalizeRealCallProfileRecoveryProvider(role, options.providers?.[role]);
|
|
4090
|
+
if (!provider) {
|
|
4091
|
+
return [];
|
|
4092
|
+
}
|
|
4093
|
+
return [
|
|
4094
|
+
profileRealCallRecoveryEvent(createVoiceProviderDecisionTraceEvent({
|
|
4095
|
+
at: at + index,
|
|
4096
|
+
elapsedMs: provider.elapsedMs ?? defaultElapsedMs,
|
|
4097
|
+
kind: role,
|
|
4098
|
+
provider: provider.provider,
|
|
4099
|
+
reason: provider.reason ?? `Real-call profile recovery selected ${provider.provider} for ${role.toUpperCase()} evidence.`,
|
|
4100
|
+
scenarioId,
|
|
4101
|
+
selectedProvider: provider.selectedProvider,
|
|
4102
|
+
sessionId,
|
|
4103
|
+
status: provider.status,
|
|
4104
|
+
surface: provider.surface ?? defaultSurface
|
|
4105
|
+
}), options.profileId, options.metadata)
|
|
4106
|
+
];
|
|
4107
|
+
});
|
|
4108
|
+
const browserEvents = browser === false ? [] : [
|
|
4109
|
+
profileRealCallRecoveryEvent(createVoiceTraceEvent({
|
|
4110
|
+
at: at + 3,
|
|
4111
|
+
payload: {
|
|
4112
|
+
firstAudioLatencyMs: browser.firstAudioLatencyMs ?? 420,
|
|
4113
|
+
messageCount: browser.messageCount,
|
|
4114
|
+
openSockets: browser.openSockets,
|
|
4115
|
+
receivedBytes: browser.receivedBytes,
|
|
4116
|
+
sentBytes: browser.sentBytes,
|
|
4117
|
+
status: browser.status ?? "pass"
|
|
4118
|
+
},
|
|
4119
|
+
scenarioId,
|
|
4120
|
+
sessionId,
|
|
4121
|
+
type: "client.browser_media"
|
|
4122
|
+
}), options.profileId, options.metadata)
|
|
4123
|
+
];
|
|
4124
|
+
const liveEvents = live === false ? [] : [
|
|
4125
|
+
profileRealCallRecoveryEvent(createVoiceTraceEvent({
|
|
4126
|
+
at: at + 4,
|
|
4127
|
+
payload: {
|
|
4128
|
+
latencyMs: live.latencyMs ?? 420,
|
|
4129
|
+
status: live.status ?? "pass"
|
|
4130
|
+
},
|
|
4131
|
+
scenarioId,
|
|
4132
|
+
sessionId,
|
|
4133
|
+
type: "client.live_latency"
|
|
4134
|
+
}), options.profileId, options.metadata)
|
|
4135
|
+
];
|
|
4136
|
+
const events = await Promise.all([...providerEvents, ...browserEvents, ...liveEvents].map((event) => options.store.append(event)));
|
|
4137
|
+
return { events, sessionId };
|
|
4138
|
+
};
|
|
2294
4139
|
var runVoiceRealCallProfileRecoveryLoop = async (options) => {
|
|
2295
4140
|
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
2296
4141
|
const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
|
|
@@ -3119,7 +4964,7 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
|
|
|
3119
4964
|
}
|
|
3120
4965
|
};
|
|
3121
4966
|
};
|
|
3122
|
-
var
|
|
4967
|
+
var escapeHtml9 = (value) => String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3123
4968
|
var escapeMarkdown = (value) => value.replaceAll("|", "\\|");
|
|
3124
4969
|
var renderVoiceProofTrendRecommendationMarkdown = (report, title = "Voice Provider Runtime Recommendations") => [
|
|
3125
4970
|
`# ${title}`,
|
|
@@ -3152,11 +4997,11 @@ var renderVoiceProofTrendRecommendationMarkdown = (report, title = "Voice Provid
|
|
|
3152
4997
|
].join(`
|
|
3153
4998
|
`);
|
|
3154
4999
|
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>${
|
|
5000
|
+
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("");
|
|
5001
|
+
const issues = report.issues.length === 0 ? "<li>None</li>" : report.issues.map((issue) => `<li>${escapeHtml9(issue)}</li>`).join("");
|
|
5002
|
+
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("");
|
|
5003
|
+
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("");
|
|
5004
|
+
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
5005
|
};
|
|
3161
5006
|
var renderVoiceRealCallProfileHistoryMarkdown = (report, title = "Voice Real-Call Profile History") => [
|
|
3162
5007
|
`# ${title}`,
|
|
@@ -3190,18 +5035,18 @@ var renderVoiceRealCallProfileHistoryMarkdown = (report, title = "Voice Real-Cal
|
|
|
3190
5035
|
].join(`
|
|
3191
5036
|
`);
|
|
3192
5037
|
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>${
|
|
5038
|
+
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>';
|
|
5039
|
+
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>';
|
|
5040
|
+
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("");
|
|
5041
|
+
const issues = report.issues.length === 0 ? "<li>None</li>" : report.issues.map((issue) => `<li>${escapeHtml9(issue)}</li>`).join("");
|
|
5042
|
+
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
5043
|
};
|
|
3199
5044
|
var createVoiceProofTrendRecommendationRoutes = (options) => {
|
|
3200
5045
|
const path = options.path ?? "/api/voice/proof-trend-recommendations";
|
|
3201
5046
|
const htmlPath = options.htmlPath === undefined ? "/voice/proof-trend-recommendations" : options.htmlPath;
|
|
3202
5047
|
const markdownPath = options.markdownPath === undefined ? "/voice/proof-trend-recommendations.md" : options.markdownPath;
|
|
3203
5048
|
const title = options.title ?? "Voice Provider Runtime Recommendations";
|
|
3204
|
-
const routes = new
|
|
5049
|
+
const routes = new Elysia4({
|
|
3205
5050
|
name: options.name ?? "absolutejs-voice-proof-trend-recommendations"
|
|
3206
5051
|
});
|
|
3207
5052
|
const loadReport = async () => {
|
|
@@ -3243,7 +5088,7 @@ var createVoiceRealCallProfileHistoryRoutes = (options = {}) => {
|
|
|
3243
5088
|
const htmlPath = options.htmlPath === undefined ? "/voice/real-call-profile-history" : options.htmlPath;
|
|
3244
5089
|
const markdownPath = options.markdownPath === undefined ? "/voice/real-call-profile-history.md" : options.markdownPath;
|
|
3245
5090
|
const title = options.title ?? "Voice Real-Call Profile History";
|
|
3246
|
-
const routes = new
|
|
5091
|
+
const routes = new Elysia4({
|
|
3247
5092
|
name: options.name ?? "absolutejs-voice-real-call-profile-history"
|
|
3248
5093
|
});
|
|
3249
5094
|
const loadReport = async () => {
|
|
@@ -3295,7 +5140,7 @@ var loadVoiceRealCallProfileHistoryRouteReport = async (options) => {
|
|
|
3295
5140
|
};
|
|
3296
5141
|
var createVoiceRealCallProfileRecoveryActionRoutes = (options = {}) => {
|
|
3297
5142
|
const path = options.path ?? "/api/voice/real-call-profile-history";
|
|
3298
|
-
const routes = new
|
|
5143
|
+
const routes = new Elysia4({
|
|
3299
5144
|
name: options.name ?? "absolutejs-voice-real-call-profile-recovery-actions"
|
|
3300
5145
|
});
|
|
3301
5146
|
const actionPath = (actionId) => `${path}${realCallProfileActionPaths[actionId]}`;
|
|
@@ -3470,7 +5315,7 @@ var createVoiceRealCallProfileRecoveryActionRoutes = (options = {}) => {
|
|
|
3470
5315
|
};
|
|
3471
5316
|
var createVoiceProofTrendRoutes = (options) => {
|
|
3472
5317
|
const path = options.path ?? "/api/voice/proof-trends";
|
|
3473
|
-
const routes = new
|
|
5318
|
+
const routes = new Elysia4({
|
|
3474
5319
|
name: options.name ?? "absolutejs-voice-proof-trends"
|
|
3475
5320
|
});
|
|
3476
5321
|
routes.get(path, async () => {
|
|
@@ -3510,7 +5355,7 @@ var DEFAULT_LINKS2 = [
|
|
|
3510
5355
|
{ href: "/voice/proof-trends", label: "Trend page" },
|
|
3511
5356
|
{ href: "/api/voice/proof-trends", label: "Trend JSON" }
|
|
3512
5357
|
];
|
|
3513
|
-
var
|
|
5358
|
+
var escapeHtml10 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3514
5359
|
var formatMs = (value) => typeof value === "number" && Number.isFinite(value) ? `${Math.round(value)}ms` : "n/a";
|
|
3515
5360
|
var statusLabel = (report) => {
|
|
3516
5361
|
if (!report) {
|
|
@@ -3560,19 +5405,19 @@ var createVoiceProofTrendsViewModel = (snapshot, options = {}) => {
|
|
|
3560
5405
|
var renderVoiceProofTrendsHTML = (snapshot, options = {}) => {
|
|
3561
5406
|
const model = createVoiceProofTrendsViewModel(snapshot, options);
|
|
3562
5407
|
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--${
|
|
5408
|
+
<span>${escapeHtml10(metric.label)}</span>
|
|
5409
|
+
<strong>${escapeHtml10(metric.value)}</strong>
|
|
5410
|
+
</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>`;
|
|
5411
|
+
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>` : "";
|
|
5412
|
+
return `<section class="absolute-voice-proof-trends absolute-voice-proof-trends--${escapeHtml10(model.status)}">
|
|
3568
5413
|
<header class="absolute-voice-proof-trends__header">
|
|
3569
|
-
<span class="absolute-voice-proof-trends__eyebrow">${
|
|
3570
|
-
<strong class="absolute-voice-proof-trends__label">${
|
|
5414
|
+
<span class="absolute-voice-proof-trends__eyebrow">${escapeHtml10(model.title)}</span>
|
|
5415
|
+
<strong class="absolute-voice-proof-trends__label">${escapeHtml10(model.label)}</strong>
|
|
3571
5416
|
</header>
|
|
3572
|
-
<p class="absolute-voice-proof-trends__description">${
|
|
5417
|
+
<p class="absolute-voice-proof-trends__description">${escapeHtml10(model.description)}</p>
|
|
3573
5418
|
${metrics}
|
|
3574
5419
|
${links}
|
|
3575
|
-
${model.error ? `<p class="absolute-voice-proof-trends__error">${
|
|
5420
|
+
${model.error ? `<p class="absolute-voice-proof-trends__error">${escapeHtml10(model.error)}</p>` : ""}
|
|
3576
5421
|
</section>`;
|
|
3577
5422
|
};
|
|
3578
5423
|
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 +5602,7 @@ var DEFAULT_LINKS3 = [
|
|
|
3757
5602
|
{ href: "/voice/real-call-profile-history", label: "Profile history" },
|
|
3758
5603
|
{ href: "/api/voice/real-call-profile-history", label: "JSON" }
|
|
3759
5604
|
];
|
|
3760
|
-
var
|
|
5605
|
+
var escapeHtml11 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3761
5606
|
var formatMs2 = (value) => typeof value === "number" && Number.isFinite(value) ? `${Math.round(value)}ms` : "n/a";
|
|
3762
5607
|
var formatProviderRoutes = (profile) => Object.entries(profile.providerRoutes).map(([role, provider]) => `${role}: ${provider}`).join(", ") || "No complete route yet";
|
|
3763
5608
|
var createProfileView = (profile) => ({
|
|
@@ -3788,25 +5633,25 @@ var createVoiceProfileComparisonViewModel = (snapshot, options = {}) => {
|
|
|
3788
5633
|
};
|
|
3789
5634
|
var renderVoiceProfileComparisonHTML = (snapshot, options = {}) => {
|
|
3790
5635
|
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--${
|
|
5636
|
+
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
5637
|
<header>
|
|
3793
|
-
<span>${
|
|
3794
|
-
<strong>${
|
|
5638
|
+
<span>${escapeHtml11(profile.status)}</span>
|
|
5639
|
+
<strong>${escapeHtml11(profile.label)}</strong>
|
|
3795
5640
|
</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--${
|
|
5641
|
+
<p>${escapeHtml11(profile.providerRoutes)}</p>
|
|
5642
|
+
<div>${profile.evidence.map((metric) => `<span><small>${escapeHtml11(metric.label)}</small><b>${escapeHtml11(metric.value)}</b></span>`).join("")}</div>
|
|
5643
|
+
<em>${escapeHtml11(profile.nextMove)}</em>
|
|
5644
|
+
</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>`;
|
|
5645
|
+
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>` : "";
|
|
5646
|
+
return `<section class="absolute-voice-profile-comparison absolute-voice-profile-comparison--${escapeHtml11(model.status)}">
|
|
3802
5647
|
<header class="absolute-voice-profile-comparison__header">
|
|
3803
|
-
<span class="absolute-voice-profile-comparison__eyebrow">${
|
|
3804
|
-
<strong class="absolute-voice-profile-comparison__label">${
|
|
5648
|
+
<span class="absolute-voice-profile-comparison__eyebrow">${escapeHtml11(model.title)}</span>
|
|
5649
|
+
<strong class="absolute-voice-profile-comparison__label">${escapeHtml11(model.label)}</strong>
|
|
3805
5650
|
</header>
|
|
3806
|
-
<p class="absolute-voice-profile-comparison__description">${
|
|
5651
|
+
<p class="absolute-voice-profile-comparison__description">${escapeHtml11(model.description)}</p>
|
|
3807
5652
|
${profiles}
|
|
3808
5653
|
${links}
|
|
3809
|
-
${model.error ? `<p class="absolute-voice-profile-comparison__error">${
|
|
5654
|
+
${model.error ? `<p class="absolute-voice-profile-comparison__error">${escapeHtml11(model.error)}</p>` : ""}
|
|
3810
5655
|
</section>`;
|
|
3811
5656
|
};
|
|
3812
5657
|
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 +5874,27 @@ var createVoiceProfileSwitchRecommendationStore = (path = "/api/voice/profile-sw
|
|
|
4029
5874
|
// src/client/profileSwitchRecommendationWidget.ts
|
|
4030
5875
|
var DEFAULT_TITLE7 = "Profile Switch Recommendation";
|
|
4031
5876
|
var DEFAULT_DESCRIPTION7 = "Compares the current session against measured profile evidence and recommends whether to switch stacks.";
|
|
4032
|
-
var
|
|
5877
|
+
var escapeHtml12 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4033
5878
|
var formatRoute = (routes) => routes ? Object.entries(routes).map(([role, provider]) => `${role}: ${provider}`).join(", ") : "No route";
|
|
4034
5879
|
var renderVoiceProfileSwitchRecommendationHTML = (snapshot, options = {}) => {
|
|
4035
5880
|
const recommendation = snapshot.recommendation;
|
|
4036
5881
|
const status = snapshot.error ? "error" : recommendation ? recommendation.status : snapshot.isLoading ? "loading" : "empty";
|
|
4037
5882
|
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
5883
|
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--${
|
|
5884
|
+
<p><strong>Current:</strong> ${escapeHtml12(recommendation.currentProfile?.label ?? recommendation.currentProfile?.profileId ?? "Unknown")}</p>
|
|
5885
|
+
<p><strong>Recommended:</strong> ${escapeHtml12(recommendation.recommendedProfile?.label ?? recommendation.recommendedProfile?.profileId ?? "None")}</p>
|
|
5886
|
+
<p><strong>Routes:</strong> ${escapeHtml12(formatRoute(recommendation.recommendedProfile?.providerRoutes))}</p>
|
|
5887
|
+
<ul>${recommendation.reasons.map((reason) => `<li>${escapeHtml12(reason)}</li>`).join("")}</ul>
|
|
5888
|
+
<em>${escapeHtml12(recommendation.nextMove)}</em>
|
|
5889
|
+
</div>` : `<p class="absolute-voice-profile-switch__empty">${escapeHtml12(snapshot.error ?? "Run session traffic to populate a recommendation.")}</p>`;
|
|
5890
|
+
return `<section class="absolute-voice-profile-switch absolute-voice-profile-switch--${escapeHtml12(status)}">
|
|
4046
5891
|
<header class="absolute-voice-profile-switch__header">
|
|
4047
|
-
<span class="absolute-voice-profile-switch__eyebrow">${
|
|
4048
|
-
<strong class="absolute-voice-profile-switch__label">${
|
|
5892
|
+
<span class="absolute-voice-profile-switch__eyebrow">${escapeHtml12(options.title ?? DEFAULT_TITLE7)}</span>
|
|
5893
|
+
<strong class="absolute-voice-profile-switch__label">${escapeHtml12(label)}</strong>
|
|
4049
5894
|
</header>
|
|
4050
|
-
<p class="absolute-voice-profile-switch__description">${
|
|
5895
|
+
<p class="absolute-voice-profile-switch__description">${escapeHtml12(options.description ?? DEFAULT_DESCRIPTION7)}</p>
|
|
4051
5896
|
${body}
|
|
4052
|
-
${snapshot.error ? `<p class="absolute-voice-profile-switch__error">${
|
|
5897
|
+
${snapshot.error ? `<p class="absolute-voice-profile-switch__error">${escapeHtml12(snapshot.error)}</p>` : ""}
|
|
4053
5898
|
</section>`;
|
|
4054
5899
|
};
|
|
4055
5900
|
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 +6049,7 @@ var DEFAULT_LINKS4 = [
|
|
|
4204
6049
|
{ href: "/production-readiness", label: "Readiness page" },
|
|
4205
6050
|
{ href: "/voice/slo-readiness-thresholds", label: "Gate thresholds" }
|
|
4206
6051
|
];
|
|
4207
|
-
var
|
|
6052
|
+
var escapeHtml13 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4208
6053
|
var formatExplanationValue = (value, unit) => {
|
|
4209
6054
|
if (value === undefined || value === null) {
|
|
4210
6055
|
return "n/a";
|
|
@@ -4245,23 +6090,23 @@ var createVoiceReadinessFailuresViewModel = (snapshot, options = {}) => {
|
|
|
4245
6090
|
};
|
|
4246
6091
|
var renderVoiceReadinessFailuresHTML = (snapshot, options = {}) => {
|
|
4247
6092
|
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--${
|
|
6093
|
+
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)}">
|
|
6094
|
+
<span>${escapeHtml13(failure.status.toUpperCase())}</span>
|
|
6095
|
+
<strong>${escapeHtml13(failure.label)}</strong>
|
|
6096
|
+
<p>Observed ${escapeHtml13(failure.observed)} against ${escapeHtml13(failure.thresholdLabel)} ${escapeHtml13(failure.threshold)}.</p>
|
|
6097
|
+
<p>${escapeHtml13(failure.remediation)}</p>
|
|
6098
|
+
<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>
|
|
6099
|
+
</article>`).join("")}</div>` : `<p class="absolute-voice-readiness-failures__empty">${model.error ? escapeHtml13(model.error) : "No calibrated readiness gate explanations are open."}</p>`;
|
|
6100
|
+
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>` : "";
|
|
6101
|
+
return `<section class="absolute-voice-readiness-failures absolute-voice-readiness-failures--${escapeHtml13(model.status)}">
|
|
4257
6102
|
<header class="absolute-voice-readiness-failures__header">
|
|
4258
|
-
<span class="absolute-voice-readiness-failures__eyebrow">${
|
|
4259
|
-
<strong class="absolute-voice-readiness-failures__label">${
|
|
6103
|
+
<span class="absolute-voice-readiness-failures__eyebrow">${escapeHtml13(model.title)}</span>
|
|
6104
|
+
<strong class="absolute-voice-readiness-failures__label">${escapeHtml13(model.label)}</strong>
|
|
4260
6105
|
</header>
|
|
4261
|
-
<p class="absolute-voice-readiness-failures__description">${
|
|
6106
|
+
<p class="absolute-voice-readiness-failures__description">${escapeHtml13(model.description)}</p>
|
|
4262
6107
|
${failures}
|
|
4263
6108
|
${links}
|
|
4264
|
-
${model.error ? `<p class="absolute-voice-readiness-failures__error">${
|
|
6109
|
+
${model.error ? `<p class="absolute-voice-readiness-failures__error">${escapeHtml13(model.error)}</p>` : ""}
|
|
4265
6110
|
</section>`;
|
|
4266
6111
|
};
|
|
4267
6112
|
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 +6335,7 @@ var createVoiceProviderSimulationControlsStore = (options) => {
|
|
|
4490
6335
|
};
|
|
4491
6336
|
|
|
4492
6337
|
// src/client/providerSimulationControlsWidget.ts
|
|
4493
|
-
var
|
|
6338
|
+
var escapeHtml14 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4494
6339
|
var formatKind = (kind) => (kind ?? "stt").toUpperCase();
|
|
4495
6340
|
var createVoiceProviderSimulationControlsViewModel = (snapshot, options) => {
|
|
4496
6341
|
const configuredProviders = options.providers.filter((provider) => provider.configured !== false);
|
|
@@ -4510,18 +6355,18 @@ var createVoiceProviderSimulationControlsViewModel = (snapshot, options) => {
|
|
|
4510
6355
|
};
|
|
4511
6356
|
var renderVoiceProviderSimulationControlsHTML = (snapshot, options) => {
|
|
4512
6357
|
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="${
|
|
6358
|
+
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("");
|
|
6359
|
+
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
6360
|
return `<section class="absolute-voice-provider-simulation absolute-voice-provider-simulation--${snapshot.error ? "error" : snapshot.isRunning ? "running" : "ready"}">
|
|
4516
6361
|
<header class="absolute-voice-provider-simulation__header">
|
|
4517
|
-
<span class="absolute-voice-provider-simulation__eyebrow">${
|
|
4518
|
-
<strong class="absolute-voice-provider-simulation__label">${
|
|
6362
|
+
<span class="absolute-voice-provider-simulation__eyebrow">${escapeHtml14(model.title)}</span>
|
|
6363
|
+
<strong class="absolute-voice-provider-simulation__label">${escapeHtml14(model.label)}</strong>
|
|
4519
6364
|
</header>
|
|
4520
|
-
<p class="absolute-voice-provider-simulation__description">${
|
|
4521
|
-
${model.canSimulateFailure ? "" : `<p class="absolute-voice-provider-simulation__empty">${
|
|
6365
|
+
<p class="absolute-voice-provider-simulation__description">${escapeHtml14(model.description)}</p>
|
|
6366
|
+
${model.canSimulateFailure ? "" : `<p class="absolute-voice-provider-simulation__empty">${escapeHtml14(options.fallbackRequiredMessage ?? "Configure fallback providers before simulating failure.")}</p>`}
|
|
4522
6367
|
<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">${
|
|
6368
|
+
${snapshot.error ? `<p class="absolute-voice-provider-simulation__error">${escapeHtml14(snapshot.error)}</p>` : ""}
|
|
6369
|
+
${model.resultText ? `<pre class="absolute-voice-provider-simulation__result">${escapeHtml14(model.resultText)}</pre>` : ""}
|
|
4525
6370
|
</section>`;
|
|
4526
6371
|
};
|
|
4527
6372
|
var bindVoiceProviderSimulationControls = (element, store) => {
|
|
@@ -4781,7 +6626,7 @@ var useVoiceProviderCapabilities = (path = "/api/provider-capabilities", options
|
|
|
4781
6626
|
// src/client/providerCapabilitiesWidget.ts
|
|
4782
6627
|
var DEFAULT_TITLE9 = "Provider Capabilities";
|
|
4783
6628
|
var DEFAULT_DESCRIPTION9 = "Configured, selected, and healthy voice providers for this deployment.";
|
|
4784
|
-
var
|
|
6629
|
+
var escapeHtml15 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4785
6630
|
var formatProvider = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
|
|
4786
6631
|
var formatKind2 = (kind) => kind.toUpperCase();
|
|
4787
6632
|
var formatStatus2 = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
@@ -4836,25 +6681,25 @@ var createVoiceProviderCapabilitiesViewModel = (snapshot, options = {}) => {
|
|
|
4836
6681
|
};
|
|
4837
6682
|
var renderVoiceProviderCapabilitiesHTML = (snapshot, options = {}) => {
|
|
4838
6683
|
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--${
|
|
6684
|
+
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
6685
|
<header>
|
|
4841
|
-
<strong>${
|
|
4842
|
-
<span>${
|
|
6686
|
+
<strong>${escapeHtml15(capability.label)}</strong>
|
|
6687
|
+
<span>${escapeHtml15(formatStatus2(capability.status))}</span>
|
|
4843
6688
|
</header>
|
|
4844
|
-
<p>${
|
|
6689
|
+
<p>${escapeHtml15(capability.detail)}</p>
|
|
4845
6690
|
<dl>${capability.rows.map((row) => `<div>
|
|
4846
|
-
<dt>${
|
|
4847
|
-
<dd>${
|
|
6691
|
+
<dt>${escapeHtml15(row.label)}</dt>
|
|
6692
|
+
<dd>${escapeHtml15(row.value)}</dd>
|
|
4848
6693
|
</div>`).join("")}</dl>
|
|
4849
6694
|
</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--${
|
|
6695
|
+
return `<section class="absolute-voice-provider-capabilities absolute-voice-provider-capabilities--${escapeHtml15(model.status)}">
|
|
4851
6696
|
<header class="absolute-voice-provider-capabilities__header">
|
|
4852
|
-
<span class="absolute-voice-provider-capabilities__eyebrow">${
|
|
4853
|
-
<strong class="absolute-voice-provider-capabilities__label">${
|
|
6697
|
+
<span class="absolute-voice-provider-capabilities__eyebrow">${escapeHtml15(model.title)}</span>
|
|
6698
|
+
<strong class="absolute-voice-provider-capabilities__label">${escapeHtml15(model.label)}</strong>
|
|
4854
6699
|
</header>
|
|
4855
|
-
<p class="absolute-voice-provider-capabilities__description">${
|
|
6700
|
+
<p class="absolute-voice-provider-capabilities__description">${escapeHtml15(model.description)}</p>
|
|
4856
6701
|
${capabilities}
|
|
4857
|
-
${model.error ? `<p class="absolute-voice-provider-capabilities__error">${
|
|
6702
|
+
${model.error ? `<p class="absolute-voice-provider-capabilities__error">${escapeHtml15(model.error)}</p>` : ""}
|
|
4858
6703
|
</section>`;
|
|
4859
6704
|
};
|
|
4860
6705
|
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 +6917,7 @@ var useVoiceProviderContracts = (path = "/api/provider-contracts", options = {})
|
|
|
5072
6917
|
// src/client/providerContractsWidget.ts
|
|
5073
6918
|
var DEFAULT_TITLE10 = "Provider Contracts";
|
|
5074
6919
|
var DEFAULT_DESCRIPTION10 = "Production contract coverage for provider env, latency, fallback, streaming, and capabilities.";
|
|
5075
|
-
var
|
|
6920
|
+
var escapeHtml16 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
5076
6921
|
var formatProvider2 = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
|
|
5077
6922
|
var formatStatus3 = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
5078
6923
|
var contractDetail = (row) => {
|
|
@@ -5116,26 +6961,26 @@ var createVoiceProviderContractsViewModel = (snapshot, options = {}) => {
|
|
|
5116
6961
|
};
|
|
5117
6962
|
var renderVoiceProviderContractsHTML = (snapshot, options = {}) => {
|
|
5118
6963
|
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--${
|
|
6964
|
+
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
6965
|
<header>
|
|
5121
|
-
<strong>${
|
|
5122
|
-
<span>${
|
|
6966
|
+
<strong>${escapeHtml16(row.label)}</strong>
|
|
6967
|
+
<span>${escapeHtml16(formatStatus3(row.status))}</span>
|
|
5123
6968
|
</header>
|
|
5124
|
-
<p>${
|
|
5125
|
-
${row.remediations.length ? `<ul class="absolute-voice-provider-contracts__remediations">${row.remediations.map((remediation) => `<li>${remediation.href ? `<a href="${
|
|
6969
|
+
<p>${escapeHtml16(row.detail)}</p>
|
|
6970
|
+
${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
6971
|
<dl>${row.rows.map((item) => `<div>
|
|
5127
|
-
<dt>${
|
|
5128
|
-
<dd>${
|
|
6972
|
+
<dt>${escapeHtml16(item.label)}</dt>
|
|
6973
|
+
<dd>${escapeHtml16(item.value)}</dd>
|
|
5129
6974
|
</div>`).join("")}</dl>
|
|
5130
6975
|
</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--${
|
|
6976
|
+
return `<section class="absolute-voice-provider-contracts absolute-voice-provider-contracts--${escapeHtml16(model.status)}">
|
|
5132
6977
|
<header class="absolute-voice-provider-contracts__header">
|
|
5133
|
-
<span class="absolute-voice-provider-contracts__eyebrow">${
|
|
5134
|
-
<strong class="absolute-voice-provider-contracts__label">${
|
|
6978
|
+
<span class="absolute-voice-provider-contracts__eyebrow">${escapeHtml16(model.title)}</span>
|
|
6979
|
+
<strong class="absolute-voice-provider-contracts__label">${escapeHtml16(model.label)}</strong>
|
|
5135
6980
|
</header>
|
|
5136
|
-
<p class="absolute-voice-provider-contracts__description">${
|
|
6981
|
+
<p class="absolute-voice-provider-contracts__description">${escapeHtml16(model.description)}</p>
|
|
5137
6982
|
${rows}
|
|
5138
|
-
${model.error ? `<p class="absolute-voice-provider-contracts__error">${
|
|
6983
|
+
${model.error ? `<p class="absolute-voice-provider-contracts__error">${escapeHtml16(model.error)}</p>` : ""}
|
|
5139
6984
|
</section>`;
|
|
5140
6985
|
};
|
|
5141
6986
|
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 +7219,7 @@ var useVoiceProviderStatus = (path = "/api/provider-status", options = {}) => {
|
|
|
5374
7219
|
// src/client/providerStatusWidget.ts
|
|
5375
7220
|
var DEFAULT_TITLE11 = "Voice Providers";
|
|
5376
7221
|
var DEFAULT_DESCRIPTION11 = "Live provider health, fallback counts, latency, and suppression state from your self-hosted trace store.";
|
|
5377
|
-
var
|
|
7222
|
+
var escapeHtml17 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
5378
7223
|
var formatProvider3 = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
|
|
5379
7224
|
var formatStatus4 = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
5380
7225
|
var formatLatency = (value) => typeof value === "number" ? `${value}ms` : "No samples";
|
|
@@ -5430,25 +7275,25 @@ var createVoiceProviderStatusViewModel = (snapshot, options = {}) => {
|
|
|
5430
7275
|
};
|
|
5431
7276
|
var renderVoiceProviderStatusHTML = (snapshot, options = {}) => {
|
|
5432
7277
|
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--${
|
|
7278
|
+
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
7279
|
<header>
|
|
5435
|
-
<strong>${
|
|
5436
|
-
<span>${
|
|
7280
|
+
<strong>${escapeHtml17(provider.label)}</strong>
|
|
7281
|
+
<span>${escapeHtml17(formatStatus4(provider.status))}</span>
|
|
5437
7282
|
</header>
|
|
5438
|
-
<p>${
|
|
7283
|
+
<p>${escapeHtml17(provider.detail)}</p>
|
|
5439
7284
|
<dl>${provider.rows.map((row) => `<div>
|
|
5440
|
-
<dt>${
|
|
5441
|
-
<dd>${
|
|
7285
|
+
<dt>${escapeHtml17(row.label)}</dt>
|
|
7286
|
+
<dd>${escapeHtml17(row.value)}</dd>
|
|
5442
7287
|
</div>`).join("")}</dl>
|
|
5443
7288
|
</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--${
|
|
7289
|
+
return `<section class="absolute-voice-provider-status absolute-voice-provider-status--${escapeHtml17(model.status)}">
|
|
5445
7290
|
<header class="absolute-voice-provider-status__header">
|
|
5446
|
-
<span class="absolute-voice-provider-status__eyebrow">${
|
|
5447
|
-
<strong class="absolute-voice-provider-status__label">${
|
|
7291
|
+
<span class="absolute-voice-provider-status__eyebrow">${escapeHtml17(model.title)}</span>
|
|
7292
|
+
<strong class="absolute-voice-provider-status__label">${escapeHtml17(model.label)}</strong>
|
|
5448
7293
|
</header>
|
|
5449
|
-
<p class="absolute-voice-provider-status__description">${
|
|
7294
|
+
<p class="absolute-voice-provider-status__description">${escapeHtml17(model.description)}</p>
|
|
5450
7295
|
${providers}
|
|
5451
|
-
${model.error ? `<p class="absolute-voice-provider-status__error">${
|
|
7296
|
+
${model.error ? `<p class="absolute-voice-provider-status__error">${escapeHtml17(model.error)}</p>` : ""}
|
|
5452
7297
|
</section>`;
|
|
5453
7298
|
};
|
|
5454
7299
|
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 +7516,7 @@ var useVoiceRoutingStatus = (path = "/api/routing/latest", options = {}) => {
|
|
|
5671
7516
|
// src/client/routingStatusWidget.ts
|
|
5672
7517
|
var DEFAULT_TITLE12 = "Voice Routing";
|
|
5673
7518
|
var DEFAULT_DESCRIPTION12 = "Latest provider routing decision from the self-hosted trace store.";
|
|
5674
|
-
var
|
|
7519
|
+
var escapeHtml18 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
5675
7520
|
var formatValue = (value, fallback = "None") => typeof value === "string" && value.trim() ? value : typeof value === "number" && Number.isFinite(value) ? String(value) : fallback;
|
|
5676
7521
|
var formatProviderRoutes2 = (routes) => routes && typeof routes === "object" ? Object.entries(routes).map(([role, provider]) => `${role}: ${formatValue(provider)}`).join(", ") || "None" : "None";
|
|
5677
7522
|
var getProviderRoute = (routes, role) => routes && typeof routes === "object" ? formatValue(routes[role], "Not configured") : "Not configured";
|
|
@@ -5753,22 +7598,22 @@ var createVoiceRoutingStatusViewModel = (snapshot, options = {}) => {
|
|
|
5753
7598
|
var renderVoiceRoutingStatusHTML = (snapshot, options = {}) => {
|
|
5754
7599
|
const model = createVoiceRoutingStatusViewModel(snapshot, options);
|
|
5755
7600
|
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>${
|
|
7601
|
+
<span>${escapeHtml18(item.label)}</span>
|
|
7602
|
+
<strong>${escapeHtml18(item.value)}</strong>
|
|
5758
7603
|
</div>`).join("")}</div>` : "";
|
|
5759
7604
|
const rows = model.rows.length ? `<div class="absolute-voice-routing-status__grid">${model.rows.map((row) => `<div>
|
|
5760
|
-
<span>${
|
|
5761
|
-
<strong>${
|
|
7605
|
+
<span>${escapeHtml18(row.label)}</span>
|
|
7606
|
+
<strong>${escapeHtml18(row.value)}</strong>
|
|
5762
7607
|
</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--${
|
|
7608
|
+
return `<section class="absolute-voice-routing-status absolute-voice-routing-status--${escapeHtml18(model.status)}">
|
|
5764
7609
|
<header class="absolute-voice-routing-status__header">
|
|
5765
|
-
<span class="absolute-voice-routing-status__eyebrow">${
|
|
5766
|
-
<strong class="absolute-voice-routing-status__label">${
|
|
7610
|
+
<span class="absolute-voice-routing-status__eyebrow">${escapeHtml18(model.title)}</span>
|
|
7611
|
+
<strong class="absolute-voice-routing-status__label">${escapeHtml18(model.label)}</strong>
|
|
5767
7612
|
</header>
|
|
5768
|
-
<p class="absolute-voice-routing-status__description">${
|
|
7613
|
+
<p class="absolute-voice-routing-status__description">${escapeHtml18(model.description)}</p>
|
|
5769
7614
|
${activeStack}
|
|
5770
7615
|
${rows}
|
|
5771
|
-
${model.error ? `<p class="absolute-voice-routing-status__error">${
|
|
7616
|
+
${model.error ? `<p class="absolute-voice-routing-status__error">${escapeHtml18(model.error)}</p>` : ""}
|
|
5772
7617
|
</section>`;
|
|
5773
7618
|
};
|
|
5774
7619
|
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 +7793,7 @@ var createVoiceTraceTimelineStore = (path = "/api/voice-traces", options = {}) =
|
|
|
5948
7793
|
// src/client/traceTimelineWidget.ts
|
|
5949
7794
|
var DEFAULT_TITLE13 = "Voice Traces";
|
|
5950
7795
|
var DEFAULT_DESCRIPTION13 = "Latest call timelines with provider latency, fallbacks, handoffs, and errors from your self-hosted trace store.";
|
|
5951
|
-
var
|
|
7796
|
+
var escapeHtml19 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
5952
7797
|
var formatMs3 = (value) => typeof value === "number" ? `${value}ms` : "n/a";
|
|
5953
7798
|
var formatProviders = (session) => session.providers.length ? session.providers.map((provider) => provider.provider).join(", ") : "No providers";
|
|
5954
7799
|
var createVoiceTraceTimelineViewModel = (snapshot, options = {}) => {
|
|
@@ -5978,27 +7823,27 @@ var renderVoiceTraceTimelineWidgetHTML = (snapshot, options = {}) => {
|
|
|
5978
7823
|
const model = createVoiceTraceTimelineViewModel(snapshot, options);
|
|
5979
7824
|
const sessions = model.sessions.length ? `<div class="absolute-voice-trace-timeline__sessions">${model.sessions.map((session) => {
|
|
5980
7825
|
const supportLinks = [
|
|
5981
|
-
`<a href="${
|
|
5982
|
-
session.operationsRecordHref ? `<a href="${
|
|
5983
|
-
session.incidentBundleHref ? `<a href="${
|
|
7826
|
+
`<a href="${escapeHtml19(session.detailHref)}">Open timeline</a>`,
|
|
7827
|
+
session.operationsRecordHref ? `<a href="${escapeHtml19(session.operationsRecordHref)}">Open operations record</a>` : undefined,
|
|
7828
|
+
session.incidentBundleHref ? `<a href="${escapeHtml19(session.incidentBundleHref)}">Export incident bundle</a>` : undefined
|
|
5984
7829
|
].filter(Boolean).join("");
|
|
5985
|
-
return `<article class="absolute-voice-trace-timeline__session absolute-voice-trace-timeline__session--${
|
|
7830
|
+
return `<article class="absolute-voice-trace-timeline__session absolute-voice-trace-timeline__session--${escapeHtml19(session.status)}">
|
|
5986
7831
|
<header>
|
|
5987
|
-
<strong>${
|
|
5988
|
-
<span>${
|
|
7832
|
+
<strong>${escapeHtml19(session.sessionId)}</strong>
|
|
7833
|
+
<span>${escapeHtml19(session.status)}</span>
|
|
5989
7834
|
</header>
|
|
5990
|
-
<p>${
|
|
7835
|
+
<p>${escapeHtml19(session.label)} \xB7 ${escapeHtml19(session.durationLabel)} \xB7 ${escapeHtml19(session.providerLabel)}</p>
|
|
5991
7836
|
<p class="absolute-voice-trace-timeline__actions">${supportLinks}</p>
|
|
5992
7837
|
</article>`;
|
|
5993
7838
|
}).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--${
|
|
7839
|
+
return `<section class="absolute-voice-trace-timeline absolute-voice-trace-timeline--${escapeHtml19(model.status)}">
|
|
5995
7840
|
<header class="absolute-voice-trace-timeline__header">
|
|
5996
|
-
<span class="absolute-voice-trace-timeline__eyebrow">${
|
|
5997
|
-
<strong class="absolute-voice-trace-timeline__label">${
|
|
7841
|
+
<span class="absolute-voice-trace-timeline__eyebrow">${escapeHtml19(model.title)}</span>
|
|
7842
|
+
<strong class="absolute-voice-trace-timeline__label">${escapeHtml19(model.label)}</strong>
|
|
5998
7843
|
</header>
|
|
5999
|
-
<p class="absolute-voice-trace-timeline__description">${
|
|
7844
|
+
<p class="absolute-voice-trace-timeline__description">${escapeHtml19(model.description)}</p>
|
|
6000
7845
|
${sessions}
|
|
6001
|
-
${model.error ? `<p class="absolute-voice-trace-timeline__error">${
|
|
7846
|
+
${model.error ? `<p class="absolute-voice-trace-timeline__error">${escapeHtml19(model.error)}</p>` : ""}
|
|
6002
7847
|
</section>`;
|
|
6003
7848
|
};
|
|
6004
7849
|
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 +8002,8 @@ var VoiceTraceTimeline = ({
|
|
|
6157
8002
|
import { useEffect as useEffect15, useRef as useRef15, useSyncExternalStore as useSyncExternalStore15 } from "react";
|
|
6158
8003
|
|
|
6159
8004
|
// src/client/agentSquadStatus.ts
|
|
6160
|
-
var
|
|
6161
|
-
var getPayloadString = (event, key) =>
|
|
8005
|
+
var getString4 = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
8006
|
+
var getPayloadString = (event, key) => getString4(event.payload?.[key]);
|
|
6162
8007
|
var eventStatus = (event) => {
|
|
6163
8008
|
const status = getPayloadString(event, "status");
|
|
6164
8009
|
if (status === "blocked")
|
|
@@ -6250,7 +8095,7 @@ var useVoiceAgentSquadStatus = (path = "/api/voice-traces", options = {}) => {
|
|
|
6250
8095
|
// src/client/agentSquadStatusWidget.ts
|
|
6251
8096
|
var DEFAULT_TITLE14 = "Voice Agent Squad";
|
|
6252
8097
|
var DEFAULT_DESCRIPTION14 = "Current specialist and recent handoffs from your self-hosted voice traces.";
|
|
6253
|
-
var
|
|
8098
|
+
var escapeHtml20 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6254
8099
|
var labelFor = (current) => {
|
|
6255
8100
|
if (!current)
|
|
6256
8101
|
return "Waiting for specialist activity";
|
|
@@ -6277,24 +8122,24 @@ var renderVoiceAgentSquadStatusHTML = (snapshot, options = {}) => {
|
|
|
6277
8122
|
const model = createVoiceAgentSquadStatusViewModel(snapshot, options);
|
|
6278
8123
|
const current = model.current;
|
|
6279
8124
|
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>${
|
|
8125
|
+
<span>${escapeHtml20(session.sessionId)}</span>
|
|
8126
|
+
<strong>${escapeHtml20(session.targetAgentId ?? "none")}</strong>
|
|
8127
|
+
<em>${escapeHtml20(session.status)}</em>
|
|
8128
|
+
${session.summary || session.reason ? `<p>${escapeHtml20(session.summary ?? session.reason ?? "")}</p>` : ""}
|
|
6284
8129
|
</li>`).join("") : "<li><span>No squad traces yet.</span><strong>Waiting</strong></li>";
|
|
6285
8130
|
return `<section class="absolute-voice-agent-squad-status">
|
|
6286
8131
|
<header>
|
|
6287
|
-
<span>${
|
|
6288
|
-
<strong>${
|
|
8132
|
+
<span>${escapeHtml20(model.title)}</span>
|
|
8133
|
+
<strong>${escapeHtml20(model.label)}</strong>
|
|
6289
8134
|
</header>
|
|
6290
|
-
<p>${
|
|
8135
|
+
<p>${escapeHtml20(model.description)}</p>
|
|
6291
8136
|
<div>
|
|
6292
|
-
<span>Session</span><strong>${
|
|
6293
|
-
<span>From</span><strong>${
|
|
6294
|
-
<span>Status</span><strong>${
|
|
8137
|
+
<span>Session</span><strong>${escapeHtml20(current?.sessionId ?? "n/a")}</strong>
|
|
8138
|
+
<span>From</span><strong>${escapeHtml20(current?.fromAgentId ?? "n/a")}</strong>
|
|
8139
|
+
<span>Status</span><strong>${escapeHtml20(current?.status ?? "idle")}</strong>
|
|
6295
8140
|
</div>
|
|
6296
8141
|
<ul>${rows}</ul>
|
|
6297
|
-
${model.error ? `<p class="absolute-voice-agent-squad-status__error">${
|
|
8142
|
+
${model.error ? `<p class="absolute-voice-agent-squad-status__error">${escapeHtml20(model.error)}</p>` : ""}
|
|
6298
8143
|
</section>`;
|
|
6299
8144
|
};
|
|
6300
8145
|
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 +8376,7 @@ var useVoiceTurnLatency = (path = "/api/turn-latency", options = {}) => {
|
|
|
6531
8376
|
var DEFAULT_TITLE15 = "Turn Latency";
|
|
6532
8377
|
var DEFAULT_DESCRIPTION15 = "Per-turn timing from first transcript to commit and assistant response start.";
|
|
6533
8378
|
var DEFAULT_PROOF_LABEL = "Run latency proof";
|
|
6534
|
-
var
|
|
8379
|
+
var escapeHtml21 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6535
8380
|
var formatMs4 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
|
|
6536
8381
|
var createVoiceTurnLatencyViewModel = (snapshot, options = {}) => {
|
|
6537
8382
|
const turns = (snapshot.report?.turns ?? []).map((turn) => ({
|
|
@@ -6559,25 +8404,25 @@ var createVoiceTurnLatencyViewModel = (snapshot, options = {}) => {
|
|
|
6559
8404
|
};
|
|
6560
8405
|
var renderVoiceTurnLatencyHTML = (snapshot, options = {}) => {
|
|
6561
8406
|
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--${
|
|
8407
|
+
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
8408
|
<header>
|
|
6564
|
-
<strong>${
|
|
6565
|
-
<span>${
|
|
8409
|
+
<strong>${escapeHtml21(turn.label)}</strong>
|
|
8410
|
+
<span>${escapeHtml21(turn.status)}</span>
|
|
6566
8411
|
</header>
|
|
6567
8412
|
<dl>${turn.rows.map((row) => `<div>
|
|
6568
|
-
<dt>${
|
|
6569
|
-
<dd>${
|
|
8413
|
+
<dt>${escapeHtml21(row.label)}</dt>
|
|
8414
|
+
<dd>${escapeHtml21(row.value)}</dd>
|
|
6570
8415
|
</div>`).join("")}</dl>
|
|
6571
8416
|
</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--${
|
|
8417
|
+
return `<section class="absolute-voice-turn-latency absolute-voice-turn-latency--${escapeHtml21(model.status)}">
|
|
6573
8418
|
<header class="absolute-voice-turn-latency__header">
|
|
6574
|
-
<span class="absolute-voice-turn-latency__eyebrow">${
|
|
6575
|
-
<strong class="absolute-voice-turn-latency__label">${
|
|
8419
|
+
<span class="absolute-voice-turn-latency__eyebrow">${escapeHtml21(model.title)}</span>
|
|
8420
|
+
<strong class="absolute-voice-turn-latency__label">${escapeHtml21(model.label)}</strong>
|
|
6576
8421
|
</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">${
|
|
8422
|
+
<p class="absolute-voice-turn-latency__description">${escapeHtml21(model.description)}</p>
|
|
8423
|
+
${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
8424
|
${turns}
|
|
6580
|
-
${model.error ? `<p class="absolute-voice-turn-latency__error">${
|
|
8425
|
+
${model.error ? `<p class="absolute-voice-turn-latency__error">${escapeHtml21(model.error)}</p>` : ""}
|
|
6581
8426
|
</section>`;
|
|
6582
8427
|
};
|
|
6583
8428
|
var mountVoiceTurnLatency = (element, path = "/api/turn-latency", options = {}) => {
|
|
@@ -6813,7 +8658,7 @@ var useVoiceTurnQuality = (path = "/api/turn-quality", options = {}) => {
|
|
|
6813
8658
|
// src/client/turnQualityWidget.ts
|
|
6814
8659
|
var DEFAULT_TITLE16 = "Turn Quality";
|
|
6815
8660
|
var DEFAULT_DESCRIPTION16 = "Per-turn STT confidence, fallback selection, corrections, and transcript coverage.";
|
|
6816
|
-
var
|
|
8661
|
+
var escapeHtml22 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6817
8662
|
var formatConfidence = (value) => typeof value === "number" ? `${Math.round(value * 100)}%` : "n/a";
|
|
6818
8663
|
var formatMaybe = (value) => value === undefined || value === "" ? "n/a" : String(value);
|
|
6819
8664
|
var getTurnDetail = (turn) => {
|
|
@@ -6863,25 +8708,25 @@ var createVoiceTurnQualityViewModel = (snapshot, options = {}) => {
|
|
|
6863
8708
|
};
|
|
6864
8709
|
var renderVoiceTurnQualityHTML = (snapshot, options = {}) => {
|
|
6865
8710
|
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--${
|
|
8711
|
+
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
8712
|
<header>
|
|
6868
|
-
<strong>${
|
|
6869
|
-
<span>${
|
|
8713
|
+
<strong>${escapeHtml22(turn.label)}</strong>
|
|
8714
|
+
<span>${escapeHtml22(turn.status)}</span>
|
|
6870
8715
|
</header>
|
|
6871
|
-
<p>${
|
|
8716
|
+
<p>${escapeHtml22(turn.detail)}</p>
|
|
6872
8717
|
<dl>${turn.rows.map((row) => `<div>
|
|
6873
|
-
<dt>${
|
|
6874
|
-
<dd>${
|
|
8718
|
+
<dt>${escapeHtml22(row.label)}</dt>
|
|
8719
|
+
<dd>${escapeHtml22(row.value)}</dd>
|
|
6875
8720
|
</div>`).join("")}</dl>
|
|
6876
8721
|
</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--${
|
|
8722
|
+
return `<section class="absolute-voice-turn-quality absolute-voice-turn-quality--${escapeHtml22(model.status)}">
|
|
6878
8723
|
<header class="absolute-voice-turn-quality__header">
|
|
6879
|
-
<span class="absolute-voice-turn-quality__eyebrow">${
|
|
6880
|
-
<strong class="absolute-voice-turn-quality__label">${
|
|
8724
|
+
<span class="absolute-voice-turn-quality__eyebrow">${escapeHtml22(model.title)}</span>
|
|
8725
|
+
<strong class="absolute-voice-turn-quality__label">${escapeHtml22(model.label)}</strong>
|
|
6881
8726
|
</header>
|
|
6882
|
-
<p class="absolute-voice-turn-quality__description">${
|
|
8727
|
+
<p class="absolute-voice-turn-quality__description">${escapeHtml22(model.description)}</p>
|
|
6883
8728
|
${turns}
|
|
6884
|
-
${model.error ? `<p class="absolute-voice-turn-quality__error">${
|
|
8729
|
+
${model.error ? `<p class="absolute-voice-turn-quality__error">${escapeHtml22(model.error)}</p>` : ""}
|
|
6885
8730
|
</section>`;
|
|
6886
8731
|
};
|
|
6887
8732
|
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}`;
|