@absolutejs/voice 0.0.22-beta.41 → 0.0.22-beta.43
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/evalRoutes.d.ts +123 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1113 -855
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7623,230 +7623,188 @@ var createVoiceDiagnosticsRoutes = (options) => {
|
|
|
7623
7623
|
});
|
|
7624
7624
|
return routes;
|
|
7625
7625
|
};
|
|
7626
|
-
// src/
|
|
7626
|
+
// src/evalRoutes.ts
|
|
7627
|
+
import { Elysia as Elysia7 } from "elysia";
|
|
7628
|
+
import { mkdir } from "fs/promises";
|
|
7629
|
+
import { dirname } from "path";
|
|
7630
|
+
|
|
7631
|
+
// src/qualityRoutes.ts
|
|
7632
|
+
import { Elysia as Elysia6 } from "elysia";
|
|
7633
|
+
|
|
7634
|
+
// src/handoffHealth.ts
|
|
7627
7635
|
import { Elysia as Elysia5 } from "elysia";
|
|
7628
|
-
var getString4 = (value) => typeof value === "string" ? value : undefined;
|
|
7629
7636
|
var escapeHtml7 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
7637
|
+
var getString4 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
7638
|
+
var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
|
|
7630
7639
|
var increment2 = (record, key) => {
|
|
7631
7640
|
record[key] = (record[key] ?? 0) + 1;
|
|
7632
7641
|
};
|
|
7633
|
-
var
|
|
7634
|
-
const
|
|
7635
|
-
const getTurn = (turnId) => {
|
|
7636
|
-
const existing = turns.get(turnId);
|
|
7637
|
-
if (existing) {
|
|
7638
|
-
return existing;
|
|
7639
|
-
}
|
|
7640
|
-
const turn = {
|
|
7641
|
-
assistantReplies: [],
|
|
7642
|
-
errors: [],
|
|
7643
|
-
id: turnId,
|
|
7644
|
-
modelCalls: [],
|
|
7645
|
-
tools: [],
|
|
7646
|
-
transcripts: []
|
|
7647
|
-
};
|
|
7648
|
-
turns.set(turnId, turn);
|
|
7649
|
-
return turn;
|
|
7650
|
-
};
|
|
7651
|
-
for (const event of events) {
|
|
7652
|
-
const turnId = event.turnId ?? "session";
|
|
7653
|
-
const turn = getTurn(turnId);
|
|
7654
|
-
switch (event.type) {
|
|
7655
|
-
case "turn.transcript":
|
|
7656
|
-
turn.transcripts.push({
|
|
7657
|
-
isFinal: event.payload.isFinal === true,
|
|
7658
|
-
text: getString4(event.payload.text)
|
|
7659
|
-
});
|
|
7660
|
-
break;
|
|
7661
|
-
case "turn.committed":
|
|
7662
|
-
turn.committedText = getString4(event.payload.text);
|
|
7663
|
-
break;
|
|
7664
|
-
case "turn.assistant": {
|
|
7665
|
-
const text = getString4(event.payload.text);
|
|
7666
|
-
if (text) {
|
|
7667
|
-
turn.assistantReplies.push(text);
|
|
7668
|
-
}
|
|
7669
|
-
break;
|
|
7670
|
-
}
|
|
7671
|
-
case "agent.model":
|
|
7672
|
-
case "assistant.run":
|
|
7673
|
-
turn.modelCalls.push(event.payload);
|
|
7674
|
-
break;
|
|
7675
|
-
case "agent.tool":
|
|
7676
|
-
turn.tools.push(event.payload);
|
|
7677
|
-
break;
|
|
7678
|
-
case "session.error":
|
|
7679
|
-
turn.errors.push(event.payload);
|
|
7680
|
-
break;
|
|
7681
|
-
}
|
|
7682
|
-
}
|
|
7683
|
-
return [...turns.values()];
|
|
7684
|
-
};
|
|
7685
|
-
var summarizeVoiceSessionReplay = async (options) => {
|
|
7686
|
-
const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
|
|
7687
|
-
const events = filterVoiceTraceEvents(sourceEvents, {
|
|
7688
|
-
sessionId: options.sessionId
|
|
7689
|
-
});
|
|
7690
|
-
const replay = buildVoiceTraceReplay(events, {
|
|
7691
|
-
evaluation: options.evaluation,
|
|
7692
|
-
redact: options.redact,
|
|
7693
|
-
title: options.title ?? `Voice Session ${options.sessionId}`
|
|
7694
|
-
});
|
|
7695
|
-
const startedAt = replay.summary.startedAt;
|
|
7642
|
+
var normalizeDelivery = (adapterId, value) => {
|
|
7643
|
+
const record = value && typeof value === "object" ? value : {};
|
|
7696
7644
|
return {
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
timeline: events.map((event) => ({
|
|
7704
|
-
at: event.at,
|
|
7705
|
-
offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
|
|
7706
|
-
payload: event.payload,
|
|
7707
|
-
turnId: event.turnId,
|
|
7708
|
-
type: event.type
|
|
7709
|
-
})),
|
|
7710
|
-
turns: buildReplayTurns(events)
|
|
7645
|
+
adapterId: getString4(record.adapterId) ?? adapterId,
|
|
7646
|
+
adapterKind: getString4(record.adapterKind),
|
|
7647
|
+
deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
|
|
7648
|
+
deliveredTo: getString4(record.deliveredTo),
|
|
7649
|
+
error: getString4(record.error),
|
|
7650
|
+
status: isStatus(record.status) ? record.status : "failed"
|
|
7711
7651
|
};
|
|
7712
7652
|
};
|
|
7713
|
-
var
|
|
7714
|
-
const
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
7653
|
+
var normalizeDeliveries = (payload) => {
|
|
7654
|
+
const deliveries = payload.deliveries;
|
|
7655
|
+
if (!deliveries || typeof deliveries !== "object") {
|
|
7656
|
+
return [];
|
|
7718
7657
|
}
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
if (outcome) {
|
|
7744
|
-
latestOutcome = outcome;
|
|
7745
|
-
}
|
|
7746
|
-
}
|
|
7658
|
+
return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
|
|
7659
|
+
};
|
|
7660
|
+
var resolveReplayHref = (event, replayHref) => {
|
|
7661
|
+
if (replayHref === false) {
|
|
7662
|
+
return;
|
|
7663
|
+
}
|
|
7664
|
+
if (typeof replayHref === "function") {
|
|
7665
|
+
return replayHref(event);
|
|
7666
|
+
}
|
|
7667
|
+
return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
|
|
7668
|
+
};
|
|
7669
|
+
var summarizeVoiceHandoffHealth = async (options = {}) => {
|
|
7670
|
+
const sourceEvents = options.events ?? await options.store?.list() ?? [];
|
|
7671
|
+
const search = options.q?.trim().toLowerCase();
|
|
7672
|
+
const byAction = {};
|
|
7673
|
+
const byAdapter = {};
|
|
7674
|
+
const byStatus = {
|
|
7675
|
+
delivered: 0,
|
|
7676
|
+
failed: 0,
|
|
7677
|
+
skipped: 0
|
|
7678
|
+
};
|
|
7679
|
+
const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
|
|
7680
|
+
const status = isStatus(event.payload.status) ? event.payload.status : "failed";
|
|
7681
|
+
const deliveries = normalizeDeliveries(event.payload);
|
|
7747
7682
|
const item = {
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
startedAt: summary.startedAt,
|
|
7756
|
-
status: errorCount > 0 ? "failed" : "healthy",
|
|
7757
|
-
transcriptCount: summary.transcriptCount,
|
|
7758
|
-
turnCount: summary.turnCount
|
|
7683
|
+
action: getString4(event.payload.action),
|
|
7684
|
+
at: event.at,
|
|
7685
|
+
deliveries,
|
|
7686
|
+
reason: getString4(event.payload.reason),
|
|
7687
|
+
sessionId: event.sessionId,
|
|
7688
|
+
status,
|
|
7689
|
+
target: getString4(event.payload.target)
|
|
7759
7690
|
};
|
|
7760
|
-
const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
|
|
7761
7691
|
return {
|
|
7762
7692
|
...item,
|
|
7763
|
-
replayHref
|
|
7693
|
+
replayHref: resolveReplayHref(item, options.replayHref)
|
|
7764
7694
|
};
|
|
7765
|
-
})
|
|
7766
|
-
|
|
7767
|
-
return sessions.filter((session) => {
|
|
7768
|
-
if (options.status && options.status !== "all" && session.status !== options.status) {
|
|
7769
|
-
return false;
|
|
7770
|
-
}
|
|
7771
|
-
if (options.provider && !session.providers.includes(options.provider)) {
|
|
7695
|
+
}).filter((event) => {
|
|
7696
|
+
if (options.status && options.status !== "all" && event.status !== options.status) {
|
|
7772
7697
|
return false;
|
|
7773
7698
|
}
|
|
7774
7699
|
if (!search) {
|
|
7775
7700
|
return true;
|
|
7776
7701
|
}
|
|
7777
7702
|
return [
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7703
|
+
event.action,
|
|
7704
|
+
event.reason,
|
|
7705
|
+
event.sessionId,
|
|
7706
|
+
event.status,
|
|
7707
|
+
event.target,
|
|
7708
|
+
...event.deliveries.flatMap((delivery) => [
|
|
7709
|
+
delivery.adapterId,
|
|
7710
|
+
delivery.adapterKind,
|
|
7711
|
+
delivery.deliveredTo,
|
|
7712
|
+
delivery.error,
|
|
7713
|
+
delivery.status
|
|
7714
|
+
])
|
|
7782
7715
|
].some((value) => value?.toLowerCase().includes(search));
|
|
7783
|
-
}).sort((left, right) =>
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
`<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
|
|
7797
|
-
`<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
|
|
7798
|
-
"</dl>",
|
|
7799
|
-
session.latestOutcome ? `<p>Outcome: ${escapeHtml7(session.latestOutcome)}</p>` : "",
|
|
7800
|
-
session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml7).join(", ")}</p>` : "",
|
|
7801
|
-
session.replayHref ? `<p><a href="${escapeHtml7(session.replayHref)}">Open replay</a></p>` : "",
|
|
7802
|
-
"</article>"
|
|
7803
|
-
].join("")),
|
|
7804
|
-
"</div>"
|
|
7805
|
-
].join("");
|
|
7806
|
-
var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
|
|
7807
|
-
...options,
|
|
7808
|
-
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
7809
|
-
provider: query?.provider ?? options.provider,
|
|
7810
|
-
q: query?.q ?? options.q,
|
|
7811
|
-
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
7812
|
-
});
|
|
7813
|
-
var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
|
|
7814
|
-
const sessions = await summarizeVoiceSessions({
|
|
7815
|
-
...options,
|
|
7816
|
-
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
7817
|
-
provider: query?.provider ?? options.provider,
|
|
7818
|
-
q: query?.q ?? options.q,
|
|
7819
|
-
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
7820
|
-
});
|
|
7821
|
-
const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
|
|
7822
|
-
return new Response(body, {
|
|
7823
|
-
headers: {
|
|
7824
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
7825
|
-
...options.headers
|
|
7716
|
+
}).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
|
|
7717
|
+
for (const event of events) {
|
|
7718
|
+
byStatus[event.status] += 1;
|
|
7719
|
+
if (event.action) {
|
|
7720
|
+
increment2(byAction, event.action);
|
|
7721
|
+
}
|
|
7722
|
+
for (const delivery of event.deliveries) {
|
|
7723
|
+
byAdapter[delivery.adapterId] ??= {
|
|
7724
|
+
delivered: 0,
|
|
7725
|
+
failed: 0,
|
|
7726
|
+
skipped: 0
|
|
7727
|
+
};
|
|
7728
|
+
byAdapter[delivery.adapterId][delivery.status] += 1;
|
|
7826
7729
|
}
|
|
7827
|
-
});
|
|
7828
|
-
};
|
|
7829
|
-
var createVoiceSessionListRoutes = (options = {}) => {
|
|
7830
|
-
const path = options.path ?? "/api/voice-sessions";
|
|
7831
|
-
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
7832
|
-
const routes = new Elysia5({
|
|
7833
|
-
name: options.name ?? "absolutejs-voice-session-list"
|
|
7834
|
-
}).get(path, createVoiceSessionsJSONHandler(options));
|
|
7835
|
-
if (htmlPath) {
|
|
7836
|
-
routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
|
|
7837
7730
|
}
|
|
7838
|
-
return
|
|
7731
|
+
return {
|
|
7732
|
+
byAction,
|
|
7733
|
+
byAdapter,
|
|
7734
|
+
byStatus,
|
|
7735
|
+
events,
|
|
7736
|
+
failed: byStatus.failed,
|
|
7737
|
+
total: events.length
|
|
7738
|
+
};
|
|
7839
7739
|
};
|
|
7840
|
-
var
|
|
7740
|
+
var renderMetricGrid = (summary) => [
|
|
7741
|
+
'<section class="voice-handoff-health-grid">',
|
|
7742
|
+
`<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
|
|
7743
|
+
`<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
|
|
7744
|
+
`<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
|
|
7745
|
+
`<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
|
|
7746
|
+
"</section>"
|
|
7747
|
+
].join("");
|
|
7748
|
+
var renderActionSummary = (summary) => {
|
|
7749
|
+
const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
|
|
7750
|
+
const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
|
|
7751
|
+
return [
|
|
7752
|
+
'<section class="voice-handoff-health-columns">',
|
|
7753
|
+
"<article><h3>Actions</h3>",
|
|
7754
|
+
actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml7(action)}: ${String(count)}</li>`).join("")}</ul>`,
|
|
7755
|
+
"</article>",
|
|
7756
|
+
"<article><h3>Adapters</h3>",
|
|
7757
|
+
adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml7(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
|
|
7758
|
+
"</article>",
|
|
7759
|
+
"</section>"
|
|
7760
|
+
].join("");
|
|
7761
|
+
};
|
|
7762
|
+
var renderVoiceHandoffHealthHTML = (summary) => [
|
|
7763
|
+
'<div class="voice-handoff-health">',
|
|
7764
|
+
renderMetricGrid(summary),
|
|
7765
|
+
renderActionSummary(summary),
|
|
7766
|
+
"<section>",
|
|
7767
|
+
"<h3>Recent Handoffs</h3>",
|
|
7768
|
+
summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
|
|
7769
|
+
'<div class="voice-handoff-health-events">',
|
|
7770
|
+
...summary.events.map((event) => [
|
|
7771
|
+
`<article class="${escapeHtml7(event.status)}">`,
|
|
7772
|
+
'<div class="voice-handoff-health-event-header">',
|
|
7773
|
+
`<strong>${escapeHtml7(event.action ?? "handoff")}</strong>`,
|
|
7774
|
+
`<span>${escapeHtml7(event.status)}</span>`,
|
|
7775
|
+
"</div>",
|
|
7776
|
+
`<p><small>${escapeHtml7(event.sessionId)}</small></p>`,
|
|
7777
|
+
event.target ? `<p>Target: ${escapeHtml7(event.target)}</p>` : "",
|
|
7778
|
+
event.reason ? `<p>Reason: ${escapeHtml7(event.reason)}</p>` : "",
|
|
7779
|
+
event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
|
|
7780
|
+
"<li>",
|
|
7781
|
+
`${escapeHtml7(delivery.adapterId)}: ${escapeHtml7(delivery.status)}`,
|
|
7782
|
+
delivery.deliveredTo ? ` to ${escapeHtml7(delivery.deliveredTo)}` : "",
|
|
7783
|
+
delivery.error ? ` (${escapeHtml7(delivery.error)})` : "",
|
|
7784
|
+
"</li>"
|
|
7785
|
+
].join("")).join("")}</ul>` : "",
|
|
7786
|
+
event.replayHref ? `<p><a href="${escapeHtml7(event.replayHref)}">Open replay</a></p>` : "",
|
|
7787
|
+
"</article>"
|
|
7788
|
+
].join("")),
|
|
7789
|
+
"</div>"
|
|
7790
|
+
].join(""),
|
|
7791
|
+
"</section>",
|
|
7792
|
+
"</div>"
|
|
7793
|
+
].join("");
|
|
7794
|
+
var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
|
|
7841
7795
|
...options,
|
|
7842
|
-
|
|
7796
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
7797
|
+
q: query?.q ?? options.q,
|
|
7798
|
+
status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
|
|
7843
7799
|
});
|
|
7844
|
-
var
|
|
7845
|
-
const
|
|
7800
|
+
var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
|
|
7801
|
+
const summary = await summarizeVoiceHandoffHealth({
|
|
7846
7802
|
...options,
|
|
7847
|
-
|
|
7803
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
7804
|
+
q: query?.q ?? options.q,
|
|
7805
|
+
status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
|
|
7848
7806
|
});
|
|
7849
|
-
const body = await (options.render?.(
|
|
7807
|
+
const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
|
|
7850
7808
|
return new Response(body, {
|
|
7851
7809
|
headers: {
|
|
7852
7810
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -7854,112 +7812,744 @@ var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
|
|
|
7854
7812
|
}
|
|
7855
7813
|
});
|
|
7856
7814
|
};
|
|
7857
|
-
var
|
|
7858
|
-
const path = options.path ?? "/api/voice-
|
|
7815
|
+
var createVoiceHandoffHealthRoutes = (options = {}) => {
|
|
7816
|
+
const path = options.path ?? "/api/voice-handoffs";
|
|
7859
7817
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
7860
7818
|
const routes = new Elysia5({
|
|
7861
|
-
name: options.name ?? "absolutejs-voice-
|
|
7862
|
-
}).get(path,
|
|
7819
|
+
name: options.name ?? "absolutejs-voice-handoff-health"
|
|
7820
|
+
}).get(path, createVoiceHandoffHealthJSONHandler(options));
|
|
7863
7821
|
if (htmlPath) {
|
|
7864
|
-
routes.get(htmlPath,
|
|
7822
|
+
routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
|
|
7865
7823
|
}
|
|
7866
7824
|
return routes;
|
|
7867
7825
|
};
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
return [];
|
|
7880
|
-
}
|
|
7881
|
-
throw error;
|
|
7882
|
-
}
|
|
7883
|
-
};
|
|
7884
|
-
var encodeStoreId = (id) => `${encodeURIComponent(id)}.json`;
|
|
7885
|
-
var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
|
|
7886
|
-
var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
|
|
7887
|
-
var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
7888
|
-
var writeJsonFile = async (path, value, options) => {
|
|
7889
|
-
await mkdir(options.directory, {
|
|
7890
|
-
recursive: true
|
|
7891
|
-
});
|
|
7892
|
-
const tempPath = `${path}.${crypto.randomUUID()}.tmp`;
|
|
7893
|
-
await writeFile(tempPath, JSON.stringify(value, null, options.pretty === false ? undefined : 2));
|
|
7894
|
-
await rename(tempPath, path);
|
|
7826
|
+
|
|
7827
|
+
// src/qualityRoutes.ts
|
|
7828
|
+
var DEFAULT_THRESHOLDS = {
|
|
7829
|
+
maxDuplicateTurnRate: 0,
|
|
7830
|
+
maxEmptyTurnRate: 0.02,
|
|
7831
|
+
maxHandoffFailureRate: 0,
|
|
7832
|
+
maxMissingAssistantReplyRate: 0.05,
|
|
7833
|
+
maxProviderAverageLatencyMs: 3000,
|
|
7834
|
+
maxProviderErrorRate: 0.05,
|
|
7835
|
+
maxProviderFallbackRate: 0.25,
|
|
7836
|
+
maxProviderTimeoutRate: 0.03
|
|
7895
7837
|
};
|
|
7896
|
-
var
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7838
|
+
var getString5 = (value) => typeof value === "string" ? value : undefined;
|
|
7839
|
+
var getNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
7840
|
+
var rate = (count, total) => count / Math.max(1, total);
|
|
7841
|
+
var roundMetric2 = (value) => Math.round(value * 1e4) / 1e4;
|
|
7842
|
+
var createMetric = (input) => ({
|
|
7843
|
+
...input,
|
|
7844
|
+
actual: roundMetric2(input.actual),
|
|
7845
|
+
pass: input.actual <= input.threshold
|
|
7846
|
+
});
|
|
7847
|
+
var evaluateVoiceQuality = async (input) => {
|
|
7848
|
+
const events = filterVoiceTraceEvents(input.events ?? await input.store?.list() ?? []);
|
|
7849
|
+
const thresholds = {
|
|
7850
|
+
...DEFAULT_THRESHOLDS,
|
|
7851
|
+
...input.thresholds
|
|
7907
7852
|
};
|
|
7908
|
-
const
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7853
|
+
const committedTurns = events.filter((event) => event.type === "turn.committed");
|
|
7854
|
+
const assistantReplies = events.filter((event) => event.type === "turn.assistant");
|
|
7855
|
+
const sessionIdsWithAssistantReply = new Set(assistantReplies.map((event) => event.sessionId));
|
|
7856
|
+
const sessionsWithTurns = new Set(committedTurns.map((event) => event.sessionId));
|
|
7857
|
+
const emptyTurns = committedTurns.filter((event) => !getString5(event.payload.text)?.trim());
|
|
7858
|
+
const turnTextsBySession = new Map;
|
|
7859
|
+
let duplicateTurns = 0;
|
|
7860
|
+
for (const turn of committedTurns) {
|
|
7861
|
+
const normalized = getString5(turn.payload.text)?.trim().toLowerCase();
|
|
7862
|
+
if (!normalized) {
|
|
7863
|
+
continue;
|
|
7912
7864
|
}
|
|
7913
|
-
const
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
};
|
|
7917
|
-
const set = async (id, value) => {
|
|
7918
|
-
await writeJsonFile(resolveFilePath(options.directory, id), value, options);
|
|
7919
|
-
};
|
|
7920
|
-
const list = async () => {
|
|
7921
|
-
const files = await listJsonFiles(options.directory);
|
|
7922
|
-
const sessions = await Promise.all(files.map((file) => readJsonFile(file)));
|
|
7923
|
-
return sessions.map((session) => toVoiceSessionSummary(session)).sort((first, second) => (second.lastActivityAt ?? second.createdAt) - (first.lastActivityAt ?? first.createdAt));
|
|
7924
|
-
};
|
|
7925
|
-
const remove = async (id) => {
|
|
7926
|
-
await rm(resolveFilePath(options.directory, id), {
|
|
7927
|
-
force: true
|
|
7928
|
-
});
|
|
7929
|
-
};
|
|
7930
|
-
return { get, getOrCreate, list, remove, set };
|
|
7931
|
-
};
|
|
7932
|
-
var createVoiceFileReviewStore = (options) => {
|
|
7933
|
-
const get = async (id) => {
|
|
7934
|
-
const path = resolveFilePath(options.directory, id);
|
|
7935
|
-
try {
|
|
7936
|
-
return await readJsonFile(path);
|
|
7937
|
-
} catch (error) {
|
|
7938
|
-
if (error.code === "ENOENT") {
|
|
7939
|
-
return;
|
|
7940
|
-
}
|
|
7941
|
-
throw error;
|
|
7865
|
+
const seen = turnTextsBySession.get(turn.sessionId) ?? new Set;
|
|
7866
|
+
if (seen.has(normalized)) {
|
|
7867
|
+
duplicateTurns += 1;
|
|
7942
7868
|
}
|
|
7869
|
+
seen.add(normalized);
|
|
7870
|
+
turnTextsBySession.set(turn.sessionId, seen);
|
|
7871
|
+
}
|
|
7872
|
+
const missingAssistantReplySessions = [...sessionsWithTurns].filter((sessionId) => !sessionIdsWithAssistantReply.has(sessionId)).length;
|
|
7873
|
+
const providerEvents = events.filter((event) => event.type === "session.error" && typeof event.payload.provider === "string" && typeof event.payload.providerStatus === "string");
|
|
7874
|
+
const providerErrors = providerEvents.filter((event) => event.payload.providerStatus === "error");
|
|
7875
|
+
const providerFallbacks = providerEvents.filter((event) => event.payload.providerStatus === "fallback");
|
|
7876
|
+
const providerTimeouts = providerEvents.filter((event) => event.payload.timedOut === true);
|
|
7877
|
+
const providerLatencies = providerEvents.map((event) => getNumber3(event.payload.elapsedMs)).filter((value) => value !== undefined);
|
|
7878
|
+
const averageProviderLatencyMs = providerLatencies.length > 0 ? providerLatencies.reduce((sum, value) => sum + value, 0) / providerLatencies.length : 0;
|
|
7879
|
+
const handoffHealth = await summarizeVoiceHandoffHealth({ events });
|
|
7880
|
+
const metrics = {
|
|
7881
|
+
duplicateTurnRate: createMetric({
|
|
7882
|
+
actual: rate(duplicateTurns, committedTurns.length),
|
|
7883
|
+
label: "Duplicate turn rate",
|
|
7884
|
+
threshold: thresholds.maxDuplicateTurnRate,
|
|
7885
|
+
unit: "rate"
|
|
7886
|
+
}),
|
|
7887
|
+
emptyTurnRate: createMetric({
|
|
7888
|
+
actual: rate(emptyTurns.length, committedTurns.length),
|
|
7889
|
+
label: "Empty turn rate",
|
|
7890
|
+
threshold: thresholds.maxEmptyTurnRate,
|
|
7891
|
+
unit: "rate"
|
|
7892
|
+
}),
|
|
7893
|
+
handoffFailureRate: createMetric({
|
|
7894
|
+
actual: rate(handoffHealth.failed, handoffHealth.total),
|
|
7895
|
+
label: "Handoff failure rate",
|
|
7896
|
+
threshold: thresholds.maxHandoffFailureRate,
|
|
7897
|
+
unit: "rate"
|
|
7898
|
+
}),
|
|
7899
|
+
missingAssistantReplyRate: createMetric({
|
|
7900
|
+
actual: rate(missingAssistantReplySessions, sessionsWithTurns.size),
|
|
7901
|
+
label: "Missing assistant reply rate",
|
|
7902
|
+
threshold: thresholds.maxMissingAssistantReplyRate,
|
|
7903
|
+
unit: "rate"
|
|
7904
|
+
}),
|
|
7905
|
+
providerAverageLatencyMs: createMetric({
|
|
7906
|
+
actual: averageProviderLatencyMs,
|
|
7907
|
+
label: "Average provider latency",
|
|
7908
|
+
threshold: thresholds.maxProviderAverageLatencyMs,
|
|
7909
|
+
unit: "ms"
|
|
7910
|
+
}),
|
|
7911
|
+
providerErrorRate: createMetric({
|
|
7912
|
+
actual: rate(providerErrors.length, providerEvents.length),
|
|
7913
|
+
label: "Provider error rate",
|
|
7914
|
+
threshold: thresholds.maxProviderErrorRate,
|
|
7915
|
+
unit: "rate"
|
|
7916
|
+
}),
|
|
7917
|
+
providerFallbackRate: createMetric({
|
|
7918
|
+
actual: rate(providerFallbacks.length, providerEvents.length),
|
|
7919
|
+
label: "Provider fallback rate",
|
|
7920
|
+
threshold: thresholds.maxProviderFallbackRate,
|
|
7921
|
+
unit: "rate"
|
|
7922
|
+
}),
|
|
7923
|
+
providerTimeoutRate: createMetric({
|
|
7924
|
+
actual: rate(providerTimeouts.length, providerEvents.length),
|
|
7925
|
+
label: "Provider timeout rate",
|
|
7926
|
+
threshold: thresholds.maxProviderTimeoutRate,
|
|
7927
|
+
unit: "rate"
|
|
7928
|
+
})
|
|
7943
7929
|
};
|
|
7944
|
-
const
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
};
|
|
7952
|
-
const remove = async (id) => {
|
|
7953
|
-
await rm(resolveFilePath(options.directory, id), {
|
|
7954
|
-
force: true
|
|
7955
|
-
});
|
|
7930
|
+
const status = Object.values(metrics).every((metric) => metric.pass) ? "pass" : "fail";
|
|
7931
|
+
return {
|
|
7932
|
+
checkedAt: Date.now(),
|
|
7933
|
+
eventCount: events.length,
|
|
7934
|
+
metrics,
|
|
7935
|
+
status,
|
|
7936
|
+
thresholds
|
|
7956
7937
|
};
|
|
7957
|
-
return { get, list, remove, set };
|
|
7958
7938
|
};
|
|
7959
|
-
var
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7939
|
+
var escapeHtml8 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
7940
|
+
var formatMetricValue = (metric) => metric.unit === "rate" ? `${(metric.actual * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.actual)}ms` : String(metric.actual);
|
|
7941
|
+
var formatThreshold = (metric) => metric.unit === "rate" ? `${(metric.threshold * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.threshold)}ms` : String(metric.threshold);
|
|
7942
|
+
var renderVoiceQualityHTML = (report, options = {}) => {
|
|
7943
|
+
const rows = Object.entries(report.metrics).map(([key, metric]) => `<tr class="${metric.pass ? "pass" : "fail"}"><td>${escapeHtml8(metric.label)}</td><td>${escapeHtml8(formatMetricValue(metric))}</td><td>${escapeHtml8(formatThreshold(metric))}</td><td>${metric.pass ? "pass" : "fail"}</td><td><code>${escapeHtml8(key)}</code></td></tr>`).join("");
|
|
7944
|
+
const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml8(link.href)}">${escapeHtml8(link.label)}</a>`).join("")}</nav>` : "";
|
|
7945
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>AbsoluteJS Voice Quality</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}nav{display:flex;flex-wrap:wrap;gap:.5rem;margin:0 0 1.25rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;padding:.35rem .75rem;font-weight:800}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}table{border-collapse:collapse;width:100%;background:white;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}.pass td{border-left:4px solid #16a34a}.fail td{border-left:4px solid #dc2626}code{background:#f3f4f6;padding:.15rem .3rem;border-radius:.3rem}</style></head><body><main>${links}<h1>Voice quality gates</h1><p class="status ${report.status}">${report.status}</p><p>${report.eventCount} event(s) checked.</p><table><thead><tr><th>Metric</th><th>Actual</th><th>Threshold</th><th>Status</th><th>Key</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
|
|
7946
|
+
};
|
|
7947
|
+
var createVoiceQualityRoutes = (options) => {
|
|
7948
|
+
const path = options.path ?? "/quality";
|
|
7949
|
+
const routes = new Elysia6({
|
|
7950
|
+
name: options.name ?? "absolutejs-voice-quality"
|
|
7951
|
+
});
|
|
7952
|
+
const getReport = () => evaluateVoiceQuality({
|
|
7953
|
+
events: options.events,
|
|
7954
|
+
store: options.store,
|
|
7955
|
+
thresholds: options.thresholds
|
|
7956
|
+
});
|
|
7957
|
+
routes.get(path, async () => {
|
|
7958
|
+
const report = await getReport();
|
|
7959
|
+
return new Response(renderVoiceQualityHTML(report, { links: options.links }), {
|
|
7960
|
+
headers: {
|
|
7961
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7962
|
+
...options.headers
|
|
7963
|
+
}
|
|
7964
|
+
});
|
|
7965
|
+
});
|
|
7966
|
+
routes.get(`${path}/json`, async () => getReport());
|
|
7967
|
+
routes.get(`${path}/status`, async ({ set }) => {
|
|
7968
|
+
const report = await getReport();
|
|
7969
|
+
if (report.status === "fail") {
|
|
7970
|
+
set.status = 503;
|
|
7971
|
+
}
|
|
7972
|
+
return report;
|
|
7973
|
+
});
|
|
7974
|
+
return routes;
|
|
7975
|
+
};
|
|
7976
|
+
|
|
7977
|
+
// src/evalRoutes.ts
|
|
7978
|
+
var escapeHtml9 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
7979
|
+
var rate2 = (count, total) => count / Math.max(1, total);
|
|
7980
|
+
var sessionTime = (events) => {
|
|
7981
|
+
const sorted = filterVoiceTraceEvents(events);
|
|
7982
|
+
return {
|
|
7983
|
+
endedAt: sorted.at(-1)?.at,
|
|
7984
|
+
startedAt: sorted[0]?.at
|
|
7985
|
+
};
|
|
7986
|
+
};
|
|
7987
|
+
var bucketKey = (timestamp) => new Date(timestamp).toISOString().slice(0, 10);
|
|
7988
|
+
var buildTrend = (sessions) => {
|
|
7989
|
+
const buckets = new Map;
|
|
7990
|
+
for (const session of sessions) {
|
|
7991
|
+
const endedAt = session.endedAt ?? session.startedAt ?? session.quality.checkedAt;
|
|
7992
|
+
const key = bucketKey(endedAt);
|
|
7993
|
+
const bucket = buckets.get(key) ?? {
|
|
7994
|
+
endedAt,
|
|
7995
|
+
failed: 0,
|
|
7996
|
+
key,
|
|
7997
|
+
passed: 0,
|
|
7998
|
+
total: 0
|
|
7999
|
+
};
|
|
8000
|
+
bucket.endedAt = Math.max(bucket.endedAt, endedAt);
|
|
8001
|
+
bucket.total += 1;
|
|
8002
|
+
if (session.status === "pass") {
|
|
8003
|
+
bucket.passed += 1;
|
|
8004
|
+
} else {
|
|
8005
|
+
bucket.failed += 1;
|
|
8006
|
+
}
|
|
8007
|
+
buckets.set(key, bucket);
|
|
8008
|
+
}
|
|
8009
|
+
return [...buckets.values()].sort((left, right) => right.endedAt - left.endedAt);
|
|
8010
|
+
};
|
|
8011
|
+
var runVoiceSessionEvals = async (options = {}) => {
|
|
8012
|
+
const events = filterVoiceTraceEvents(options.events ?? await options.store?.list() ?? []);
|
|
8013
|
+
const grouped = new Map;
|
|
8014
|
+
for (const event of events) {
|
|
8015
|
+
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
8016
|
+
}
|
|
8017
|
+
const sessions = await Promise.all([...grouped.entries()].map(async ([sessionId, sessionEvents]) => {
|
|
8018
|
+
const sorted = filterVoiceTraceEvents(sessionEvents);
|
|
8019
|
+
const quality = await evaluateVoiceQuality({
|
|
8020
|
+
events: sorted,
|
|
8021
|
+
thresholds: options.thresholds
|
|
8022
|
+
});
|
|
8023
|
+
const { endedAt, startedAt } = sessionTime(sorted);
|
|
8024
|
+
const summary = summarizeVoiceTrace(sorted);
|
|
8025
|
+
const scenarioId = sorted.find((event) => event.scenarioId)?.scenarioId;
|
|
8026
|
+
return {
|
|
8027
|
+
endedAt,
|
|
8028
|
+
eventCount: sorted.length,
|
|
8029
|
+
quality,
|
|
8030
|
+
scenarioId,
|
|
8031
|
+
sessionId,
|
|
8032
|
+
startedAt,
|
|
8033
|
+
status: quality.status,
|
|
8034
|
+
summary
|
|
8035
|
+
};
|
|
8036
|
+
}));
|
|
8037
|
+
const limitedSessions = sessions.sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 100);
|
|
8038
|
+
const failed = limitedSessions.filter((session) => session.status === "fail").length;
|
|
8039
|
+
const passed = limitedSessions.length - failed;
|
|
8040
|
+
return {
|
|
8041
|
+
checkedAt: Date.now(),
|
|
8042
|
+
failed,
|
|
8043
|
+
passed,
|
|
8044
|
+
sessions: limitedSessions,
|
|
8045
|
+
status: failed > 0 ? "fail" : "pass",
|
|
8046
|
+
total: limitedSessions.length,
|
|
8047
|
+
trend: buildTrend(limitedSessions)
|
|
8048
|
+
};
|
|
8049
|
+
};
|
|
8050
|
+
var summarizeEvalBaseline = (report) => {
|
|
8051
|
+
const failedSessionIds = report.sessions.filter((session) => session.status === "fail").map((session) => session.sessionId).sort();
|
|
8052
|
+
return {
|
|
8053
|
+
failed: report.failed,
|
|
8054
|
+
failedSessionIds,
|
|
8055
|
+
passRate: rate2(report.passed, report.total),
|
|
8056
|
+
passed: report.passed,
|
|
8057
|
+
total: report.total
|
|
8058
|
+
};
|
|
8059
|
+
};
|
|
8060
|
+
var compareVoiceEvalBaseline = (currentReport, baselineReport, options = {}) => {
|
|
8061
|
+
const baseline = summarizeEvalBaseline(baselineReport);
|
|
8062
|
+
const current = summarizeEvalBaseline(currentReport);
|
|
8063
|
+
const maxFailedDelta = options.maxFailedDelta ?? 0;
|
|
8064
|
+
const maxPassRateDrop = options.maxPassRateDrop ?? 0;
|
|
8065
|
+
const failOnNewFailedSessions = options.failOnNewFailedSessions ?? true;
|
|
8066
|
+
const baselineFailed = new Set(baseline.failedSessionIds);
|
|
8067
|
+
const currentFailed = new Set(current.failedSessionIds);
|
|
8068
|
+
const newFailedSessionIds = current.failedSessionIds.filter((sessionId) => !baselineFailed.has(sessionId));
|
|
8069
|
+
const recoveredSessionIds = baseline.failedSessionIds.filter((sessionId) => !currentFailed.has(sessionId));
|
|
8070
|
+
const deltas = {
|
|
8071
|
+
failed: current.failed - baseline.failed,
|
|
8072
|
+
passRate: current.passRate - baseline.passRate,
|
|
8073
|
+
passed: current.passed - baseline.passed,
|
|
8074
|
+
total: current.total - baseline.total
|
|
8075
|
+
};
|
|
8076
|
+
const reasons = [];
|
|
8077
|
+
if (deltas.failed > maxFailedDelta) {
|
|
8078
|
+
reasons.push(`Failed sessions increased by ${deltas.failed}, above allowed delta ${maxFailedDelta}.`);
|
|
8079
|
+
}
|
|
8080
|
+
if (deltas.passRate < -maxPassRateDrop) {
|
|
8081
|
+
reasons.push(`Pass rate dropped by ${Math.abs(deltas.passRate).toFixed(4)}, above allowed drop ${maxPassRateDrop}.`);
|
|
8082
|
+
}
|
|
8083
|
+
if (failOnNewFailedSessions && newFailedSessionIds.length > 0) {
|
|
8084
|
+
reasons.push(`${newFailedSessionIds.length} session(s) failed that were not failing in the baseline.`);
|
|
8085
|
+
}
|
|
8086
|
+
return {
|
|
8087
|
+
baseline,
|
|
8088
|
+
checkedAt: Date.now(),
|
|
8089
|
+
current,
|
|
8090
|
+
deltas,
|
|
8091
|
+
newFailedSessionIds,
|
|
8092
|
+
recoveredSessionIds,
|
|
8093
|
+
reasons,
|
|
8094
|
+
status: reasons.length > 0 ? "fail" : "pass"
|
|
8095
|
+
};
|
|
8096
|
+
};
|
|
8097
|
+
var createVoiceFileEvalBaselineStore = (filePath) => ({
|
|
8098
|
+
get: async () => {
|
|
8099
|
+
const file = Bun.file(filePath);
|
|
8100
|
+
if (!await file.exists()) {
|
|
8101
|
+
return;
|
|
8102
|
+
}
|
|
8103
|
+
const text = await file.text();
|
|
8104
|
+
return text.trim() ? JSON.parse(text) : undefined;
|
|
8105
|
+
},
|
|
8106
|
+
set: async (report) => {
|
|
8107
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
8108
|
+
await Bun.write(filePath, JSON.stringify(report, null, 2));
|
|
8109
|
+
}
|
|
8110
|
+
});
|
|
8111
|
+
var formatTime = (value) => value === undefined ? "unknown" : new Date(value).toLocaleString();
|
|
8112
|
+
var formatPercent = (value) => `${(value * 100).toFixed(2)}%`;
|
|
8113
|
+
var renderVoiceEvalHTML = (report, options = {}) => {
|
|
8114
|
+
const title = options.title ?? "AbsoluteJS Voice Evals";
|
|
8115
|
+
const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
|
|
8116
|
+
const trend = report.trend.length ? report.trend.map((bucket) => `<tr><td>${escapeHtml9(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>';
|
|
8117
|
+
const sessions = report.sessions.length ? report.sessions.map((session) => {
|
|
8118
|
+
const failedMetrics = Object.entries(session.quality.metrics).filter(([, metric]) => !metric.pass).map(([, metric]) => metric.label).join(", ");
|
|
8119
|
+
return `<tr class="${session.status}"><td>${escapeHtml9(session.sessionId)}</td><td>${escapeHtml9(session.status)}</td><td>${session.eventCount}</td><td>${session.summary.turnCount}</td><td>${session.summary.errorCount}</td><td>${escapeHtml9(formatTime(session.endedAt))}</td><td>${escapeHtml9(failedMetrics || "none")}</td></tr>`;
|
|
8120
|
+
}).join("") : '<tr><td colspan="7">No sessions found.</td></tr>';
|
|
8121
|
+
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{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}.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{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.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>${escapeHtml9(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><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>`;
|
|
8122
|
+
};
|
|
8123
|
+
var renderVoiceEvalBaselineHTML = (comparison, options = {}) => {
|
|
8124
|
+
const title = options.title ?? "AbsoluteJS Voice Eval Baseline";
|
|
8125
|
+
const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
|
|
8126
|
+
const reasons = comparison.reasons.length ? comparison.reasons.map((reason) => `<li>${escapeHtml9(reason)}</li>`).join("") : "<li>No baseline regressions detected.</li>";
|
|
8127
|
+
const newFailures = comparison.newFailedSessionIds.length ? comparison.newFailedSessionIds.map((id) => `<li>${escapeHtml9(id)}</li>`).join("") : "<li>none</li>";
|
|
8128
|
+
const recovered = comparison.recoveredSessionIds.length ? comparison.recoveredSessionIds.map((id) => `<li>${escapeHtml9(id)}</li>`).join("") : "<li>none</li>";
|
|
8129
|
+
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{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1000px;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}.status{border-radius:999px;display:inline-flex;font-weight:800;padding:.35rem .75rem}.pass{background:#dcfce7;color:#166534}.fail{background:#fee2e2;color:#991b1b}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:1rem 0}.card{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.card strong{display:block;font-size:2rem}section{background:white;border:1px solid #e7e5e4;border-radius:1rem;margin:1rem 0;padding:1rem}</style></head><body><main>${links}<h1>${escapeHtml9(title)}</h1><p class="status ${comparison.status}">${comparison.status}</p><div class="grid"><article class="card"><span>Baseline pass rate</span><strong>${escapeHtml9(formatPercent(comparison.baseline.passRate))}</strong></article><article class="card"><span>Current pass rate</span><strong>${escapeHtml9(formatPercent(comparison.current.passRate))}</strong></article><article class="card"><span>Failed delta</span><strong>${comparison.deltas.failed}</strong></article><article class="card"><span>Pass rate delta</span><strong>${escapeHtml9(formatPercent(comparison.deltas.passRate))}</strong></article></div><section><h2>Regression Reasons</h2><ul>${reasons}</ul></section><section><h2>New Failed Sessions</h2><ul>${newFailures}</ul></section><section><h2>Recovered Sessions</h2><ul>${recovered}</ul></section></main></body></html>`;
|
|
8130
|
+
};
|
|
8131
|
+
var createVoiceEvalRoutes = (options) => {
|
|
8132
|
+
const path = options.path ?? "/evals";
|
|
8133
|
+
const routes = new Elysia7({
|
|
8134
|
+
name: options.name ?? "absolutejs-voice-evals"
|
|
8135
|
+
});
|
|
8136
|
+
const getReport = () => runVoiceSessionEvals({
|
|
8137
|
+
events: options.events,
|
|
8138
|
+
limit: options.limit,
|
|
8139
|
+
store: options.store,
|
|
8140
|
+
thresholds: options.thresholds
|
|
8141
|
+
});
|
|
8142
|
+
const getBaseline = async () => typeof options.baseline === "function" ? options.baseline() : options.baseline ?? await options.baselineStore?.get();
|
|
8143
|
+
const getBaselineComparison = async () => {
|
|
8144
|
+
const [current, baseline] = await Promise.all([getReport(), getBaseline()]);
|
|
8145
|
+
return baseline ? compareVoiceEvalBaseline(current, baseline, options.baselineComparison) : undefined;
|
|
8146
|
+
};
|
|
8147
|
+
routes.get(path, async () => {
|
|
8148
|
+
const report = await getReport();
|
|
8149
|
+
return new Response(renderVoiceEvalHTML(report, {
|
|
8150
|
+
links: options.links,
|
|
8151
|
+
title: options.title
|
|
8152
|
+
}), {
|
|
8153
|
+
headers: {
|
|
8154
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
8155
|
+
...options.headers
|
|
8156
|
+
}
|
|
8157
|
+
});
|
|
8158
|
+
});
|
|
8159
|
+
routes.get(`${path}/json`, async () => getReport());
|
|
8160
|
+
routes.get(`${path}/status`, async ({ set }) => {
|
|
8161
|
+
const report = await getReport();
|
|
8162
|
+
if (report.status === "fail") {
|
|
8163
|
+
set.status = 503;
|
|
8164
|
+
}
|
|
8165
|
+
return report;
|
|
8166
|
+
});
|
|
8167
|
+
routes.get(`${path}/baseline`, async ({ set }) => {
|
|
8168
|
+
const comparison = await getBaselineComparison();
|
|
8169
|
+
if (!comparison) {
|
|
8170
|
+
set.status = 404;
|
|
8171
|
+
return Response.json({ error: "No voice eval baseline found." });
|
|
8172
|
+
}
|
|
8173
|
+
return new Response(renderVoiceEvalBaselineHTML(comparison, {
|
|
8174
|
+
links: options.links,
|
|
8175
|
+
title: `${options.title ?? "AbsoluteJS Voice Evals"} Baseline`
|
|
8176
|
+
}), {
|
|
8177
|
+
headers: {
|
|
8178
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
8179
|
+
...options.headers
|
|
8180
|
+
}
|
|
8181
|
+
});
|
|
8182
|
+
});
|
|
8183
|
+
routes.get(`${path}/baseline/json`, async ({ set }) => {
|
|
8184
|
+
const comparison = await getBaselineComparison();
|
|
8185
|
+
if (!comparison) {
|
|
8186
|
+
set.status = 404;
|
|
8187
|
+
return { error: "No voice eval baseline found." };
|
|
8188
|
+
}
|
|
8189
|
+
return comparison;
|
|
8190
|
+
});
|
|
8191
|
+
routes.get(`${path}/baseline/status`, async ({ set }) => {
|
|
8192
|
+
const comparison = await getBaselineComparison();
|
|
8193
|
+
if (!comparison) {
|
|
8194
|
+
set.status = 404;
|
|
8195
|
+
return { error: "No voice eval baseline found." };
|
|
8196
|
+
}
|
|
8197
|
+
if (comparison.status === "fail") {
|
|
8198
|
+
set.status = 503;
|
|
8199
|
+
}
|
|
8200
|
+
return comparison;
|
|
8201
|
+
});
|
|
8202
|
+
routes.post(`${path}/baseline`, async ({ set }) => {
|
|
8203
|
+
if (!options.baselineStore) {
|
|
8204
|
+
set.status = 501;
|
|
8205
|
+
return { error: "No voice eval baseline store configured." };
|
|
8206
|
+
}
|
|
8207
|
+
const report = await getReport();
|
|
8208
|
+
await options.baselineStore.set(report);
|
|
8209
|
+
return {
|
|
8210
|
+
baseline: report,
|
|
8211
|
+
status: "saved"
|
|
8212
|
+
};
|
|
8213
|
+
});
|
|
8214
|
+
return routes;
|
|
8215
|
+
};
|
|
8216
|
+
// src/sessionReplay.ts
|
|
8217
|
+
import { Elysia as Elysia8 } from "elysia";
|
|
8218
|
+
var getString6 = (value) => typeof value === "string" ? value : undefined;
|
|
8219
|
+
var escapeHtml10 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
8220
|
+
var increment3 = (record, key) => {
|
|
8221
|
+
record[key] = (record[key] ?? 0) + 1;
|
|
8222
|
+
};
|
|
8223
|
+
var buildReplayTurns = (events) => {
|
|
8224
|
+
const turns = new Map;
|
|
8225
|
+
const getTurn = (turnId) => {
|
|
8226
|
+
const existing = turns.get(turnId);
|
|
8227
|
+
if (existing) {
|
|
8228
|
+
return existing;
|
|
8229
|
+
}
|
|
8230
|
+
const turn = {
|
|
8231
|
+
assistantReplies: [],
|
|
8232
|
+
errors: [],
|
|
8233
|
+
id: turnId,
|
|
8234
|
+
modelCalls: [],
|
|
8235
|
+
tools: [],
|
|
8236
|
+
transcripts: []
|
|
8237
|
+
};
|
|
8238
|
+
turns.set(turnId, turn);
|
|
8239
|
+
return turn;
|
|
8240
|
+
};
|
|
8241
|
+
for (const event of events) {
|
|
8242
|
+
const turnId = event.turnId ?? "session";
|
|
8243
|
+
const turn = getTurn(turnId);
|
|
8244
|
+
switch (event.type) {
|
|
8245
|
+
case "turn.transcript":
|
|
8246
|
+
turn.transcripts.push({
|
|
8247
|
+
isFinal: event.payload.isFinal === true,
|
|
8248
|
+
text: getString6(event.payload.text)
|
|
8249
|
+
});
|
|
8250
|
+
break;
|
|
8251
|
+
case "turn.committed":
|
|
8252
|
+
turn.committedText = getString6(event.payload.text);
|
|
8253
|
+
break;
|
|
8254
|
+
case "turn.assistant": {
|
|
8255
|
+
const text = getString6(event.payload.text);
|
|
8256
|
+
if (text) {
|
|
8257
|
+
turn.assistantReplies.push(text);
|
|
8258
|
+
}
|
|
8259
|
+
break;
|
|
8260
|
+
}
|
|
8261
|
+
case "agent.model":
|
|
8262
|
+
case "assistant.run":
|
|
8263
|
+
turn.modelCalls.push(event.payload);
|
|
8264
|
+
break;
|
|
8265
|
+
case "agent.tool":
|
|
8266
|
+
turn.tools.push(event.payload);
|
|
8267
|
+
break;
|
|
8268
|
+
case "session.error":
|
|
8269
|
+
turn.errors.push(event.payload);
|
|
8270
|
+
break;
|
|
8271
|
+
}
|
|
8272
|
+
}
|
|
8273
|
+
return [...turns.values()];
|
|
8274
|
+
};
|
|
8275
|
+
var summarizeVoiceSessionReplay = async (options) => {
|
|
8276
|
+
const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
|
|
8277
|
+
const events = filterVoiceTraceEvents(sourceEvents, {
|
|
8278
|
+
sessionId: options.sessionId
|
|
8279
|
+
});
|
|
8280
|
+
const replay = buildVoiceTraceReplay(events, {
|
|
8281
|
+
evaluation: options.evaluation,
|
|
8282
|
+
redact: options.redact,
|
|
8283
|
+
title: options.title ?? `Voice Session ${options.sessionId}`
|
|
8284
|
+
});
|
|
8285
|
+
const startedAt = replay.summary.startedAt;
|
|
8286
|
+
return {
|
|
8287
|
+
evaluation: replay.evaluation,
|
|
8288
|
+
events,
|
|
8289
|
+
html: replay.html,
|
|
8290
|
+
markdown: replay.markdown,
|
|
8291
|
+
sessionId: options.sessionId,
|
|
8292
|
+
summary: replay.summary,
|
|
8293
|
+
timeline: events.map((event) => ({
|
|
8294
|
+
at: event.at,
|
|
8295
|
+
offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
|
|
8296
|
+
payload: event.payload,
|
|
8297
|
+
turnId: event.turnId,
|
|
8298
|
+
type: event.type
|
|
8299
|
+
})),
|
|
8300
|
+
turns: buildReplayTurns(events)
|
|
8301
|
+
};
|
|
8302
|
+
};
|
|
8303
|
+
var summarizeVoiceSessions = async (options = {}) => {
|
|
8304
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
8305
|
+
const grouped = new Map;
|
|
8306
|
+
for (const event of events) {
|
|
8307
|
+
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
8308
|
+
}
|
|
8309
|
+
const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
|
|
8310
|
+
const sorted = filterVoiceTraceEvents(sessionEvents);
|
|
8311
|
+
const summary = buildVoiceTraceReplay(sorted, {
|
|
8312
|
+
evaluation: {
|
|
8313
|
+
requireAssistantReply: false,
|
|
8314
|
+
requireCompletedCall: false,
|
|
8315
|
+
requireTranscript: false,
|
|
8316
|
+
requireTurn: false
|
|
8317
|
+
}
|
|
8318
|
+
}).summary;
|
|
8319
|
+
const providerErrors = {};
|
|
8320
|
+
const providers = new Set;
|
|
8321
|
+
let latestOutcome;
|
|
8322
|
+
let errorCount = 0;
|
|
8323
|
+
for (const event of sorted) {
|
|
8324
|
+
const provider = getString6(event.payload.provider);
|
|
8325
|
+
if (provider) {
|
|
8326
|
+
providers.add(provider);
|
|
8327
|
+
}
|
|
8328
|
+
if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
|
|
8329
|
+
errorCount += 1;
|
|
8330
|
+
increment3(providerErrors, provider ?? "unknown");
|
|
8331
|
+
}
|
|
8332
|
+
const outcome = getString6(event.payload.outcome);
|
|
8333
|
+
if (outcome) {
|
|
8334
|
+
latestOutcome = outcome;
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
8337
|
+
const item = {
|
|
8338
|
+
endedAt: summary.endedAt,
|
|
8339
|
+
errorCount,
|
|
8340
|
+
eventCount: summary.eventCount,
|
|
8341
|
+
latestOutcome,
|
|
8342
|
+
providerErrors,
|
|
8343
|
+
providers: [...providers].sort(),
|
|
8344
|
+
sessionId,
|
|
8345
|
+
startedAt: summary.startedAt,
|
|
8346
|
+
status: errorCount > 0 ? "failed" : "healthy",
|
|
8347
|
+
transcriptCount: summary.transcriptCount,
|
|
8348
|
+
turnCount: summary.turnCount
|
|
8349
|
+
};
|
|
8350
|
+
const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
|
|
8351
|
+
return {
|
|
8352
|
+
...item,
|
|
8353
|
+
replayHref
|
|
8354
|
+
};
|
|
8355
|
+
});
|
|
8356
|
+
const search = options.q?.trim().toLowerCase();
|
|
8357
|
+
return sessions.filter((session) => {
|
|
8358
|
+
if (options.status && options.status !== "all" && session.status !== options.status) {
|
|
8359
|
+
return false;
|
|
8360
|
+
}
|
|
8361
|
+
if (options.provider && !session.providers.includes(options.provider)) {
|
|
8362
|
+
return false;
|
|
8363
|
+
}
|
|
8364
|
+
if (!search) {
|
|
8365
|
+
return true;
|
|
8366
|
+
}
|
|
8367
|
+
return [
|
|
8368
|
+
session.sessionId,
|
|
8369
|
+
session.latestOutcome,
|
|
8370
|
+
session.status,
|
|
8371
|
+
...session.providers
|
|
8372
|
+
].some((value) => value?.toLowerCase().includes(search));
|
|
8373
|
+
}).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
|
|
8374
|
+
};
|
|
8375
|
+
var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
|
|
8376
|
+
'<div class="voice-sessions-list">',
|
|
8377
|
+
...sessions.map((session) => [
|
|
8378
|
+
`<article class="voice-session-card ${escapeHtml10(session.status)}">`,
|
|
8379
|
+
'<div class="voice-session-card-header">',
|
|
8380
|
+
`<strong>${escapeHtml10(session.sessionId)}</strong>`,
|
|
8381
|
+
`<span>${escapeHtml10(session.status)}</span>`,
|
|
8382
|
+
"</div>",
|
|
8383
|
+
"<dl>",
|
|
8384
|
+
`<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
|
|
8385
|
+
`<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
|
|
8386
|
+
`<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
|
|
8387
|
+
`<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
|
|
8388
|
+
"</dl>",
|
|
8389
|
+
session.latestOutcome ? `<p>Outcome: ${escapeHtml10(session.latestOutcome)}</p>` : "",
|
|
8390
|
+
session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml10).join(", ")}</p>` : "",
|
|
8391
|
+
session.replayHref ? `<p><a href="${escapeHtml10(session.replayHref)}">Open replay</a></p>` : "",
|
|
8392
|
+
"</article>"
|
|
8393
|
+
].join("")),
|
|
8394
|
+
"</div>"
|
|
8395
|
+
].join("");
|
|
8396
|
+
var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
|
|
8397
|
+
...options,
|
|
8398
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
8399
|
+
provider: query?.provider ?? options.provider,
|
|
8400
|
+
q: query?.q ?? options.q,
|
|
8401
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
8402
|
+
});
|
|
8403
|
+
var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
|
|
8404
|
+
const sessions = await summarizeVoiceSessions({
|
|
8405
|
+
...options,
|
|
8406
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
8407
|
+
provider: query?.provider ?? options.provider,
|
|
8408
|
+
q: query?.q ?? options.q,
|
|
8409
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
8410
|
+
});
|
|
8411
|
+
const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
|
|
8412
|
+
return new Response(body, {
|
|
8413
|
+
headers: {
|
|
8414
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
8415
|
+
...options.headers
|
|
8416
|
+
}
|
|
8417
|
+
});
|
|
8418
|
+
};
|
|
8419
|
+
var createVoiceSessionListRoutes = (options = {}) => {
|
|
8420
|
+
const path = options.path ?? "/api/voice-sessions";
|
|
8421
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
8422
|
+
const routes = new Elysia8({
|
|
8423
|
+
name: options.name ?? "absolutejs-voice-session-list"
|
|
8424
|
+
}).get(path, createVoiceSessionsJSONHandler(options));
|
|
8425
|
+
if (htmlPath) {
|
|
8426
|
+
routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
|
|
8427
|
+
}
|
|
8428
|
+
return routes;
|
|
8429
|
+
};
|
|
8430
|
+
var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
|
|
8431
|
+
...options,
|
|
8432
|
+
sessionId: params.sessionId ?? ""
|
|
8433
|
+
});
|
|
8434
|
+
var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
|
|
8435
|
+
const replay = await summarizeVoiceSessionReplay({
|
|
8436
|
+
...options,
|
|
8437
|
+
sessionId: params.sessionId ?? ""
|
|
8438
|
+
});
|
|
8439
|
+
const body = await (options.render?.(replay) ?? replay.html);
|
|
8440
|
+
return new Response(body, {
|
|
8441
|
+
headers: {
|
|
8442
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
8443
|
+
...options.headers
|
|
8444
|
+
}
|
|
8445
|
+
});
|
|
8446
|
+
};
|
|
8447
|
+
var createVoiceSessionReplayRoutes = (options) => {
|
|
8448
|
+
const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
|
|
8449
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
8450
|
+
const routes = new Elysia8({
|
|
8451
|
+
name: options.name ?? "absolutejs-voice-session-replay"
|
|
8452
|
+
}).get(path, createVoiceSessionReplayJSONHandler(options));
|
|
8453
|
+
if (htmlPath) {
|
|
8454
|
+
routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
|
|
8455
|
+
}
|
|
8456
|
+
return routes;
|
|
8457
|
+
};
|
|
8458
|
+
// src/fileStore.ts
|
|
8459
|
+
import { mkdir as mkdir2, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
8460
|
+
import { join } from "path";
|
|
8461
|
+
var listJsonFiles = async (directory) => {
|
|
8462
|
+
try {
|
|
8463
|
+
const entries = await readdir(directory, {
|
|
8464
|
+
withFileTypes: true
|
|
8465
|
+
});
|
|
8466
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => join(directory, entry.name));
|
|
8467
|
+
} catch (error) {
|
|
8468
|
+
if (error.code === "ENOENT") {
|
|
8469
|
+
return [];
|
|
8470
|
+
}
|
|
8471
|
+
throw error;
|
|
8472
|
+
}
|
|
8473
|
+
};
|
|
8474
|
+
var encodeStoreId = (id) => `${encodeURIComponent(id)}.json`;
|
|
8475
|
+
var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
|
|
8476
|
+
var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
|
|
8477
|
+
var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
8478
|
+
var writeJsonFile = async (path, value, options) => {
|
|
8479
|
+
await mkdir2(options.directory, {
|
|
8480
|
+
recursive: true
|
|
8481
|
+
});
|
|
8482
|
+
const tempPath = `${path}.${crypto.randomUUID()}.tmp`;
|
|
8483
|
+
await writeFile(tempPath, JSON.stringify(value, null, options.pretty === false ? undefined : 2));
|
|
8484
|
+
await rename(tempPath, path);
|
|
8485
|
+
};
|
|
8486
|
+
var createVoiceFileSessionStore = (options) => {
|
|
8487
|
+
const get = async (id) => {
|
|
8488
|
+
const path = resolveFilePath(options.directory, id);
|
|
8489
|
+
try {
|
|
8490
|
+
return await readJsonFile(path);
|
|
8491
|
+
} catch (error) {
|
|
8492
|
+
if (error.code === "ENOENT") {
|
|
8493
|
+
return;
|
|
8494
|
+
}
|
|
8495
|
+
throw error;
|
|
8496
|
+
}
|
|
8497
|
+
};
|
|
8498
|
+
const getOrCreate = async (id) => {
|
|
8499
|
+
const existing = await get(id);
|
|
8500
|
+
if (existing) {
|
|
8501
|
+
return existing;
|
|
8502
|
+
}
|
|
8503
|
+
const session = createVoiceSessionRecord(id);
|
|
8504
|
+
await writeJsonFile(resolveFilePath(options.directory, id), session, options);
|
|
8505
|
+
return session;
|
|
8506
|
+
};
|
|
8507
|
+
const set = async (id, value) => {
|
|
8508
|
+
await writeJsonFile(resolveFilePath(options.directory, id), value, options);
|
|
8509
|
+
};
|
|
8510
|
+
const list = async () => {
|
|
8511
|
+
const files = await listJsonFiles(options.directory);
|
|
8512
|
+
const sessions = await Promise.all(files.map((file) => readJsonFile(file)));
|
|
8513
|
+
return sessions.map((session) => toVoiceSessionSummary(session)).sort((first, second) => (second.lastActivityAt ?? second.createdAt) - (first.lastActivityAt ?? first.createdAt));
|
|
8514
|
+
};
|
|
8515
|
+
const remove = async (id) => {
|
|
8516
|
+
await rm(resolveFilePath(options.directory, id), {
|
|
8517
|
+
force: true
|
|
8518
|
+
});
|
|
8519
|
+
};
|
|
8520
|
+
return { get, getOrCreate, list, remove, set };
|
|
8521
|
+
};
|
|
8522
|
+
var createVoiceFileReviewStore = (options) => {
|
|
8523
|
+
const get = async (id) => {
|
|
8524
|
+
const path = resolveFilePath(options.directory, id);
|
|
8525
|
+
try {
|
|
8526
|
+
return await readJsonFile(path);
|
|
8527
|
+
} catch (error) {
|
|
8528
|
+
if (error.code === "ENOENT") {
|
|
8529
|
+
return;
|
|
8530
|
+
}
|
|
8531
|
+
throw error;
|
|
8532
|
+
}
|
|
8533
|
+
};
|
|
8534
|
+
const list = async () => {
|
|
8535
|
+
const files = await listJsonFiles(options.directory);
|
|
8536
|
+
const reviews = await Promise.all(files.map((file) => readJsonFile(file)));
|
|
8537
|
+
return reviews.sort((left, right) => (right.generatedAt ?? 0) - (left.generatedAt ?? 0));
|
|
8538
|
+
};
|
|
8539
|
+
const set = async (id, artifact) => {
|
|
8540
|
+
await writeJsonFile(resolveFilePath(options.directory, id), withVoiceCallReviewId(id, artifact), options);
|
|
8541
|
+
};
|
|
8542
|
+
const remove = async (id) => {
|
|
8543
|
+
await rm(resolveFilePath(options.directory, id), {
|
|
8544
|
+
force: true
|
|
8545
|
+
});
|
|
8546
|
+
};
|
|
8547
|
+
return { get, list, remove, set };
|
|
8548
|
+
};
|
|
8549
|
+
var createVoiceFileTaskStore = (options) => {
|
|
8550
|
+
const get = async (id) => {
|
|
8551
|
+
const path = resolveFilePath(options.directory, id);
|
|
8552
|
+
try {
|
|
7963
8553
|
return await readJsonFile(path);
|
|
7964
8554
|
} catch (error) {
|
|
7965
8555
|
if (error.code === "ENOENT") {
|
|
@@ -8778,571 +9368,227 @@ var createOpenAIVoiceAssistantModel = (options) => {
|
|
|
8778
9368
|
name: tool.name,
|
|
8779
9369
|
parameters: tool.parameters ?? {
|
|
8780
9370
|
additionalProperties: true,
|
|
8781
|
-
type: "object"
|
|
8782
|
-
},
|
|
8783
|
-
strict: false,
|
|
8784
|
-
type: "function"
|
|
8785
|
-
}))
|
|
8786
|
-
}),
|
|
8787
|
-
headers: {
|
|
8788
|
-
authorization: `Bearer ${options.apiKey}`,
|
|
8789
|
-
"content-type": "application/json"
|
|
8790
|
-
},
|
|
8791
|
-
method: "POST"
|
|
8792
|
-
});
|
|
8793
|
-
if (!response.ok) {
|
|
8794
|
-
throw createHTTPError("OpenAI", response);
|
|
8795
|
-
}
|
|
8796
|
-
const body = await response.json();
|
|
8797
|
-
if (body.usage && typeof body.usage === "object") {
|
|
8798
|
-
await options.onUsage?.(body.usage);
|
|
8799
|
-
}
|
|
8800
|
-
const toolCalls = extractToolCalls(body);
|
|
8801
|
-
if (toolCalls.length) {
|
|
8802
|
-
return {
|
|
8803
|
-
toolCalls
|
|
8804
|
-
};
|
|
8805
|
-
}
|
|
8806
|
-
return normalizeRouteOutput(parseJSON(extractText(body)));
|
|
8807
|
-
}
|
|
8808
|
-
};
|
|
8809
|
-
};
|
|
8810
|
-
var extractAnthropicText = (response) => {
|
|
8811
|
-
const content = Array.isArray(response.content) ? response.content : [];
|
|
8812
|
-
return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
|
|
8813
|
-
`);
|
|
8814
|
-
};
|
|
8815
|
-
var extractAnthropicToolCalls = (response) => {
|
|
8816
|
-
const content = Array.isArray(response.content) ? response.content : [];
|
|
8817
|
-
const toolCalls = [];
|
|
8818
|
-
for (const item of content) {
|
|
8819
|
-
if (!item || typeof item !== "object") {
|
|
8820
|
-
continue;
|
|
8821
|
-
}
|
|
8822
|
-
const record = item;
|
|
8823
|
-
if (record.type !== "tool_use" || typeof record.name !== "string") {
|
|
8824
|
-
continue;
|
|
8825
|
-
}
|
|
8826
|
-
toolCalls.push({
|
|
8827
|
-
args: record.input && typeof record.input === "object" ? record.input : {},
|
|
8828
|
-
id: typeof record.id === "string" ? record.id : undefined,
|
|
8829
|
-
name: record.name
|
|
8830
|
-
});
|
|
8831
|
-
}
|
|
8832
|
-
return toolCalls;
|
|
8833
|
-
};
|
|
8834
|
-
var createAnthropicVoiceAssistantModel = (options) => {
|
|
8835
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8836
|
-
const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
|
|
8837
|
-
const model = options.model ?? "claude-sonnet-4-5";
|
|
8838
|
-
return {
|
|
8839
|
-
generate: async (input) => {
|
|
8840
|
-
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
|
|
8841
|
-
body: JSON.stringify({
|
|
8842
|
-
max_tokens: options.maxOutputTokens ?? 1024,
|
|
8843
|
-
messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
|
|
8844
|
-
model,
|
|
8845
|
-
system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
8846
|
-
|
|
8847
|
-
`),
|
|
8848
|
-
temperature: options.temperature,
|
|
8849
|
-
tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
|
|
8850
|
-
tools: input.tools.map((tool) => ({
|
|
8851
|
-
description: tool.description,
|
|
8852
|
-
input_schema: tool.parameters ?? {
|
|
8853
|
-
additionalProperties: true,
|
|
8854
|
-
type: "object"
|
|
8855
|
-
},
|
|
8856
|
-
name: tool.name
|
|
8857
|
-
}))
|
|
8858
|
-
}),
|
|
8859
|
-
headers: {
|
|
8860
|
-
"anthropic-version": options.version ?? "2023-06-01",
|
|
8861
|
-
"content-type": "application/json",
|
|
8862
|
-
"x-api-key": options.apiKey
|
|
8863
|
-
},
|
|
8864
|
-
method: "POST"
|
|
8865
|
-
});
|
|
8866
|
-
if (!response.ok) {
|
|
8867
|
-
throw createHTTPError("Anthropic", response);
|
|
8868
|
-
}
|
|
8869
|
-
const body = await response.json();
|
|
8870
|
-
if (body.usage && typeof body.usage === "object") {
|
|
8871
|
-
await options.onUsage?.(body.usage);
|
|
8872
|
-
}
|
|
8873
|
-
const toolCalls = extractAnthropicToolCalls(body);
|
|
8874
|
-
if (toolCalls.length) {
|
|
8875
|
-
return {
|
|
8876
|
-
assistantText: extractAnthropicText(body) || undefined,
|
|
8877
|
-
toolCalls
|
|
8878
|
-
};
|
|
8879
|
-
}
|
|
8880
|
-
return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
|
|
8881
|
-
}
|
|
8882
|
-
};
|
|
8883
|
-
};
|
|
8884
|
-
var extractGeminiCandidateParts = (response) => {
|
|
8885
|
-
const candidates = Array.isArray(response.candidates) ? response.candidates : [];
|
|
8886
|
-
const first = candidates[0];
|
|
8887
|
-
if (!first || typeof first !== "object") {
|
|
8888
|
-
return [];
|
|
8889
|
-
}
|
|
8890
|
-
const content = first.content;
|
|
8891
|
-
if (!content || typeof content !== "object") {
|
|
8892
|
-
return [];
|
|
8893
|
-
}
|
|
8894
|
-
const parts = content.parts;
|
|
8895
|
-
return Array.isArray(parts) ? parts : [];
|
|
8896
|
-
};
|
|
8897
|
-
var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
8898
|
-
`);
|
|
8899
|
-
var extractGeminiToolCalls = (response) => {
|
|
8900
|
-
const toolCalls = [];
|
|
8901
|
-
for (const part of extractGeminiCandidateParts(response)) {
|
|
8902
|
-
if (!part || typeof part !== "object") {
|
|
8903
|
-
continue;
|
|
8904
|
-
}
|
|
8905
|
-
const functionCall = part.functionCall;
|
|
8906
|
-
if (!functionCall || typeof functionCall !== "object") {
|
|
8907
|
-
continue;
|
|
8908
|
-
}
|
|
8909
|
-
const record = functionCall;
|
|
8910
|
-
if (typeof record.name !== "string") {
|
|
8911
|
-
continue;
|
|
8912
|
-
}
|
|
8913
|
-
toolCalls.push({
|
|
8914
|
-
args: record.args && typeof record.args === "object" ? record.args : {},
|
|
8915
|
-
id: typeof record.id === "string" ? record.id : undefined,
|
|
8916
|
-
name: record.name
|
|
8917
|
-
});
|
|
8918
|
-
}
|
|
8919
|
-
return toolCalls;
|
|
8920
|
-
};
|
|
8921
|
-
var createGeminiVoiceAssistantModel = (options) => {
|
|
8922
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
8923
|
-
const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
8924
|
-
const model = options.model ?? "gemini-2.5-flash";
|
|
8925
|
-
const maxRetries = Math.max(0, options.maxRetries ?? 2);
|
|
8926
|
-
return {
|
|
8927
|
-
generate: async (input) => {
|
|
8928
|
-
const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
|
|
8929
|
-
let response;
|
|
8930
|
-
for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
|
|
8931
|
-
response = await fetchImpl(endpoint, {
|
|
8932
|
-
body: JSON.stringify({
|
|
8933
|
-
contents: input.messages.map(messageToGeminiContent).filter(Boolean),
|
|
8934
|
-
generationConfig: {
|
|
8935
|
-
maxOutputTokens: options.maxOutputTokens,
|
|
8936
|
-
...input.tools.length ? {} : {
|
|
8937
|
-
responseMimeType: "application/json",
|
|
8938
|
-
responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
|
|
8939
|
-
},
|
|
8940
|
-
temperature: options.temperature
|
|
8941
|
-
},
|
|
8942
|
-
systemInstruction: {
|
|
8943
|
-
parts: [
|
|
8944
|
-
{
|
|
8945
|
-
text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
8946
|
-
|
|
8947
|
-
`)
|
|
8948
|
-
}
|
|
8949
|
-
]
|
|
8950
|
-
},
|
|
8951
|
-
tools: input.tools.length ? [
|
|
8952
|
-
{
|
|
8953
|
-
functionDeclarations: input.tools.map((tool) => ({
|
|
8954
|
-
description: tool.description,
|
|
8955
|
-
name: tool.name,
|
|
8956
|
-
parameters: toGeminiSchema(tool.parameters ?? {
|
|
8957
|
-
additionalProperties: true,
|
|
8958
|
-
type: "object"
|
|
8959
|
-
})
|
|
8960
|
-
}))
|
|
8961
|
-
}
|
|
8962
|
-
] : undefined
|
|
8963
|
-
}),
|
|
8964
|
-
headers: {
|
|
8965
|
-
"content-type": "application/json"
|
|
8966
|
-
},
|
|
8967
|
-
method: "POST"
|
|
8968
|
-
});
|
|
8969
|
-
if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
|
|
8970
|
-
break;
|
|
8971
|
-
}
|
|
8972
|
-
const retryAfter = Number(response.headers.get("retry-after"));
|
|
8973
|
-
await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
|
|
8974
|
-
}
|
|
8975
|
-
if (!response) {
|
|
8976
|
-
throw new Error("Gemini voice assistant model failed: no response");
|
|
8977
|
-
}
|
|
9371
|
+
type: "object"
|
|
9372
|
+
},
|
|
9373
|
+
strict: false,
|
|
9374
|
+
type: "function"
|
|
9375
|
+
}))
|
|
9376
|
+
}),
|
|
9377
|
+
headers: {
|
|
9378
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
9379
|
+
"content-type": "application/json"
|
|
9380
|
+
},
|
|
9381
|
+
method: "POST"
|
|
9382
|
+
});
|
|
8978
9383
|
if (!response.ok) {
|
|
8979
|
-
throw createHTTPError("
|
|
9384
|
+
throw createHTTPError("OpenAI", response);
|
|
8980
9385
|
}
|
|
8981
9386
|
const body = await response.json();
|
|
8982
|
-
if (body.
|
|
8983
|
-
await options.onUsage?.(body.
|
|
9387
|
+
if (body.usage && typeof body.usage === "object") {
|
|
9388
|
+
await options.onUsage?.(body.usage);
|
|
8984
9389
|
}
|
|
8985
|
-
const toolCalls =
|
|
9390
|
+
const toolCalls = extractToolCalls(body);
|
|
8986
9391
|
if (toolCalls.length) {
|
|
8987
9392
|
return {
|
|
8988
|
-
assistantText: extractGeminiText(body) || undefined,
|
|
8989
9393
|
toolCalls
|
|
8990
9394
|
};
|
|
8991
9395
|
}
|
|
8992
|
-
return normalizeRouteOutput(parseJSON(
|
|
9396
|
+
return normalizeRouteOutput(parseJSON(extractText(body)));
|
|
8993
9397
|
}
|
|
8994
9398
|
};
|
|
8995
9399
|
};
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
import { Elysia as Elysia6 } from "elysia";
|
|
9001
|
-
var escapeHtml8 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9002
|
-
var getString5 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9003
|
-
var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
|
|
9004
|
-
var increment3 = (record, key) => {
|
|
9005
|
-
record[key] = (record[key] ?? 0) + 1;
|
|
9006
|
-
};
|
|
9007
|
-
var normalizeDelivery = (adapterId, value) => {
|
|
9008
|
-
const record = value && typeof value === "object" ? value : {};
|
|
9009
|
-
return {
|
|
9010
|
-
adapterId: getString5(record.adapterId) ?? adapterId,
|
|
9011
|
-
adapterKind: getString5(record.adapterKind),
|
|
9012
|
-
deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
|
|
9013
|
-
deliveredTo: getString5(record.deliveredTo),
|
|
9014
|
-
error: getString5(record.error),
|
|
9015
|
-
status: isStatus(record.status) ? record.status : "failed"
|
|
9016
|
-
};
|
|
9017
|
-
};
|
|
9018
|
-
var normalizeDeliveries = (payload) => {
|
|
9019
|
-
const deliveries = payload.deliveries;
|
|
9020
|
-
if (!deliveries || typeof deliveries !== "object") {
|
|
9021
|
-
return [];
|
|
9022
|
-
}
|
|
9023
|
-
return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
|
|
9024
|
-
};
|
|
9025
|
-
var resolveReplayHref = (event, replayHref) => {
|
|
9026
|
-
if (replayHref === false) {
|
|
9027
|
-
return;
|
|
9028
|
-
}
|
|
9029
|
-
if (typeof replayHref === "function") {
|
|
9030
|
-
return replayHref(event);
|
|
9031
|
-
}
|
|
9032
|
-
return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
|
|
9400
|
+
var extractAnthropicText = (response) => {
|
|
9401
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
9402
|
+
return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
|
|
9403
|
+
`);
|
|
9033
9404
|
};
|
|
9034
|
-
var
|
|
9035
|
-
const
|
|
9036
|
-
const
|
|
9037
|
-
const
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
delivered: 0,
|
|
9041
|
-
failed: 0,
|
|
9042
|
-
skipped: 0
|
|
9043
|
-
};
|
|
9044
|
-
const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
|
|
9045
|
-
const status = isStatus(event.payload.status) ? event.payload.status : "failed";
|
|
9046
|
-
const deliveries = normalizeDeliveries(event.payload);
|
|
9047
|
-
const item = {
|
|
9048
|
-
action: getString5(event.payload.action),
|
|
9049
|
-
at: event.at,
|
|
9050
|
-
deliveries,
|
|
9051
|
-
reason: getString5(event.payload.reason),
|
|
9052
|
-
sessionId: event.sessionId,
|
|
9053
|
-
status,
|
|
9054
|
-
target: getString5(event.payload.target)
|
|
9055
|
-
};
|
|
9056
|
-
return {
|
|
9057
|
-
...item,
|
|
9058
|
-
replayHref: resolveReplayHref(item, options.replayHref)
|
|
9059
|
-
};
|
|
9060
|
-
}).filter((event) => {
|
|
9061
|
-
if (options.status && options.status !== "all" && event.status !== options.status) {
|
|
9062
|
-
return false;
|
|
9063
|
-
}
|
|
9064
|
-
if (!search) {
|
|
9065
|
-
return true;
|
|
9066
|
-
}
|
|
9067
|
-
return [
|
|
9068
|
-
event.action,
|
|
9069
|
-
event.reason,
|
|
9070
|
-
event.sessionId,
|
|
9071
|
-
event.status,
|
|
9072
|
-
event.target,
|
|
9073
|
-
...event.deliveries.flatMap((delivery) => [
|
|
9074
|
-
delivery.adapterId,
|
|
9075
|
-
delivery.adapterKind,
|
|
9076
|
-
delivery.deliveredTo,
|
|
9077
|
-
delivery.error,
|
|
9078
|
-
delivery.status
|
|
9079
|
-
])
|
|
9080
|
-
].some((value) => value?.toLowerCase().includes(search));
|
|
9081
|
-
}).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
|
|
9082
|
-
for (const event of events) {
|
|
9083
|
-
byStatus[event.status] += 1;
|
|
9084
|
-
if (event.action) {
|
|
9085
|
-
increment3(byAction, event.action);
|
|
9405
|
+
var extractAnthropicToolCalls = (response) => {
|
|
9406
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
9407
|
+
const toolCalls = [];
|
|
9408
|
+
for (const item of content) {
|
|
9409
|
+
if (!item || typeof item !== "object") {
|
|
9410
|
+
continue;
|
|
9086
9411
|
}
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
failed: 0,
|
|
9091
|
-
skipped: 0
|
|
9092
|
-
};
|
|
9093
|
-
byAdapter[delivery.adapterId][delivery.status] += 1;
|
|
9412
|
+
const record = item;
|
|
9413
|
+
if (record.type !== "tool_use" || typeof record.name !== "string") {
|
|
9414
|
+
continue;
|
|
9094
9415
|
}
|
|
9416
|
+
toolCalls.push({
|
|
9417
|
+
args: record.input && typeof record.input === "object" ? record.input : {},
|
|
9418
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
9419
|
+
name: record.name
|
|
9420
|
+
});
|
|
9095
9421
|
}
|
|
9096
|
-
return
|
|
9097
|
-
byAction,
|
|
9098
|
-
byAdapter,
|
|
9099
|
-
byStatus,
|
|
9100
|
-
events,
|
|
9101
|
-
failed: byStatus.failed,
|
|
9102
|
-
total: events.length
|
|
9103
|
-
};
|
|
9104
|
-
};
|
|
9105
|
-
var renderMetricGrid = (summary) => [
|
|
9106
|
-
'<section class="voice-handoff-health-grid">',
|
|
9107
|
-
`<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
|
|
9108
|
-
`<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
|
|
9109
|
-
`<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
|
|
9110
|
-
`<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
|
|
9111
|
-
"</section>"
|
|
9112
|
-
].join("");
|
|
9113
|
-
var renderActionSummary = (summary) => {
|
|
9114
|
-
const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
|
|
9115
|
-
const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
|
|
9116
|
-
return [
|
|
9117
|
-
'<section class="voice-handoff-health-columns">',
|
|
9118
|
-
"<article><h3>Actions</h3>",
|
|
9119
|
-
actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml8(action)}: ${String(count)}</li>`).join("")}</ul>`,
|
|
9120
|
-
"</article>",
|
|
9121
|
-
"<article><h3>Adapters</h3>",
|
|
9122
|
-
adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml8(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
|
|
9123
|
-
"</article>",
|
|
9124
|
-
"</section>"
|
|
9125
|
-
].join("");
|
|
9422
|
+
return toolCalls;
|
|
9126
9423
|
};
|
|
9127
|
-
var
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
headers: {
|
|
9175
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
9176
|
-
...options.headers
|
|
9424
|
+
var createAnthropicVoiceAssistantModel = (options) => {
|
|
9425
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
9426
|
+
const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
|
|
9427
|
+
const model = options.model ?? "claude-sonnet-4-5";
|
|
9428
|
+
return {
|
|
9429
|
+
generate: async (input) => {
|
|
9430
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
|
|
9431
|
+
body: JSON.stringify({
|
|
9432
|
+
max_tokens: options.maxOutputTokens ?? 1024,
|
|
9433
|
+
messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
|
|
9434
|
+
model,
|
|
9435
|
+
system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
9436
|
+
|
|
9437
|
+
`),
|
|
9438
|
+
temperature: options.temperature,
|
|
9439
|
+
tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
|
|
9440
|
+
tools: input.tools.map((tool) => ({
|
|
9441
|
+
description: tool.description,
|
|
9442
|
+
input_schema: tool.parameters ?? {
|
|
9443
|
+
additionalProperties: true,
|
|
9444
|
+
type: "object"
|
|
9445
|
+
},
|
|
9446
|
+
name: tool.name
|
|
9447
|
+
}))
|
|
9448
|
+
}),
|
|
9449
|
+
headers: {
|
|
9450
|
+
"anthropic-version": options.version ?? "2023-06-01",
|
|
9451
|
+
"content-type": "application/json",
|
|
9452
|
+
"x-api-key": options.apiKey
|
|
9453
|
+
},
|
|
9454
|
+
method: "POST"
|
|
9455
|
+
});
|
|
9456
|
+
if (!response.ok) {
|
|
9457
|
+
throw createHTTPError("Anthropic", response);
|
|
9458
|
+
}
|
|
9459
|
+
const body = await response.json();
|
|
9460
|
+
if (body.usage && typeof body.usage === "object") {
|
|
9461
|
+
await options.onUsage?.(body.usage);
|
|
9462
|
+
}
|
|
9463
|
+
const toolCalls = extractAnthropicToolCalls(body);
|
|
9464
|
+
if (toolCalls.length) {
|
|
9465
|
+
return {
|
|
9466
|
+
assistantText: extractAnthropicText(body) || undefined,
|
|
9467
|
+
toolCalls
|
|
9468
|
+
};
|
|
9469
|
+
}
|
|
9470
|
+
return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
|
|
9177
9471
|
}
|
|
9178
|
-
}
|
|
9179
|
-
};
|
|
9180
|
-
var createVoiceHandoffHealthRoutes = (options = {}) => {
|
|
9181
|
-
const path = options.path ?? "/api/voice-handoffs";
|
|
9182
|
-
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
9183
|
-
const routes = new Elysia6({
|
|
9184
|
-
name: options.name ?? "absolutejs-voice-handoff-health"
|
|
9185
|
-
}).get(path, createVoiceHandoffHealthJSONHandler(options));
|
|
9186
|
-
if (htmlPath) {
|
|
9187
|
-
routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
|
|
9188
|
-
}
|
|
9189
|
-
return routes;
|
|
9472
|
+
};
|
|
9190
9473
|
};
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9474
|
+
var extractGeminiCandidateParts = (response) => {
|
|
9475
|
+
const candidates = Array.isArray(response.candidates) ? response.candidates : [];
|
|
9476
|
+
const first = candidates[0];
|
|
9477
|
+
if (!first || typeof first !== "object") {
|
|
9478
|
+
return [];
|
|
9479
|
+
}
|
|
9480
|
+
const content = first.content;
|
|
9481
|
+
if (!content || typeof content !== "object") {
|
|
9482
|
+
return [];
|
|
9483
|
+
}
|
|
9484
|
+
const parts = content.parts;
|
|
9485
|
+
return Array.isArray(parts) ? parts : [];
|
|
9203
9486
|
};
|
|
9204
|
-
var
|
|
9205
|
-
|
|
9206
|
-
var
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
actual: roundMetric2(input.actual),
|
|
9211
|
-
pass: input.actual <= input.threshold
|
|
9212
|
-
});
|
|
9213
|
-
var evaluateVoiceQuality = async (input) => {
|
|
9214
|
-
const events = filterVoiceTraceEvents(input.events ?? await input.store?.list() ?? []);
|
|
9215
|
-
const thresholds = {
|
|
9216
|
-
...DEFAULT_THRESHOLDS,
|
|
9217
|
-
...input.thresholds
|
|
9218
|
-
};
|
|
9219
|
-
const committedTurns = events.filter((event) => event.type === "turn.committed");
|
|
9220
|
-
const assistantReplies = events.filter((event) => event.type === "turn.assistant");
|
|
9221
|
-
const sessionIdsWithAssistantReply = new Set(assistantReplies.map((event) => event.sessionId));
|
|
9222
|
-
const sessionsWithTurns = new Set(committedTurns.map((event) => event.sessionId));
|
|
9223
|
-
const emptyTurns = committedTurns.filter((event) => !getString6(event.payload.text)?.trim());
|
|
9224
|
-
const turnTextsBySession = new Map;
|
|
9225
|
-
let duplicateTurns = 0;
|
|
9226
|
-
for (const turn of committedTurns) {
|
|
9227
|
-
const normalized = getString6(turn.payload.text)?.trim().toLowerCase();
|
|
9228
|
-
if (!normalized) {
|
|
9487
|
+
var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
9488
|
+
`);
|
|
9489
|
+
var extractGeminiToolCalls = (response) => {
|
|
9490
|
+
const toolCalls = [];
|
|
9491
|
+
for (const part of extractGeminiCandidateParts(response)) {
|
|
9492
|
+
if (!part || typeof part !== "object") {
|
|
9229
9493
|
continue;
|
|
9230
9494
|
}
|
|
9231
|
-
const
|
|
9232
|
-
if (
|
|
9233
|
-
|
|
9495
|
+
const functionCall = part.functionCall;
|
|
9496
|
+
if (!functionCall || typeof functionCall !== "object") {
|
|
9497
|
+
continue;
|
|
9234
9498
|
}
|
|
9235
|
-
|
|
9236
|
-
|
|
9499
|
+
const record = functionCall;
|
|
9500
|
+
if (typeof record.name !== "string") {
|
|
9501
|
+
continue;
|
|
9502
|
+
}
|
|
9503
|
+
toolCalls.push({
|
|
9504
|
+
args: record.args && typeof record.args === "object" ? record.args : {},
|
|
9505
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
9506
|
+
name: record.name
|
|
9507
|
+
});
|
|
9237
9508
|
}
|
|
9238
|
-
|
|
9239
|
-
const providerEvents = events.filter((event) => event.type === "session.error" && typeof event.payload.provider === "string" && typeof event.payload.providerStatus === "string");
|
|
9240
|
-
const providerErrors = providerEvents.filter((event) => event.payload.providerStatus === "error");
|
|
9241
|
-
const providerFallbacks = providerEvents.filter((event) => event.payload.providerStatus === "fallback");
|
|
9242
|
-
const providerTimeouts = providerEvents.filter((event) => event.payload.timedOut === true);
|
|
9243
|
-
const providerLatencies = providerEvents.map((event) => getNumber3(event.payload.elapsedMs)).filter((value) => value !== undefined);
|
|
9244
|
-
const averageProviderLatencyMs = providerLatencies.length > 0 ? providerLatencies.reduce((sum, value) => sum + value, 0) / providerLatencies.length : 0;
|
|
9245
|
-
const handoffHealth = await summarizeVoiceHandoffHealth({ events });
|
|
9246
|
-
const metrics = {
|
|
9247
|
-
duplicateTurnRate: createMetric({
|
|
9248
|
-
actual: rate(duplicateTurns, committedTurns.length),
|
|
9249
|
-
label: "Duplicate turn rate",
|
|
9250
|
-
threshold: thresholds.maxDuplicateTurnRate,
|
|
9251
|
-
unit: "rate"
|
|
9252
|
-
}),
|
|
9253
|
-
emptyTurnRate: createMetric({
|
|
9254
|
-
actual: rate(emptyTurns.length, committedTurns.length),
|
|
9255
|
-
label: "Empty turn rate",
|
|
9256
|
-
threshold: thresholds.maxEmptyTurnRate,
|
|
9257
|
-
unit: "rate"
|
|
9258
|
-
}),
|
|
9259
|
-
handoffFailureRate: createMetric({
|
|
9260
|
-
actual: rate(handoffHealth.failed, handoffHealth.total),
|
|
9261
|
-
label: "Handoff failure rate",
|
|
9262
|
-
threshold: thresholds.maxHandoffFailureRate,
|
|
9263
|
-
unit: "rate"
|
|
9264
|
-
}),
|
|
9265
|
-
missingAssistantReplyRate: createMetric({
|
|
9266
|
-
actual: rate(missingAssistantReplySessions, sessionsWithTurns.size),
|
|
9267
|
-
label: "Missing assistant reply rate",
|
|
9268
|
-
threshold: thresholds.maxMissingAssistantReplyRate,
|
|
9269
|
-
unit: "rate"
|
|
9270
|
-
}),
|
|
9271
|
-
providerAverageLatencyMs: createMetric({
|
|
9272
|
-
actual: averageProviderLatencyMs,
|
|
9273
|
-
label: "Average provider latency",
|
|
9274
|
-
threshold: thresholds.maxProviderAverageLatencyMs,
|
|
9275
|
-
unit: "ms"
|
|
9276
|
-
}),
|
|
9277
|
-
providerErrorRate: createMetric({
|
|
9278
|
-
actual: rate(providerErrors.length, providerEvents.length),
|
|
9279
|
-
label: "Provider error rate",
|
|
9280
|
-
threshold: thresholds.maxProviderErrorRate,
|
|
9281
|
-
unit: "rate"
|
|
9282
|
-
}),
|
|
9283
|
-
providerFallbackRate: createMetric({
|
|
9284
|
-
actual: rate(providerFallbacks.length, providerEvents.length),
|
|
9285
|
-
label: "Provider fallback rate",
|
|
9286
|
-
threshold: thresholds.maxProviderFallbackRate,
|
|
9287
|
-
unit: "rate"
|
|
9288
|
-
}),
|
|
9289
|
-
providerTimeoutRate: createMetric({
|
|
9290
|
-
actual: rate(providerTimeouts.length, providerEvents.length),
|
|
9291
|
-
label: "Provider timeout rate",
|
|
9292
|
-
threshold: thresholds.maxProviderTimeoutRate,
|
|
9293
|
-
unit: "rate"
|
|
9294
|
-
})
|
|
9295
|
-
};
|
|
9296
|
-
const status = Object.values(metrics).every((metric) => metric.pass) ? "pass" : "fail";
|
|
9297
|
-
return {
|
|
9298
|
-
checkedAt: Date.now(),
|
|
9299
|
-
eventCount: events.length,
|
|
9300
|
-
metrics,
|
|
9301
|
-
status,
|
|
9302
|
-
thresholds
|
|
9303
|
-
};
|
|
9304
|
-
};
|
|
9305
|
-
var escapeHtml9 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9306
|
-
var formatMetricValue = (metric) => metric.unit === "rate" ? `${(metric.actual * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.actual)}ms` : String(metric.actual);
|
|
9307
|
-
var formatThreshold = (metric) => metric.unit === "rate" ? `${(metric.threshold * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.threshold)}ms` : String(metric.threshold);
|
|
9308
|
-
var renderVoiceQualityHTML = (report, options = {}) => {
|
|
9309
|
-
const rows = Object.entries(report.metrics).map(([key, metric]) => `<tr class="${metric.pass ? "pass" : "fail"}"><td>${escapeHtml9(metric.label)}</td><td>${escapeHtml9(formatMetricValue(metric))}</td><td>${escapeHtml9(formatThreshold(metric))}</td><td>${metric.pass ? "pass" : "fail"}</td><td><code>${escapeHtml9(key)}</code></td></tr>`).join("");
|
|
9310
|
-
const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
|
|
9311
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>AbsoluteJS Voice Quality</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}nav{display:flex;flex-wrap:wrap;gap:.5rem;margin:0 0 1.25rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;padding:.35rem .75rem;font-weight:800}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}table{border-collapse:collapse;width:100%;background:white;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}.pass td{border-left:4px solid #16a34a}.fail td{border-left:4px solid #dc2626}code{background:#f3f4f6;padding:.15rem .3rem;border-radius:.3rem}</style></head><body><main>${links}<h1>Voice quality gates</h1><p class="status ${report.status}">${report.status}</p><p>${report.eventCount} event(s) checked.</p><table><thead><tr><th>Metric</th><th>Actual</th><th>Threshold</th><th>Status</th><th>Key</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
|
|
9509
|
+
return toolCalls;
|
|
9312
9510
|
};
|
|
9313
|
-
var
|
|
9314
|
-
const
|
|
9315
|
-
const
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9511
|
+
var createGeminiVoiceAssistantModel = (options) => {
|
|
9512
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
9513
|
+
const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
9514
|
+
const model = options.model ?? "gemini-2.5-flash";
|
|
9515
|
+
const maxRetries = Math.max(0, options.maxRetries ?? 2);
|
|
9516
|
+
return {
|
|
9517
|
+
generate: async (input) => {
|
|
9518
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
|
|
9519
|
+
let response;
|
|
9520
|
+
for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
|
|
9521
|
+
response = await fetchImpl(endpoint, {
|
|
9522
|
+
body: JSON.stringify({
|
|
9523
|
+
contents: input.messages.map(messageToGeminiContent).filter(Boolean),
|
|
9524
|
+
generationConfig: {
|
|
9525
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
9526
|
+
...input.tools.length ? {} : {
|
|
9527
|
+
responseMimeType: "application/json",
|
|
9528
|
+
responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
|
|
9529
|
+
},
|
|
9530
|
+
temperature: options.temperature
|
|
9531
|
+
},
|
|
9532
|
+
systemInstruction: {
|
|
9533
|
+
parts: [
|
|
9534
|
+
{
|
|
9535
|
+
text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
9536
|
+
|
|
9537
|
+
`)
|
|
9538
|
+
}
|
|
9539
|
+
]
|
|
9540
|
+
},
|
|
9541
|
+
tools: input.tools.length ? [
|
|
9542
|
+
{
|
|
9543
|
+
functionDeclarations: input.tools.map((tool) => ({
|
|
9544
|
+
description: tool.description,
|
|
9545
|
+
name: tool.name,
|
|
9546
|
+
parameters: toGeminiSchema(tool.parameters ?? {
|
|
9547
|
+
additionalProperties: true,
|
|
9548
|
+
type: "object"
|
|
9549
|
+
})
|
|
9550
|
+
}))
|
|
9551
|
+
}
|
|
9552
|
+
] : undefined
|
|
9553
|
+
}),
|
|
9554
|
+
headers: {
|
|
9555
|
+
"content-type": "application/json"
|
|
9556
|
+
},
|
|
9557
|
+
method: "POST"
|
|
9558
|
+
});
|
|
9559
|
+
if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
|
|
9560
|
+
break;
|
|
9561
|
+
}
|
|
9562
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
9563
|
+
await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
|
|
9329
9564
|
}
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9565
|
+
if (!response) {
|
|
9566
|
+
throw new Error("Gemini voice assistant model failed: no response");
|
|
9567
|
+
}
|
|
9568
|
+
if (!response.ok) {
|
|
9569
|
+
throw createHTTPError("Gemini", response);
|
|
9570
|
+
}
|
|
9571
|
+
const body = await response.json();
|
|
9572
|
+
if (body.usageMetadata && typeof body.usageMetadata === "object") {
|
|
9573
|
+
await options.onUsage?.(body.usageMetadata);
|
|
9574
|
+
}
|
|
9575
|
+
const toolCalls = extractGeminiToolCalls(body);
|
|
9576
|
+
if (toolCalls.length) {
|
|
9577
|
+
return {
|
|
9578
|
+
assistantText: extractGeminiText(body) || undefined,
|
|
9579
|
+
toolCalls
|
|
9580
|
+
};
|
|
9581
|
+
}
|
|
9582
|
+
return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
|
|
9337
9583
|
}
|
|
9338
|
-
|
|
9339
|
-
});
|
|
9340
|
-
return routes;
|
|
9584
|
+
};
|
|
9341
9585
|
};
|
|
9586
|
+
// src/opsConsoleRoutes.ts
|
|
9587
|
+
import { Elysia as Elysia10 } from "elysia";
|
|
9342
9588
|
|
|
9343
9589
|
// src/resilienceRoutes.ts
|
|
9344
|
-
import { Elysia as
|
|
9345
|
-
var
|
|
9590
|
+
import { Elysia as Elysia9 } from "elysia";
|
|
9591
|
+
var escapeHtml11 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9346
9592
|
var getString7 = (value) => typeof value === "string" ? value : undefined;
|
|
9347
9593
|
var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
9348
9594
|
var getBoolean2 = (value) => value === true;
|
|
@@ -9405,13 +9651,13 @@ var summarizeRoutingEvents = (events) => {
|
|
|
9405
9651
|
};
|
|
9406
9652
|
var renderProviderCards = (title, providers) => {
|
|
9407
9653
|
if (providers.length === 0) {
|
|
9408
|
-
return `<p class="muted">No ${
|
|
9654
|
+
return `<p class="muted">No ${escapeHtml11(title)} provider health yet.</p>`;
|
|
9409
9655
|
}
|
|
9410
9656
|
return `<div class="provider-grid">${providers.map((provider) => `
|
|
9411
|
-
<article class="card provider ${
|
|
9657
|
+
<article class="card provider ${escapeHtml11(provider.status)}">
|
|
9412
9658
|
<div class="card-header">
|
|
9413
|
-
<strong>${
|
|
9414
|
-
<span>${
|
|
9659
|
+
<strong>${escapeHtml11(provider.provider)}</strong>
|
|
9660
|
+
<span>${escapeHtml11(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
|
|
9415
9661
|
</div>
|
|
9416
9662
|
<dl>
|
|
9417
9663
|
<div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
|
|
@@ -9420,7 +9666,7 @@ var renderProviderCards = (title, providers) => {
|
|
|
9420
9666
|
<div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
|
|
9421
9667
|
<div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
|
|
9422
9668
|
</dl>
|
|
9423
|
-
${provider.lastError ? `<p class="muted">${
|
|
9669
|
+
${provider.lastError ? `<p class="muted">${escapeHtml11(provider.lastError)}</p>` : ""}
|
|
9424
9670
|
</article>
|
|
9425
9671
|
`).join("")}</div>`;
|
|
9426
9672
|
};
|
|
@@ -9429,24 +9675,24 @@ var renderTimeline2 = (events) => {
|
|
|
9429
9675
|
return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
|
|
9430
9676
|
}
|
|
9431
9677
|
return `<div class="timeline">${events.slice(0, 40).map((event) => `
|
|
9432
|
-
<article class="card event ${
|
|
9678
|
+
<article class="card event ${escapeHtml11(event.status ?? "unknown")}">
|
|
9433
9679
|
<div class="card-header">
|
|
9434
|
-
<strong>${
|
|
9680
|
+
<strong>${escapeHtml11(event.kind.toUpperCase())} ${escapeHtml11(event.operation ?? "generate")}</strong>
|
|
9435
9681
|
<span>${new Date(event.at).toLocaleString()}</span>
|
|
9436
9682
|
</div>
|
|
9437
9683
|
<p>
|
|
9438
|
-
<span class="pill">${
|
|
9439
|
-
<span class="pill">provider: ${
|
|
9440
|
-
${event.fallbackProvider ? `<span class="pill">fallback: ${
|
|
9684
|
+
<span class="pill">${escapeHtml11(event.status ?? "unknown")}</span>
|
|
9685
|
+
<span class="pill">provider: ${escapeHtml11(event.provider ?? "unknown")}</span>
|
|
9686
|
+
${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml11(event.fallbackProvider)}</span>` : ""}
|
|
9441
9687
|
${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
|
|
9442
9688
|
</p>
|
|
9443
9689
|
<dl>
|
|
9444
9690
|
<div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
|
|
9445
9691
|
<div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
|
|
9446
9692
|
<div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
|
|
9447
|
-
<div><dt>Session</dt><dd>${
|
|
9693
|
+
<div><dt>Session</dt><dd>${escapeHtml11(event.sessionId)}</dd></div>
|
|
9448
9694
|
</dl>
|
|
9449
|
-
${event.error ? `<p class="muted">${
|
|
9695
|
+
${event.error ? `<p class="muted">${escapeHtml11(event.error)}</p>` : ""}
|
|
9450
9696
|
</article>
|
|
9451
9697
|
`).join("")}</div>`;
|
|
9452
9698
|
};
|
|
@@ -9461,26 +9707,26 @@ var renderSimulationControls = (kind, simulation) => {
|
|
|
9461
9707
|
const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
|
|
9462
9708
|
const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
|
|
9463
9709
|
const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
|
|
9464
|
-
return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${
|
|
9465
|
-
<p class="muted">${
|
|
9710
|
+
return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml11(pathPrefix)}">
|
|
9711
|
+
<p class="muted">${escapeHtml11(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
|
|
9466
9712
|
<div class="simulate-actions">
|
|
9467
|
-
${failureProviders.map((provider) => `<button type="button" data-provider-fail="${
|
|
9468
|
-
${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${
|
|
9713
|
+
${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml11(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml11(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
|
|
9714
|
+
${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml11(provider.provider)}">Mark ${escapeHtml11(provider.provider)} recovered</button>`).join("")}
|
|
9469
9715
|
</div>
|
|
9470
|
-
${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${
|
|
9716
|
+
${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml11(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
|
|
9471
9717
|
<pre class="simulate-output" hidden></pre>
|
|
9472
9718
|
</div>`;
|
|
9473
9719
|
};
|
|
9474
9720
|
var renderVoiceResilienceHTML = (input) => {
|
|
9475
9721
|
const summary = summarizeRoutingEvents(input.routingEvents);
|
|
9476
|
-
const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${
|
|
9477
|
-
const links = input.links?.length ? input.links.map((link) => `<a href="${
|
|
9722
|
+
const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml11(kind)}: ${String(count)}</span>`).join("");
|
|
9723
|
+
const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml11(link.href)}">${escapeHtml11(link.label)}</a>`).join(" \xB7 ") : "";
|
|
9478
9724
|
return `<!doctype html>
|
|
9479
9725
|
<html lang="en">
|
|
9480
9726
|
<head>
|
|
9481
9727
|
<meta charset="utf-8" />
|
|
9482
9728
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
9483
|
-
<title>${
|
|
9729
|
+
<title>${escapeHtml11(input.title ?? "AbsoluteJS Voice Resilience")}</title>
|
|
9484
9730
|
<style>
|
|
9485
9731
|
:root { color-scheme: dark; }
|
|
9486
9732
|
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; }
|
|
@@ -9616,7 +9862,7 @@ var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
|
|
|
9616
9862
|
};
|
|
9617
9863
|
var createVoiceResilienceRoutes = (options) => {
|
|
9618
9864
|
const path = options.path ?? "/resilience";
|
|
9619
|
-
const routes = new
|
|
9865
|
+
const routes = new Elysia9({
|
|
9620
9866
|
name: options.name ?? "absolutejs-voice-resilience"
|
|
9621
9867
|
}).get(path, async () => {
|
|
9622
9868
|
const events = await options.store.list();
|
|
@@ -9662,6 +9908,12 @@ var DEFAULT_LINKS = [
|
|
|
9662
9908
|
label: "Quality",
|
|
9663
9909
|
statusHref: "/quality/status"
|
|
9664
9910
|
},
|
|
9911
|
+
{
|
|
9912
|
+
description: "Replay stored sessions against acceptance gates over time.",
|
|
9913
|
+
href: "/evals",
|
|
9914
|
+
label: "Evals",
|
|
9915
|
+
statusHref: "/evals/status"
|
|
9916
|
+
},
|
|
9665
9917
|
{
|
|
9666
9918
|
description: "Provider health, fallback paths, and failure simulation.",
|
|
9667
9919
|
href: "/resilience",
|
|
@@ -9683,7 +9935,7 @@ var DEFAULT_LINKS = [
|
|
|
9683
9935
|
label: "Handoffs"
|
|
9684
9936
|
}
|
|
9685
9937
|
];
|
|
9686
|
-
var
|
|
9938
|
+
var escapeHtml12 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9687
9939
|
var countProviderStatuses = (providers) => {
|
|
9688
9940
|
const degradedStatuses = new Set(["degraded", "rate-limited", "suppressed"]);
|
|
9689
9941
|
const healthy = providers.filter((provider) => provider.status === "healthy").length;
|
|
@@ -9739,20 +9991,20 @@ var buildVoiceOpsConsoleReport = async (options) => {
|
|
|
9739
9991
|
trace
|
|
9740
9992
|
};
|
|
9741
9993
|
};
|
|
9742
|
-
var renderMetricCard = (input) => `<article class="metric"><span>${
|
|
9994
|
+
var renderMetricCard = (input) => `<article class="metric"><span>${escapeHtml12(input.label)}</span><strong>${escapeHtml12(String(input.value))}</strong>${input.status ? `<p class="${escapeHtml12(input.status)}">${escapeHtml12(input.status)}</p>` : ""}${input.href ? `<a href="${escapeHtml12(input.href)}">Open</a>` : ""}</article>`;
|
|
9743
9995
|
var renderVoiceOpsConsoleHTML = (report, options = {}) => {
|
|
9744
9996
|
const links = report.links.map((link) => `<article class="surface">
|
|
9745
|
-
<div><h2>${
|
|
9746
|
-
<p><a href="${
|
|
9997
|
+
<div><h2>${escapeHtml12(link.label)}</h2>${link.description ? `<p>${escapeHtml12(link.description)}</p>` : ""}</div>
|
|
9998
|
+
<p><a href="${escapeHtml12(link.href)}">Open ${escapeHtml12(link.label)}</a>${link.statusHref ? ` \xB7 <a href="${escapeHtml12(link.statusHref)}">Status</a>` : ""}</p>
|
|
9747
9999
|
</article>`).join("");
|
|
9748
|
-
const sessions = report.recentSessions.length ? report.recentSessions.map((session) => `<tr><td>${
|
|
9749
|
-
const routing = report.recentRoutingEvents.length ? report.recentRoutingEvents.map((event) => `<tr><td>${
|
|
10000
|
+
const sessions = report.recentSessions.length ? report.recentSessions.map((session) => `<tr><td>${escapeHtml12(session.sessionId)}</td><td>${escapeHtml12(session.status)}</td><td>${session.turnCount}</td><td>${session.errorCount}</td><td>${session.replayHref ? `<a href="${escapeHtml12(session.replayHref)}">Replay</a>` : ""}</td></tr>`).join("") : '<tr><td colspan="5">No sessions yet.</td></tr>';
|
|
10001
|
+
const routing = report.recentRoutingEvents.length ? report.recentRoutingEvents.map((event) => `<tr><td>${escapeHtml12(event.kind)}</td><td>${escapeHtml12(event.provider ?? "unknown")}</td><td>${escapeHtml12(event.status ?? "unknown")}</td><td>${event.elapsedMs ?? 0}ms</td><td>${escapeHtml12(event.sessionId)}</td></tr>`).join("") : '<tr><td colspan="5">No provider routing events yet.</td></tr>';
|
|
9750
10002
|
const title = options.title ?? "AbsoluteJS Voice Ops Console";
|
|
9751
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
10003
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml12(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;background:#101316;color:#f6f2e8;margin:0}main{max-width:1180px;margin:auto;padding:32px}a{color:#fbbf24}header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}.eyebrow{color:#fbbf24;font-weight:800;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.2rem,5vw,4.5rem);line-height:.95;margin:.2rem 0 1rem}.muted{color:#a8b0b8}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.metric,.surface{background:#181d22;border:1px solid #2a323a;border-radius:20px;padding:18px}.metric strong{display:block;font-size:2.2rem;margin:.25rem 0}.pass,.healthy{color:#86efac}.fail,.failed,.degraded{color:#fca5a5}.surfaces{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin:24px 0}table{width:100%;border-collapse:collapse;background:#181d22;border-radius:16px;overflow:hidden;margin:12px 0 28px}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left}section{margin-top:30px}@media(max-width:700px){main{padding:20px}header{display:block}}</style></head><body><main><header><div><p class="eyebrow">Self-hosted voice operations</p><h1>${escapeHtml12(title)}</h1><p class="muted">One deployable control plane for quality gates, failover, traces, sessions, handoffs, and provider health.</p></div><p class="muted">Checked ${escapeHtml12(new Date(report.checkedAt).toLocaleString())}</p></header><div class="grid">${renderMetricCard({ label: "Quality", value: report.quality.status, status: report.quality.status, href: "/quality" })}${renderMetricCard({ label: "Events", value: report.eventCount, href: "/diagnostics" })}${renderMetricCard({ label: "Sessions", value: report.sessions.total, status: report.sessions.failed > 0 ? "failed" : "healthy", href: "/sessions" })}${renderMetricCard({ label: "Handoffs failed", value: report.handoffs.failed, status: report.handoffs.failed > 0 ? "failed" : "healthy", href: "/handoffs" })}${renderMetricCard({ label: "Providers degraded", value: report.providers.degraded, status: report.providers.degraded > 0 ? "degraded" : "healthy", href: "/resilience" })}</div><section><h2>Operational Surfaces</h2><div class="surfaces">${links}</div></section><section><h2>Recent Sessions</h2><table><thead><tr><th>Session</th><th>Status</th><th>Turns</th><th>Errors</th><th>Replay</th></tr></thead><tbody>${sessions}</tbody></table></section><section><h2>Recent Provider Routing</h2><table><thead><tr><th>Kind</th><th>Provider</th><th>Status</th><th>Elapsed</th><th>Session</th></tr></thead><tbody>${routing}</tbody></table></section></main></body></html>`;
|
|
9752
10004
|
};
|
|
9753
10005
|
var createVoiceOpsConsoleRoutes = (options) => {
|
|
9754
10006
|
const path = options.path ?? "/ops-console";
|
|
9755
|
-
const routes = new
|
|
10007
|
+
const routes = new Elysia10({
|
|
9756
10008
|
name: options.name ?? "absolutejs-voice-ops-console"
|
|
9757
10009
|
});
|
|
9758
10010
|
const getReport = () => buildVoiceOpsConsoleReport(options);
|
|
@@ -10612,7 +10864,7 @@ var createVoiceMemoryStore = () => {
|
|
|
10612
10864
|
return { get, getOrCreate, list, remove, set };
|
|
10613
10865
|
};
|
|
10614
10866
|
// src/opsWebhook.ts
|
|
10615
|
-
import { Elysia as
|
|
10867
|
+
import { Elysia as Elysia11 } from "elysia";
|
|
10616
10868
|
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
10617
10869
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
10618
10870
|
const encoder = new TextEncoder;
|
|
@@ -10742,7 +10994,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
|
10742
10994
|
};
|
|
10743
10995
|
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
10744
10996
|
const path = options.path ?? "/api/voice-ops/webhook";
|
|
10745
|
-
return new
|
|
10997
|
+
return new Elysia11().post(path, async ({ body, request, set }) => {
|
|
10746
10998
|
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
10747
10999
|
if (options.signingSecret) {
|
|
10748
11000
|
const verification = await verifyVoiceOpsWebhookSignature({
|
|
@@ -12904,6 +13156,7 @@ export {
|
|
|
12904
13156
|
startVoiceOpsTask,
|
|
12905
13157
|
shapeTelephonyAssistantText,
|
|
12906
13158
|
selectVoiceTraceEventsForPrune,
|
|
13159
|
+
runVoiceSessionEvals,
|
|
12907
13160
|
resolveVoiceTraceRedactionOptions,
|
|
12908
13161
|
resolveVoiceSTTRoutingStrategy,
|
|
12909
13162
|
resolveVoiceRuntimePreset,
|
|
@@ -12926,6 +13179,8 @@ export {
|
|
|
12926
13179
|
renderVoiceProviderHealthHTML,
|
|
12927
13180
|
renderVoiceOpsConsoleHTML,
|
|
12928
13181
|
renderVoiceHandoffHealthHTML,
|
|
13182
|
+
renderVoiceEvalHTML,
|
|
13183
|
+
renderVoiceEvalBaselineHTML,
|
|
12929
13184
|
renderVoiceCallReviewMarkdown,
|
|
12930
13185
|
renderVoiceCallReviewHTML,
|
|
12931
13186
|
renderVoiceAssistantHealthHTML,
|
|
@@ -13047,10 +13302,12 @@ export {
|
|
|
13047
13302
|
createVoiceFileReviewStore,
|
|
13048
13303
|
createVoiceFileIntegrationEventStore,
|
|
13049
13304
|
createVoiceFileExternalObjectMapStore,
|
|
13305
|
+
createVoiceFileEvalBaselineStore,
|
|
13050
13306
|
createVoiceFileAssistantMemoryStore,
|
|
13051
13307
|
createVoiceExternalObjectMapId,
|
|
13052
13308
|
createVoiceExternalObjectMap,
|
|
13053
13309
|
createVoiceExperiment,
|
|
13310
|
+
createVoiceEvalRoutes,
|
|
13054
13311
|
createVoiceDiagnosticsRoutes,
|
|
13055
13312
|
createVoiceCallReviewRecorder,
|
|
13056
13313
|
createVoiceCallReviewFromSession,
|
|
@@ -13083,6 +13340,7 @@ export {
|
|
|
13083
13340
|
createAnthropicVoiceAssistantModel,
|
|
13084
13341
|
conditionAudioChunk,
|
|
13085
13342
|
completeVoiceOpsTask,
|
|
13343
|
+
compareVoiceEvalBaseline,
|
|
13086
13344
|
claimVoiceOpsTask,
|
|
13087
13345
|
buildVoiceTraceReplay,
|
|
13088
13346
|
buildVoiceOpsTaskFromSLABreach,
|