@absolutejs/voice 0.0.22-beta.200 → 0.0.22-beta.202
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/README.md +1 -1
- package/dist/evalRoutes.d.ts +6 -0
- package/dist/index.js +33 -9
- package/dist/operationsRecord.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1378,7 +1378,7 @@ app.use(
|
|
|
1378
1378
|
);
|
|
1379
1379
|
```
|
|
1380
1380
|
|
|
1381
|
-
`createVoiceOperationsRecordRoutes(...)` links the call/session timeline, replay, provider
|
|
1381
|
+
`createVoiceOperationsRecordRoutes(...)` links the call/session timeline, transcript, replay, provider decisions, tools, handoffs, audit, reviews, ops tasks, integration events, and sink delivery attempts into one debuggable object. Use `/voice-operations/:sessionId` as the first place to investigate failed calls, provider failures, handoff failures, slow turns, and campaign attempts. The same mount also exposes incident handoff Markdown at `/voice-operations/:sessionId/incident.md` and `/api/voice-operations/:sessionId/incident.md` for support tooling.
|
|
1382
1382
|
|
|
1383
1383
|
Mount `createVoiceOpsRecoveryRoutes(...)` beside it when operators need one deploy-checkable recovery signal:
|
|
1384
1384
|
|
package/dist/evalRoutes.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type VoiceEvalStatus = 'pass' | 'fail';
|
|
|
5
5
|
export type VoiceEvalSessionReport = {
|
|
6
6
|
endedAt?: number;
|
|
7
7
|
eventCount: number;
|
|
8
|
+
operationsRecordHref?: string;
|
|
8
9
|
quality: VoiceQualityReport;
|
|
9
10
|
scenarioId?: string;
|
|
10
11
|
sessionId: string;
|
|
@@ -81,6 +82,7 @@ export type VoiceScenarioEvalDefinition = {
|
|
|
81
82
|
export type VoiceScenarioEvalSessionResult = {
|
|
82
83
|
eventCount: number;
|
|
83
84
|
issues: string[];
|
|
85
|
+
operationsRecordHref?: string;
|
|
84
86
|
sessionId: string;
|
|
85
87
|
status: VoiceEvalStatus;
|
|
86
88
|
};
|
|
@@ -142,6 +144,7 @@ export type VoiceEvalRoutesOptions = {
|
|
|
142
144
|
links?: VoiceEvalLink[];
|
|
143
145
|
limit?: number;
|
|
144
146
|
name?: string;
|
|
147
|
+
operationsRecordHref?: false | string | ((sessionId: string) => string);
|
|
145
148
|
path?: string;
|
|
146
149
|
scenarios?: VoiceScenarioEvalDefinition[];
|
|
147
150
|
store?: VoiceTraceEventStore;
|
|
@@ -151,17 +154,20 @@ export type VoiceEvalRoutesOptions = {
|
|
|
151
154
|
export declare const runVoiceSessionEvals: (options?: {
|
|
152
155
|
events?: StoredVoiceTraceEvent[];
|
|
153
156
|
limit?: number;
|
|
157
|
+
operationsRecordHref?: false | string | ((sessionId: string) => string);
|
|
154
158
|
store?: VoiceTraceEventStore;
|
|
155
159
|
thresholds?: VoiceQualityThresholds;
|
|
156
160
|
}) => Promise<VoiceEvalReport>;
|
|
157
161
|
export declare const runVoiceScenarioEvals: (options?: {
|
|
158
162
|
events?: StoredVoiceTraceEvent[];
|
|
163
|
+
operationsRecordHref?: false | string | ((sessionId: string) => string);
|
|
159
164
|
scenarios?: VoiceScenarioEvalDefinition[];
|
|
160
165
|
store?: VoiceTraceEventStore;
|
|
161
166
|
}) => Promise<VoiceScenarioEvalReport>;
|
|
162
167
|
export declare const runVoiceScenarioFixtureEvals: (options?: {
|
|
163
168
|
fixtures?: VoiceScenarioFixture[];
|
|
164
169
|
fixtureStore?: VoiceScenarioFixtureStore;
|
|
170
|
+
operationsRecordHref?: false | string | ((sessionId: string) => string);
|
|
165
171
|
scenarios?: VoiceScenarioEvalDefinition[];
|
|
166
172
|
}) => Promise<VoiceScenarioFixtureEvalReport>;
|
|
167
173
|
export declare const compareVoiceEvalBaseline: (currentReport: VoiceEvalReport, baselineReport: VoiceEvalReport, options?: VoiceEvalBaselineComparisonOptions) => VoiceEvalBaselineComparison;
|
package/dist/index.js
CHANGED
|
@@ -13573,6 +13573,17 @@ var escapeHtml20 = (value) => value.replaceAll("&", "&").replaceAll("<", "&l
|
|
|
13573
13573
|
var rate2 = (count, total) => count / Math.max(1, total);
|
|
13574
13574
|
var normalizeSearchText = (value) => value.trim().toLowerCase();
|
|
13575
13575
|
var getString9 = (value) => typeof value === "string" ? value : undefined;
|
|
13576
|
+
var resolveSessionHref = (value, sessionId) => {
|
|
13577
|
+
if (value === false) {
|
|
13578
|
+
return;
|
|
13579
|
+
}
|
|
13580
|
+
const href = value ?? "/voice-operations/:sessionId";
|
|
13581
|
+
if (typeof href === "function") {
|
|
13582
|
+
return href(sessionId);
|
|
13583
|
+
}
|
|
13584
|
+
const encoded = encodeURIComponent(sessionId);
|
|
13585
|
+
return href.includes(":sessionId") ? href.replace(":sessionId", encoded) : `${href.replace(/\/$/, "")}/${encoded}`;
|
|
13586
|
+
};
|
|
13576
13587
|
var getObject = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
13577
13588
|
var getPathValue = (value, path) => {
|
|
13578
13589
|
let current = value;
|
|
@@ -13638,6 +13649,7 @@ var runVoiceSessionEvals = async (options = {}) => {
|
|
|
13638
13649
|
return {
|
|
13639
13650
|
endedAt,
|
|
13640
13651
|
eventCount: sorted.length,
|
|
13652
|
+
operationsRecordHref: resolveSessionHref(options.operationsRecordHref, sessionId),
|
|
13641
13653
|
quality,
|
|
13642
13654
|
scenarioId,
|
|
13643
13655
|
sessionId,
|
|
@@ -13662,7 +13674,7 @@ var runVoiceSessionEvals = async (options = {}) => {
|
|
|
13662
13674
|
var getSessionText = (events, type) => events.filter((event) => event.type === type).map((event) => getString9(event.payload.text)).filter((text) => Boolean(text?.trim())).join(`
|
|
13663
13675
|
`);
|
|
13664
13676
|
var countProviderErrors = (events) => events.filter((event) => event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.provider === "string")).length;
|
|
13665
|
-
var evaluateScenarioSession = (scenario, sessionId, events) => {
|
|
13677
|
+
var evaluateScenarioSession = (scenario, sessionId, events, operationsRecordHref) => {
|
|
13666
13678
|
const issues = [];
|
|
13667
13679
|
const committedText = getSessionText(events, "turn.committed");
|
|
13668
13680
|
const assistantText = getSessionText(events, "turn.assistant");
|
|
@@ -13729,6 +13741,7 @@ var evaluateScenarioSession = (scenario, sessionId, events) => {
|
|
|
13729
13741
|
return {
|
|
13730
13742
|
eventCount: events.length,
|
|
13731
13743
|
issues,
|
|
13744
|
+
operationsRecordHref,
|
|
13732
13745
|
sessionId,
|
|
13733
13746
|
status: issues.length > 0 ? "fail" : "pass"
|
|
13734
13747
|
};
|
|
@@ -13741,7 +13754,7 @@ var runVoiceScenarioEvals = async (options = {}) => {
|
|
|
13741
13754
|
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
13742
13755
|
}
|
|
13743
13756
|
const results = scenarios.map((scenario) => {
|
|
13744
|
-
const sessions = [...grouped.entries()].filter(([, sessionEvents]) => scenario.scenarioId ? sessionEvents.some((event) => event.scenarioId === scenario.scenarioId) : true).map(([sessionId, sessionEvents]) => evaluateScenarioSession(scenario, sessionId, filterVoiceTraceEvents(sessionEvents))).sort((left, right) => left.sessionId.localeCompare(right.sessionId));
|
|
13757
|
+
const sessions = [...grouped.entries()].filter(([, sessionEvents]) => scenario.scenarioId ? sessionEvents.some((event) => event.scenarioId === scenario.scenarioId) : true).map(([sessionId, sessionEvents]) => evaluateScenarioSession(scenario, sessionId, filterVoiceTraceEvents(sessionEvents), resolveSessionHref(options.operationsRecordHref, sessionId))).sort((left, right) => left.sessionId.localeCompare(right.sessionId));
|
|
13745
13758
|
const issues = [];
|
|
13746
13759
|
const minSessions = scenario.minSessions ?? 1;
|
|
13747
13760
|
if (sessions.length < minSessions) {
|
|
@@ -13778,6 +13791,7 @@ var runVoiceScenarioFixtureEvals = async (options = {}) => {
|
|
|
13778
13791
|
const results = await Promise.all(fixtures.map(async (fixture) => {
|
|
13779
13792
|
const report = await runVoiceScenarioEvals({
|
|
13780
13793
|
events: fixture.events,
|
|
13794
|
+
operationsRecordHref: options.operationsRecordHref,
|
|
13781
13795
|
scenarios: options.scenarios
|
|
13782
13796
|
});
|
|
13783
13797
|
return {
|
|
@@ -13902,7 +13916,8 @@ var renderVoiceEvalHTML = (report, options = {}) => {
|
|
|
13902
13916
|
const trend = report.trend.length ? report.trend.map((bucket) => `<tr><td>${escapeHtml20(bucket.key)}</td><td>${bucket.total}</td><td>${bucket.passed}</td><td>${bucket.failed}</td></tr>`).join("") : '<tr><td colspan="4">No eval buckets yet.</td></tr>';
|
|
13903
13917
|
const sessions = report.sessions.length ? report.sessions.map((session) => {
|
|
13904
13918
|
const failedMetrics = Object.entries(session.quality.metrics).filter(([, metric]) => !metric.pass).map(([, metric]) => metric.label).join(", ");
|
|
13905
|
-
|
|
13919
|
+
const sessionLabel = session.operationsRecordHref ? `<a href="${escapeHtml20(session.operationsRecordHref)}">${escapeHtml20(session.sessionId)}</a>` : escapeHtml20(session.sessionId);
|
|
13920
|
+
return `<tr class="${session.status}"><td>${sessionLabel}</td><td>${escapeHtml20(session.status)}</td><td>${session.eventCount}</td><td>${session.summary.turnCount}</td><td>${session.summary.errorCount}</td><td>${escapeHtml20(formatTime(session.endedAt))}</td><td>${escapeHtml20(failedMetrics || "none")}</td></tr>`;
|
|
13906
13921
|
}).join("") : '<tr><td colspan="7">No sessions found.</td></tr>';
|
|
13907
13922
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml20(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1180px;margin:auto}nav{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.eyebrow{font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.status{border-radius:999px;display:inline-flex;font-weight:800;padding:.35rem .75rem}.pass{color:#166534}.fail{color:#991b1b}.status.pass{background:#dcfce7}.status.fail{background:#fee2e2}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:1rem 0}.card,.primitive{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.primitive{background:#fffdf7;border-color:#d6c7a3;margin:1rem 0}.primitive p{line-height:1.55}.primitive pre{background:#181713;border-radius:.85rem;color:#fef3c7;overflow:auto;padding:1rem}.primitive code{color:#fef3c7}.card strong{display:block;font-size:2rem}table{border-collapse:collapse;background:white;width:100%;margin:1rem 0 2rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}tr.fail td{border-left:4px solid #dc2626}tr.pass td{border-left:4px solid #16a34a}</style></head><body><main>${links}<h1>${escapeHtml20(title)}</h1><p class="status ${report.status}">${report.status}</p><div class="grid"><article class="card"><span>Total</span><strong>${report.total}</strong></article><article class="card"><span>Passed</span><strong>${report.passed}</strong></article><article class="card"><span>Failed</span><strong>${report.failed}</strong></article></div>${renderVoiceEvalPrimitiveCopy()}<h2>Trend</h2><table><thead><tr><th>Day</th><th>Total</th><th>Passed</th><th>Failed</th></tr></thead><tbody>${trend}</tbody></table><h2>Session Eval Results</h2><table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Turns</th><th>Errors</th><th>Last event</th><th>Failed metrics</th></tr></thead><tbody>${sessions}</tbody></table></main></body></html>`;
|
|
13908
13923
|
};
|
|
@@ -13919,7 +13934,10 @@ var renderVoiceScenarioEvalHTML = (report, options = {}) => {
|
|
|
13919
13934
|
const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml20(link.href)}">${escapeHtml20(link.label)}</a>`).join("")}</nav>` : "";
|
|
13920
13935
|
const scenarios = report.scenarios.length ? report.scenarios.map((scenario) => {
|
|
13921
13936
|
const scenarioIssues = scenario.issues.length ? `<ul>${scenario.issues.map((issue) => `<li>${escapeHtml20(issue)}</li>`).join("")}</ul>` : "";
|
|
13922
|
-
const sessions = scenario.sessions.length ? scenario.sessions.map((session) =>
|
|
13937
|
+
const sessions = scenario.sessions.length ? scenario.sessions.map((session) => {
|
|
13938
|
+
const sessionLabel = session.operationsRecordHref ? `<a href="${escapeHtml20(session.operationsRecordHref)}">${escapeHtml20(session.sessionId)}</a>` : escapeHtml20(session.sessionId);
|
|
13939
|
+
return `<tr class="${session.status}"><td>${sessionLabel}</td><td>${escapeHtml20(session.status)}</td><td>${session.eventCount}</td><td>${escapeHtml20(session.issues.join(", ") || "none")}</td></tr>`;
|
|
13940
|
+
}).join("") : '<tr><td colspan="4">No matching sessions.</td></tr>';
|
|
13923
13941
|
return `<section class="scenario ${scenario.status}"><h2>${escapeHtml20(scenario.label)}</h2>${scenario.description ? `<p>${escapeHtml20(scenario.description)}</p>` : ""}<p class="status ${scenario.status}">${scenario.status}</p><p>${scenario.passed} passed, ${scenario.failed} failed, ${scenario.matchedSessions} matched.</p>${scenarioIssues}<table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Issues</th></tr></thead><tbody>${sessions}</tbody></table></section>`;
|
|
13924
13942
|
}).join("") : "<section><p>No scenarios configured.</p></section>";
|
|
13925
13943
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml20(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1180px;margin:auto}nav{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.eyebrow{font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.status{border-radius:999px;display:inline-flex;font-weight:800;padding:.35rem .75rem}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:1rem 0}.card,section{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.primitive{background:#fffdf7;border-color:#d6c7a3}.primitive p{line-height:1.55}.primitive pre{background:#181713;border-radius:.85rem;color:#fef3c7;overflow:auto;padding:1rem}.primitive code{color:#fef3c7}.card strong{display:block;font-size:2rem}section{margin:1rem 0}table{border-collapse:collapse;width:100%;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}tr.fail td{border-left:4px solid #dc2626}tr.pass td{border-left:4px solid #16a34a}</style></head><body><main>${links}<h1>${escapeHtml20(title)}</h1><p class="status ${report.status}">${report.status}</p><div class="grid"><article class="card"><span>Total</span><strong>${report.total}</strong></article><article class="card"><span>Passed</span><strong>${report.passed}</strong></article><article class="card"><span>Failed</span><strong>${report.failed}</strong></article></div>${renderVoiceEvalPrimitiveCopy()}${scenarios}</main></body></html>`;
|
|
@@ -13941,6 +13959,7 @@ var createVoiceEvalRoutes = (options) => {
|
|
|
13941
13959
|
const getReport = () => runVoiceSessionEvals({
|
|
13942
13960
|
events: options.events,
|
|
13943
13961
|
limit: options.limit,
|
|
13962
|
+
operationsRecordHref: options.operationsRecordHref,
|
|
13944
13963
|
store: options.store,
|
|
13945
13964
|
thresholds: options.thresholds
|
|
13946
13965
|
});
|
|
@@ -13951,12 +13970,14 @@ var createVoiceEvalRoutes = (options) => {
|
|
|
13951
13970
|
};
|
|
13952
13971
|
const getScenarioReport = () => runVoiceScenarioEvals({
|
|
13953
13972
|
events: options.events,
|
|
13973
|
+
operationsRecordHref: options.operationsRecordHref,
|
|
13954
13974
|
scenarios: options.scenarios,
|
|
13955
13975
|
store: options.store
|
|
13956
13976
|
});
|
|
13957
13977
|
const getFixtureReport = () => runVoiceScenarioFixtureEvals({
|
|
13958
13978
|
fixtures: options.fixtures,
|
|
13959
13979
|
fixtureStore: options.fixtureStore,
|
|
13980
|
+
operationsRecordHref: options.operationsRecordHref,
|
|
13960
13981
|
scenarios: options.scenarios
|
|
13961
13982
|
});
|
|
13962
13983
|
routes.get(path, async () => {
|
|
@@ -15170,7 +15191,7 @@ var escapeHtml24 = (value) => value.replaceAll("&", "&").replaceAll("<", "&l
|
|
|
15170
15191
|
var increment4 = (record, key) => {
|
|
15171
15192
|
record[key] = (record[key] ?? 0) + 1;
|
|
15172
15193
|
};
|
|
15173
|
-
var
|
|
15194
|
+
var resolveSessionHref2 = (value, session) => {
|
|
15174
15195
|
if (value === false) {
|
|
15175
15196
|
return;
|
|
15176
15197
|
}
|
|
@@ -15334,7 +15355,7 @@ var summarizeVoiceSessions = async (options = {}) => {
|
|
|
15334
15355
|
const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
|
|
15335
15356
|
return {
|
|
15336
15357
|
...item,
|
|
15337
|
-
operationsRecordHref:
|
|
15358
|
+
operationsRecordHref: resolveSessionHref2(options.operationsRecordHref, item),
|
|
15338
15359
|
replayHref
|
|
15339
15360
|
};
|
|
15340
15361
|
});
|
|
@@ -23959,7 +23980,7 @@ var eventStatus = (event) => firstString3(event.payload, [
|
|
|
23959
23980
|
"reason"
|
|
23960
23981
|
]);
|
|
23961
23982
|
var eventElapsedMs2 = (event) => firstNumber3(event.payload, ["elapsedMs", "latencyMs", "durationMs"]);
|
|
23962
|
-
var
|
|
23983
|
+
var resolveSessionHref3 = (value, sessionId) => {
|
|
23963
23984
|
if (value === false) {
|
|
23964
23985
|
return;
|
|
23965
23986
|
}
|
|
@@ -24089,7 +24110,7 @@ var summarizeVoiceTraceTimeline = (events, options = {}) => {
|
|
|
24089
24110
|
type: event.type
|
|
24090
24111
|
})),
|
|
24091
24112
|
lastEventAt: sorted.at(-1)?.at,
|
|
24092
|
-
operationsRecordHref:
|
|
24113
|
+
operationsRecordHref: resolveSessionHref3(options.operationsRecordHref, sessionId),
|
|
24093
24114
|
providers: summarizeProviders(sorted),
|
|
24094
24115
|
sessionId,
|
|
24095
24116
|
startedAt: summary.startedAt,
|
|
@@ -24207,6 +24228,7 @@ var hasPayloadValue = (payload, key, values) => {
|
|
|
24207
24228
|
return typeof value === "string" && values.has(value);
|
|
24208
24229
|
};
|
|
24209
24230
|
var countIntegrationDeliveryStatus = (events, status) => events.filter((event) => event.deliveryStatus === status).length;
|
|
24231
|
+
var resolveRoutePath = (path, sessionId) => path.replace(":sessionId", encodeURIComponent(sessionId));
|
|
24210
24232
|
var toHandoff = (event) => ({
|
|
24211
24233
|
at: event.at,
|
|
24212
24234
|
fromAgentId: getString17(event.payload.fromAgentId),
|
|
@@ -24390,7 +24412,8 @@ var renderVoiceOperationsRecordHTML = (record, options = {}) => {
|
|
|
24390
24412
|
})
|
|
24391
24413
|
);`);
|
|
24392
24414
|
const incidentMarkdown = escapeHtml41(renderVoiceOperationsRecordIncidentMarkdown(record));
|
|
24393
|
-
|
|
24415
|
+
const incidentLink = options.incidentHref ? `<a href="${escapeHtml41(options.incidentHref)}">Download incident.md</a>` : "";
|
|
24416
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml41(options.title ?? "Voice Operations Record")}</title><style>body{background:#101417;color:#f9f4e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.eyebrow{color:#fbbf24;font-size:.8rem;font-weight:900;letter-spacing:.14em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.8rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #475569;border-radius:999px;display:inline-flex;padding:8px 12px}.healthy{color:#86efac}.warning{color:#fbbf24}.failed,.error{color:#fca5a5}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.card,.primitive{background:#182025;border:1px solid #2d3a43;border-radius:20px;padding:16px}.card span,.muted,.label{color:#a9b4bd}.label{display:block;font-size:.72rem;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.card strong{display:block;font-size:2rem}section{margin-top:28px}article{display:grid;gap:8px}ul{display:grid;gap:10px;list-style:none;padding:0}li{background:#182025;border:1px solid #2d3a43;border-radius:16px;padding:14px}pre{background:#080d10;border:1px solid #2d3a43;border-radius:16px;color:#dbeafe;overflow:auto;padding:14px}.hero-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}.hero-actions a{background:#fbbf24;border-radius:999px;color:#111827;font-weight:900;padding:10px 14px;text-decoration:none}.two-column{display:grid;gap:18px;grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr)}@media(max-width:860px){main{padding:20px}.two-column{grid-template-columns:1fr}}</style></head><body><main><p class="eyebrow">Call log replacement</p><h1>${escapeHtml41(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml41(record.status)}">${escapeHtml41(record.status)}</p><div class="hero-actions"><a href="#transcript">Transcript</a><a href="#provider-decisions">Provider decisions</a><a href="#incident-handoff">Incident handoff</a>${incidentLink}</div><section class="grid"><div class="card"><span>Events</span><strong>${String(record.summary.eventCount)}</strong></div><div class="card"><span>Turns</span><strong>${String(record.summary.turnCount)}</strong></div><div class="card"><span>Errors</span><strong>${String(record.summary.errorCount)}</strong></div><div class="card"><span>Duration</span><strong>${formatMs4(record.summary.callDurationMs)}</strong></div><div class="card"><span>Audit</span><strong>${String(record.audit?.total ?? 0)}</strong></div><div class="card"><span>Reviews</span><strong>${String(record.reviews?.total ?? 0)}</strong></div><div class="card"><span>Tasks</span><strong>${String(record.tasks?.total ?? 0)}</strong></div><div class="card"><span>Integrations</span><strong>${String(record.integrationEvents?.total ?? 0)}</strong></div></section><section class="two-column"><div><h2 id="transcript">Transcript</h2><ul>${transcript}</ul></div><div><h2 id="provider-decisions">Provider Decisions</h2><ul>${providerDecisions}</ul></div></section><section id="incident-handoff"><h2>Copyable Incident Handoff</h2><p class="muted">Paste this into Slack, Linear, Zendesk, or an incident review. ${incidentLink}</p><pre><code>${incidentMarkdown}</code></pre></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceOperationsRecordRoutes(...)</code> gives every call one debuggable object</h2><p class="muted">Use this as the support/debug payload across traces, provider routing, tools, handoffs, audit, latency, replay, reviews, tasks, and webhook delivery.</p><pre><code>${snippet}</code></pre></section><section><h2>Provider Summary</h2><div class="grid">${providers}</div></section><section><h2>Handoffs</h2><ul>${handoffs}</ul></section><section><h2>Tools</h2><ul>${tools}</ul></section><section><h2>Reviews</h2><ul>${reviews}</ul></section><section><h2>Tasks</h2><ul>${tasks}</ul></section><section><h2>Integration Events</h2><ul>${integrationEvents}</ul></section></main></body></html>`;
|
|
24394
24417
|
};
|
|
24395
24418
|
var createVoiceOperationsRecordRoutes = (options) => {
|
|
24396
24419
|
const path = options.path ?? "/api/voice-operations/:sessionId";
|
|
@@ -24432,6 +24455,7 @@ var createVoiceOperationsRecordRoutes = (options) => {
|
|
|
24432
24455
|
routes.get(htmlPath, async ({ params }) => {
|
|
24433
24456
|
const record = await buildRecord(getSessionId(params));
|
|
24434
24457
|
const body = await (options.render ?? ((input) => renderVoiceOperationsRecordHTML(input, {
|
|
24458
|
+
incidentHref: incidentHtmlPath ? resolveRoutePath(incidentHtmlPath, input.sessionId) : undefined,
|
|
24435
24459
|
title: options.title
|
|
24436
24460
|
})))(record);
|
|
24437
24461
|
return new Response(body, {
|
|
@@ -125,6 +125,7 @@ export type VoiceOperationsRecordRoutesOptions = Omit<VoiceOperationsRecordOptio
|
|
|
125
125
|
export declare const buildVoiceOperationsRecord: (options: VoiceOperationsRecordOptions) => Promise<VoiceOperationsRecord>;
|
|
126
126
|
export declare const renderVoiceOperationsRecordIncidentMarkdown: (record: VoiceOperationsRecord) => string;
|
|
127
127
|
export declare const renderVoiceOperationsRecordHTML: (record: VoiceOperationsRecord, options?: {
|
|
128
|
+
incidentHref?: string;
|
|
128
129
|
title?: string;
|
|
129
130
|
}) => string;
|
|
130
131
|
export declare const createVoiceOperationsRecordRoutes: (options: VoiceOperationsRecordRoutesOptions) => Elysia<"", {
|