@absolutejs/voice 0.0.22-beta.105 → 0.0.22-beta.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +693 -515
- package/dist/simulationSuite.d.ts +120 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10546,304 +10546,166 @@ var createVoiceAppKitRoutes = (options) => {
|
|
|
10546
10546
|
};
|
|
10547
10547
|
};
|
|
10548
10548
|
var createVoiceAppKit = createVoiceAppKitRoutes;
|
|
10549
|
-
// src/
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10549
|
+
// src/simulationSuite.ts
|
|
10550
|
+
import { Elysia as Elysia19 } from "elysia";
|
|
10551
|
+
|
|
10552
|
+
// src/outcomeContract.ts
|
|
10553
|
+
import { Elysia as Elysia17 } from "elysia";
|
|
10554
|
+
var escapeHtml18 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10555
|
+
var getPayloadString = (event, key) => typeof event.payload[key] === "string" ? event.payload[key] : undefined;
|
|
10556
|
+
var toList = async (input) => Array.isArray(input) ? input : await input?.list() ?? [];
|
|
10557
|
+
var hydrateSessions = async (input) => {
|
|
10558
|
+
if (!input)
|
|
10559
|
+
return [];
|
|
10560
|
+
if (Array.isArray(input))
|
|
10561
|
+
return input;
|
|
10562
|
+
const summaries = await input.list();
|
|
10563
|
+
const sessions = await Promise.all(summaries.map((summary) => input.get(summary.id)));
|
|
10564
|
+
const hydrated = [];
|
|
10565
|
+
for (const session of sessions) {
|
|
10566
|
+
if (session) {
|
|
10567
|
+
hydrated.push(session);
|
|
10557
10568
|
}
|
|
10558
|
-
current = record[part];
|
|
10559
|
-
}
|
|
10560
|
-
return current;
|
|
10561
|
-
};
|
|
10562
|
-
var hasValue = (value, match) => {
|
|
10563
|
-
switch (match) {
|
|
10564
|
-
case "boolean":
|
|
10565
|
-
return typeof value === "boolean";
|
|
10566
|
-
case "number":
|
|
10567
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
10568
|
-
case "string":
|
|
10569
|
-
return typeof value === "string";
|
|
10570
|
-
case "truthy":
|
|
10571
|
-
return Boolean(value);
|
|
10572
|
-
case "non-empty":
|
|
10573
|
-
default:
|
|
10574
|
-
return Array.isArray(value) ? value.length > 0 : typeof value === "string" ? value.trim().length > 0 : value !== undefined && value !== null;
|
|
10575
10569
|
}
|
|
10570
|
+
return hydrated;
|
|
10576
10571
|
};
|
|
10577
|
-
var
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
|
|
10589
|
-
|
|
10590
|
-
|
|
10572
|
+
var dispositionForSession = (session) => session.call?.disposition ?? (session.status === "completed" ? "completed" : undefined);
|
|
10573
|
+
var matchesDisposition = (disposition, expected) => expected === undefined || disposition === expected;
|
|
10574
|
+
var reportContract = (input) => {
|
|
10575
|
+
const { contract } = input;
|
|
10576
|
+
const sessions = input.sessions.filter((session) => (!contract.scenarioId || session.scenarioId === contract.scenarioId) && matchesDisposition(dispositionForSession(session), contract.expectedDisposition));
|
|
10577
|
+
const sessionIds = new Set(sessions.map((session) => session.id));
|
|
10578
|
+
const reviews = input.reviews.filter((review) => matchesDisposition(review.summary.outcome, contract.expectedDisposition));
|
|
10579
|
+
const tasks = input.tasks.filter((task) => matchesDisposition(task.outcome, contract.expectedDisposition));
|
|
10580
|
+
const handoffs = input.handoffs.filter((handoff) => (!contract.expectedDisposition || handoff.action === contract.expectedDisposition || contract.expectedDisposition === "transferred" && handoff.action === "transfer" || contract.expectedDisposition === "escalated" && handoff.action === "escalate") && (sessionIds.size === 0 || sessionIds.has(handoff.sessionId)));
|
|
10581
|
+
const events = input.events.filter((event) => {
|
|
10582
|
+
const eventSessionId = getPayloadString(event, "sessionId");
|
|
10583
|
+
const eventOutcome = getPayloadString(event, "outcome") ?? getPayloadString(event, "disposition");
|
|
10584
|
+
return (sessionIds.size === 0 || !eventSessionId || sessionIds.has(eventSessionId)) && (!contract.expectedDisposition || eventOutcome === contract.expectedDisposition);
|
|
10585
|
+
});
|
|
10591
10586
|
const issues = [];
|
|
10592
|
-
const
|
|
10593
|
-
|
|
10594
|
-
const outcome = resolveOutcome2(routeResult);
|
|
10595
|
-
if (definition.outcome && outcome !== definition.outcome) {
|
|
10587
|
+
const minSessions = contract.minSessions ?? 1;
|
|
10588
|
+
if (sessions.length < minSessions) {
|
|
10596
10589
|
issues.push({
|
|
10597
|
-
code: "
|
|
10598
|
-
message: `Expected
|
|
10590
|
+
code: "outcome.sessions_missing",
|
|
10591
|
+
message: `Expected at least ${minSessions} matching session(s), saw ${sessions.length}.`
|
|
10599
10592
|
});
|
|
10600
10593
|
}
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10594
|
+
if (contract.requireReview !== false && reviews.length === 0) {
|
|
10595
|
+
issues.push({
|
|
10596
|
+
code: "outcome.review_missing",
|
|
10597
|
+
message: "Expected at least one matching review artifact."
|
|
10598
|
+
});
|
|
10599
|
+
}
|
|
10600
|
+
if (contract.requireTask && tasks.length < (contract.minTasks ?? 1)) {
|
|
10601
|
+
issues.push({
|
|
10602
|
+
code: "outcome.task_missing",
|
|
10603
|
+
message: `Expected at least ${contract.minTasks ?? 1} matching task(s), saw ${tasks.length}.`
|
|
10604
|
+
});
|
|
10605
|
+
}
|
|
10606
|
+
for (const action of contract.requireHandoffActions ?? []) {
|
|
10607
|
+
if (!handoffs.some((handoff) => handoff.action === action)) {
|
|
10608
10608
|
issues.push({
|
|
10609
|
-
code: "
|
|
10610
|
-
|
|
10611
|
-
|
|
10609
|
+
code: "outcome.handoff_missing",
|
|
10610
|
+
message: `Expected handoff action ${action}.`
|
|
10611
|
+
});
|
|
10612
|
+
}
|
|
10613
|
+
}
|
|
10614
|
+
for (const type of contract.requireIntegrationEvents ?? []) {
|
|
10615
|
+
if (!events.some((event) => event.type === type)) {
|
|
10616
|
+
issues.push({
|
|
10617
|
+
code: "outcome.integration_event_missing",
|
|
10618
|
+
message: `Expected integration event ${type}.`
|
|
10612
10619
|
});
|
|
10613
10620
|
}
|
|
10614
10621
|
}
|
|
10615
|
-
issues.push(...definition.validate?.({
|
|
10616
|
-
result: routeResult.result,
|
|
10617
|
-
routeResult
|
|
10618
|
-
}) ?? []);
|
|
10619
10622
|
return {
|
|
10620
|
-
contractId:
|
|
10623
|
+
contractId: contract.id,
|
|
10624
|
+
description: contract.description,
|
|
10621
10625
|
issues,
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
+
label: contract.label,
|
|
10627
|
+
matched: {
|
|
10628
|
+
handoffs: handoffs.length,
|
|
10629
|
+
integrationEvents: events.length,
|
|
10630
|
+
reviews: reviews.length,
|
|
10631
|
+
sessions: sessions.length,
|
|
10632
|
+
tasks: tasks.length
|
|
10633
|
+
},
|
|
10634
|
+
pass: issues.length === 0
|
|
10626
10635
|
};
|
|
10627
10636
|
};
|
|
10628
|
-
var
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
|
|
10633
|
-
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10637
|
+
var runVoiceOutcomeContractSuite = async (options) => {
|
|
10638
|
+
const [sessions, reviews, tasks, events, handoffs] = await Promise.all([
|
|
10639
|
+
hydrateSessions(options.sessions),
|
|
10640
|
+
toList(options.reviews),
|
|
10641
|
+
toList(options.tasks),
|
|
10642
|
+
toList(options.events),
|
|
10643
|
+
toList(options.handoffs)
|
|
10644
|
+
]);
|
|
10645
|
+
const contracts = options.contracts.map((contract) => reportContract({ contract, events, handoffs, reviews, sessions, tasks }));
|
|
10646
|
+
const passed = contracts.filter((contract) => contract.pass).length;
|
|
10647
|
+
const failed = contracts.length - passed;
|
|
10648
|
+
return {
|
|
10649
|
+
checkedAt: Date.now(),
|
|
10650
|
+
contracts,
|
|
10651
|
+
failed,
|
|
10652
|
+
passed,
|
|
10653
|
+
status: failed > 0 ? "fail" : "pass",
|
|
10654
|
+
total: contracts.length
|
|
10655
|
+
};
|
|
10656
|
+
};
|
|
10657
|
+
var renderVoiceOutcomeContractHTML = (report, options = {}) => {
|
|
10658
|
+
const title = options.title ?? "Voice Outcome Contracts";
|
|
10659
|
+
const contracts = report.contracts.map((contract) => `<section class="contract ${contract.pass ? "pass" : "fail"}">
|
|
10660
|
+
<div class="contract-header">
|
|
10661
|
+
<div>
|
|
10662
|
+
<p class="eyebrow">${escapeHtml18(contract.contractId)}</p>
|
|
10663
|
+
<h2>${escapeHtml18(contract.label ?? contract.contractId)}</h2>
|
|
10664
|
+
${contract.description ? `<p>${escapeHtml18(contract.description)}</p>` : ""}
|
|
10665
|
+
</div>
|
|
10666
|
+
<strong>${contract.pass ? "pass" : "fail"}</strong>
|
|
10667
|
+
</div>
|
|
10668
|
+
<div class="grid">
|
|
10669
|
+
<span>sessions ${String(contract.matched.sessions)}</span>
|
|
10670
|
+
<span>reviews ${String(contract.matched.reviews)}</span>
|
|
10671
|
+
<span>tasks ${String(contract.matched.tasks)}</span>
|
|
10672
|
+
<span>handoffs ${String(contract.matched.handoffs)}</span>
|
|
10673
|
+
<span>events ${String(contract.matched.integrationEvents)}</span>
|
|
10674
|
+
</div>
|
|
10675
|
+
${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml18(issue.message)}</li>`).join("")}</ul>` : ""}
|
|
10676
|
+
</section>`).join("");
|
|
10677
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml18(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.contract{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.14),rgba(14,165,233,.12))}.eyebrow{color:#7dd3fc;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0}.summary,.grid{display:flex;flex-wrap:wrap;gap:10px}.pill,.grid span{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}li{margin:8px 0}@media(max-width:800px){main{padding:18px}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Business Outcome Verification</p><h1>${escapeHtml18(title)}</h1><div class="summary"><span class="pill ${report.status}">${report.status}</span><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} contracts</span></div></section>${contracts || '<section class="contract"><p>No outcome contracts configured.</p></section>'}</main></body></html>`;
|
|
10678
|
+
};
|
|
10679
|
+
var createVoiceOutcomeContractJSONHandler = (options) => async () => runVoiceOutcomeContractSuite(options);
|
|
10680
|
+
var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
|
|
10681
|
+
const report = await runVoiceOutcomeContractSuite(options);
|
|
10682
|
+
const render = options.render ?? ((input) => renderVoiceOutcomeContractHTML(input, options));
|
|
10683
|
+
return new Response(await render(report), {
|
|
10684
|
+
headers: {
|
|
10685
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10686
|
+
...options.headers
|
|
10651
10687
|
}
|
|
10652
|
-
}
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
}
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
aliases: ["summary", "assistantSummary"],
|
|
10674
|
-
label: "Summary",
|
|
10675
|
-
path: "appointment.summary"
|
|
10676
|
-
}
|
|
10677
|
-
],
|
|
10678
|
-
id: "appointment-booking",
|
|
10679
|
-
label: "Appointment booking",
|
|
10680
|
-
outcome: "complete",
|
|
10681
|
-
requiredDisposition: "completed"
|
|
10682
|
-
},
|
|
10683
|
-
"lead-qualification": {
|
|
10684
|
-
description: "Lead qualification should complete with contact, need, qualification, and next-step fields.",
|
|
10685
|
-
fields: [
|
|
10686
|
-
{ aliases: ["name", "lead.name"], label: "Lead name", path: "contact.name" },
|
|
10687
|
-
{
|
|
10688
|
-
aliases: ["email", "lead.email"],
|
|
10689
|
-
label: "Lead email",
|
|
10690
|
-
path: "contact.email"
|
|
10691
|
-
},
|
|
10692
|
-
{
|
|
10693
|
-
aliases: ["need", "pain", "summary"],
|
|
10694
|
-
label: "Need",
|
|
10695
|
-
path: "qualification.need"
|
|
10696
|
-
},
|
|
10697
|
-
{
|
|
10698
|
-
aliases: ["qualified", "qualification.qualified"],
|
|
10699
|
-
label: "Qualified",
|
|
10700
|
-
match: "boolean",
|
|
10701
|
-
path: "qualification.isQualified"
|
|
10702
|
-
},
|
|
10703
|
-
{
|
|
10704
|
-
aliases: ["nextStep", "followUp"],
|
|
10705
|
-
label: "Next step",
|
|
10706
|
-
path: "qualification.nextStep"
|
|
10707
|
-
}
|
|
10708
|
-
],
|
|
10709
|
-
id: "lead-qualification",
|
|
10710
|
-
label: "Lead qualification",
|
|
10711
|
-
outcome: "complete",
|
|
10712
|
-
requiredDisposition: "completed"
|
|
10713
|
-
},
|
|
10714
|
-
"support-triage": {
|
|
10715
|
-
description: "Support triage should capture identity, issue summary, severity, and the operational follow-up.",
|
|
10716
|
-
fields: [
|
|
10717
|
-
{
|
|
10718
|
-
aliases: ["name", "customer.name"],
|
|
10719
|
-
label: "Customer name",
|
|
10720
|
-
path: "customer.name"
|
|
10721
|
-
},
|
|
10722
|
-
{
|
|
10723
|
-
aliases: ["issue", "summary", "assistantSummary"],
|
|
10724
|
-
label: "Issue summary",
|
|
10725
|
-
path: "issue.summary"
|
|
10726
|
-
},
|
|
10727
|
-
{
|
|
10728
|
-
aliases: ["priority", "severity"],
|
|
10729
|
-
label: "Severity",
|
|
10730
|
-
path: "issue.severity"
|
|
10731
|
-
},
|
|
10732
|
-
{
|
|
10733
|
-
aliases: ["nextStep", "task.title"],
|
|
10734
|
-
label: "Next step",
|
|
10735
|
-
path: "resolution.nextStep"
|
|
10736
|
-
}
|
|
10737
|
-
],
|
|
10738
|
-
id: "support-triage",
|
|
10739
|
-
label: "Support triage",
|
|
10740
|
-
outcome: "complete",
|
|
10741
|
-
requiredDisposition: "completed"
|
|
10742
|
-
},
|
|
10743
|
-
"transfer-handoff": {
|
|
10744
|
-
description: "Transfer handoff should produce a routed transfer plus handoff evidence.",
|
|
10745
|
-
fields: [
|
|
10746
|
-
{
|
|
10747
|
-
aliases: ["target", "callTarget"],
|
|
10748
|
-
label: "Transfer target",
|
|
10749
|
-
path: "transfer.target"
|
|
10750
|
-
},
|
|
10751
|
-
{
|
|
10752
|
-
aliases: ["reason", "callReason"],
|
|
10753
|
-
label: "Transfer reason",
|
|
10754
|
-
path: "transfer.reason"
|
|
10755
|
-
},
|
|
10756
|
-
{
|
|
10757
|
-
aliases: ["summary", "assistantSummary"],
|
|
10758
|
-
label: "Transfer summary",
|
|
10759
|
-
path: "transfer.summary"
|
|
10760
|
-
}
|
|
10761
|
-
],
|
|
10762
|
-
id: "transfer-handoff",
|
|
10763
|
-
label: "Transfer handoff",
|
|
10764
|
-
outcome: "transfer",
|
|
10765
|
-
requiredDisposition: "transferred",
|
|
10766
|
-
requiredHandoffActions: ["transfer"]
|
|
10767
|
-
},
|
|
10768
|
-
"voicemail-callback": {
|
|
10769
|
-
description: "Voicemail callback should preserve enough caller and callback context for follow-up.",
|
|
10770
|
-
fields: [
|
|
10771
|
-
{
|
|
10772
|
-
aliases: ["name", "caller.name"],
|
|
10773
|
-
label: "Caller name",
|
|
10774
|
-
path: "voicemail.callerName"
|
|
10775
|
-
},
|
|
10776
|
-
{
|
|
10777
|
-
aliases: ["phone", "caller.phone"],
|
|
10778
|
-
label: "Callback phone",
|
|
10779
|
-
path: "voicemail.callbackPhone"
|
|
10780
|
-
},
|
|
10781
|
-
{
|
|
10782
|
-
aliases: ["message", "summary", "assistantSummary"],
|
|
10783
|
-
label: "Voicemail summary",
|
|
10784
|
-
path: "voicemail.summary"
|
|
10785
|
-
}
|
|
10786
|
-
],
|
|
10787
|
-
id: "voicemail-callback",
|
|
10788
|
-
label: "Voicemail callback",
|
|
10789
|
-
outcome: "voicemail",
|
|
10790
|
-
requiredDisposition: "voicemail",
|
|
10791
|
-
requiredHandoffActions: ["voicemail"]
|
|
10792
|
-
}
|
|
10793
|
-
};
|
|
10794
|
-
var createVoiceWorkflowContractPreset = (name, options = {}) => {
|
|
10795
|
-
const preset = presetDefinitions[name];
|
|
10796
|
-
return createVoiceWorkflowContract({
|
|
10797
|
-
...preset,
|
|
10798
|
-
...options,
|
|
10799
|
-
fields: options.fields ?? preset.fields,
|
|
10800
|
-
id: options.id ?? preset.id
|
|
10801
|
-
});
|
|
10802
|
-
};
|
|
10803
|
-
var recordVoiceWorkflowContractTrace = async (input) => input.store.append({
|
|
10804
|
-
at: input.at ?? Date.now(),
|
|
10805
|
-
payload: {
|
|
10806
|
-
contractId: input.contractId ?? input.validation.contractId,
|
|
10807
|
-
issues: input.validation.issues,
|
|
10808
|
-
missingFields: input.validation.missingFields,
|
|
10809
|
-
outcome: input.validation.outcome,
|
|
10810
|
-
requiredFields: input.validation.requiredFields,
|
|
10811
|
-
status: input.validation.pass ? "pass" : "fail"
|
|
10812
|
-
},
|
|
10813
|
-
scenarioId: input.scenarioId,
|
|
10814
|
-
sessionId: input.sessionId,
|
|
10815
|
-
traceId: input.traceId,
|
|
10816
|
-
turnId: input.turnId,
|
|
10817
|
-
type: "workflow.contract"
|
|
10818
|
-
});
|
|
10819
|
-
var createVoiceWorkflowContractHandler = (input) => {
|
|
10820
|
-
return async (session, turn, api, context) => {
|
|
10821
|
-
const legacyHandler = input.handler;
|
|
10822
|
-
const objectHandler = input.handler;
|
|
10823
|
-
const result = input.handler.length >= 4 ? await legacyHandler(session, turn, api, context) : await objectHandler({ api, context, session, turn });
|
|
10824
|
-
if (!result)
|
|
10825
|
-
return result;
|
|
10826
|
-
const resolved = input.resolveContract?.({ context, result, session, turn }) ?? input.contract;
|
|
10827
|
-
if (!resolved)
|
|
10828
|
-
return result;
|
|
10829
|
-
const contract = "validateRouteResult" in resolved ? resolved : createVoiceWorkflowContract(resolved);
|
|
10830
|
-
const validation = contract.validateRouteResult(result);
|
|
10831
|
-
if (input.store) {
|
|
10832
|
-
await recordVoiceWorkflowContractTrace({
|
|
10833
|
-
scenarioId: session.scenarioId,
|
|
10834
|
-
sessionId: session.id,
|
|
10835
|
-
store: input.store,
|
|
10836
|
-
turnId: turn.id,
|
|
10837
|
-
validation
|
|
10838
|
-
});
|
|
10839
|
-
}
|
|
10840
|
-
return result;
|
|
10841
|
-
};
|
|
10842
|
-
};
|
|
10843
|
-
// src/toolRuntime.ts
|
|
10844
|
-
var toErrorMessage4 = (error) => error instanceof Error ? error.message : String(error);
|
|
10845
|
-
var sleep4 = (ms) => new Promise((resolve2) => {
|
|
10846
|
-
setTimeout(resolve2, ms);
|
|
10688
|
+
});
|
|
10689
|
+
};
|
|
10690
|
+
var createVoiceOutcomeContractRoutes = (options) => {
|
|
10691
|
+
const path = options.path ?? "/api/outcome-contracts";
|
|
10692
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
10693
|
+
const routes = new Elysia17({
|
|
10694
|
+
name: options.name ?? "absolutejs-voice-outcome-contracts"
|
|
10695
|
+
}).get(path, createVoiceOutcomeContractJSONHandler(options));
|
|
10696
|
+
if (htmlPath) {
|
|
10697
|
+
routes.get(htmlPath, createVoiceOutcomeContractHTMLHandler(options));
|
|
10698
|
+
}
|
|
10699
|
+
return routes;
|
|
10700
|
+
};
|
|
10701
|
+
|
|
10702
|
+
// src/toolContract.ts
|
|
10703
|
+
import { Elysia as Elysia18 } from "elysia";
|
|
10704
|
+
|
|
10705
|
+
// src/toolRuntime.ts
|
|
10706
|
+
var toErrorMessage4 = (error) => error instanceof Error ? error.message : String(error);
|
|
10707
|
+
var sleep4 = (ms) => new Promise((resolve2) => {
|
|
10708
|
+
setTimeout(resolve2, ms);
|
|
10847
10709
|
});
|
|
10848
10710
|
var formatToolResult2 = (result) => {
|
|
10849
10711
|
if (typeof result === "string") {
|
|
@@ -11036,8 +10898,8 @@ var createVoiceToolIdempotencyKey = (input) => {
|
|
|
11036
10898
|
args
|
|
11037
10899
|
].join(":");
|
|
11038
10900
|
};
|
|
10901
|
+
|
|
11039
10902
|
// src/toolContract.ts
|
|
11040
|
-
import { Elysia as Elysia17 } from "elysia";
|
|
11041
10903
|
var createDefaultSession = (contractId, caseId) => createVoiceSessionRecord(`tool-contract-${contractId}-${caseId}`);
|
|
11042
10904
|
var createDefaultTurn = (caseId) => ({
|
|
11043
10905
|
committedAt: Date.now(),
|
|
@@ -11047,7 +10909,7 @@ var createDefaultTurn = (caseId) => ({
|
|
|
11047
10909
|
});
|
|
11048
10910
|
var defaultApi = {};
|
|
11049
10911
|
var sameJSON = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
11050
|
-
var
|
|
10912
|
+
var escapeHtml19 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11051
10913
|
var evaluateExpectation = (input) => {
|
|
11052
10914
|
const issues = [];
|
|
11053
10915
|
const expect = input.expect;
|
|
@@ -11213,19 +11075,19 @@ var renderVoiceToolContractHTML = (report, options = {}) => {
|
|
|
11213
11075
|
const title = options.title ?? "Voice Tool Contracts";
|
|
11214
11076
|
const contracts = report.contracts.map((contract) => {
|
|
11215
11077
|
const cases = contract.cases.map((testCase) => `<tr>
|
|
11216
|
-
<td>${
|
|
11078
|
+
<td>${escapeHtml19(testCase.label ?? testCase.caseId)}</td>
|
|
11217
11079
|
<td class="${testCase.pass ? "pass" : "fail"}">${testCase.pass ? "pass" : "fail"}</td>
|
|
11218
|
-
<td>${
|
|
11080
|
+
<td>${escapeHtml19(testCase.status)}</td>
|
|
11219
11081
|
<td>${String(testCase.attempts)}</td>
|
|
11220
11082
|
<td>${String(testCase.elapsedMs)}ms</td>
|
|
11221
11083
|
<td>${testCase.timedOut ? "yes" : "no"}</td>
|
|
11222
|
-
<td>${
|
|
11084
|
+
<td>${escapeHtml19(testCase.issues.map((issue) => issue.message).join(" ") || testCase.error || "")}</td>
|
|
11223
11085
|
</tr>`).join("");
|
|
11224
11086
|
return `<section class="contract ${contract.pass ? "pass" : "fail"}">
|
|
11225
11087
|
<div class="contract-header">
|
|
11226
11088
|
<div>
|
|
11227
|
-
<p class="eyebrow">${
|
|
11228
|
-
<h2>${
|
|
11089
|
+
<p class="eyebrow">${escapeHtml19(contract.toolName)}</p>
|
|
11090
|
+
<h2>${escapeHtml19(contract.label ?? contract.contractId)}</h2>
|
|
11229
11091
|
</div>
|
|
11230
11092
|
<strong class="${contract.pass ? "pass" : "fail"}">${contract.pass ? "Passing" : "Failing"}</strong>
|
|
11231
11093
|
</div>
|
|
@@ -11235,7 +11097,7 @@ var renderVoiceToolContractHTML = (report, options = {}) => {
|
|
|
11235
11097
|
</table>
|
|
11236
11098
|
</section>`;
|
|
11237
11099
|
}).join("");
|
|
11238
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
11100
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml19(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.contract{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.14),rgba(245,158,11,.12))}.eyebrow{color:#fbbf24;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}h2{margin:.2rem 0 1rem}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}table{border-collapse:collapse;width:100%}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left;vertical-align:top}th{color:#a8b0b8;font-size:.82rem}@media(max-width:800px){main{padding:18px}table{display:block;overflow:auto}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Tool Reliability</p><h1>${escapeHtml19(title)}</h1><div class="summary"><span class="pill ${report.status === "pass" ? "pass" : "fail"}">${escapeHtml19(report.status)}</span><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} contracts</span></div></section>${contracts || '<section class="contract"><p>No tool contracts configured.</p></section>'}</main></body></html>`;
|
|
11239
11101
|
};
|
|
11240
11102
|
var createVoiceToolContractJSONHandler = (options) => () => runVoiceToolContractSuite(options);
|
|
11241
11103
|
var createVoiceToolContractHTMLHandler = (options) => async () => {
|
|
@@ -11249,22 +11111,484 @@ var createVoiceToolContractHTMLHandler = (options) => async () => {
|
|
|
11249
11111
|
}
|
|
11250
11112
|
});
|
|
11251
11113
|
};
|
|
11252
|
-
var createVoiceToolContractRoutes = (options) => {
|
|
11253
|
-
const path = options.path ?? "/api/tool-contracts";
|
|
11254
|
-
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
11255
|
-
const routes = new
|
|
11256
|
-
name: options.name ?? "absolutejs-voice-tool-contracts"
|
|
11257
|
-
}).get(path, createVoiceToolContractJSONHandler(options));
|
|
11258
|
-
if (htmlPath) {
|
|
11259
|
-
routes.get(htmlPath, createVoiceToolContractHTMLHandler(options));
|
|
11260
|
-
}
|
|
11261
|
-
return routes;
|
|
11114
|
+
var createVoiceToolContractRoutes = (options) => {
|
|
11115
|
+
const path = options.path ?? "/api/tool-contracts";
|
|
11116
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
11117
|
+
const routes = new Elysia18({
|
|
11118
|
+
name: options.name ?? "absolutejs-voice-tool-contracts"
|
|
11119
|
+
}).get(path, createVoiceToolContractJSONHandler(options));
|
|
11120
|
+
if (htmlPath) {
|
|
11121
|
+
routes.get(htmlPath, createVoiceToolContractHTMLHandler(options));
|
|
11122
|
+
}
|
|
11123
|
+
return routes;
|
|
11124
|
+
};
|
|
11125
|
+
|
|
11126
|
+
// src/simulationSuite.ts
|
|
11127
|
+
var escapeHtml20 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11128
|
+
var summarizeSection = (report) => ({
|
|
11129
|
+
failed: report.failed,
|
|
11130
|
+
passed: report.passed,
|
|
11131
|
+
status: report.status,
|
|
11132
|
+
total: report.total
|
|
11133
|
+
});
|
|
11134
|
+
var hasWork = (options) => Boolean(options.store || options.scenarios?.length || options.fixtures?.length || options.fixtureStore || options.tools?.length || options.outcomes?.contracts.length);
|
|
11135
|
+
var collectSimulationActions = (input) => {
|
|
11136
|
+
const actions = [];
|
|
11137
|
+
if (input.sessions?.failed) {
|
|
11138
|
+
const firstFailed = input.sessions.sessions.find((session) => session.status === "fail");
|
|
11139
|
+
actions.push({
|
|
11140
|
+
description: firstFailed ? `Inspect session ${firstFailed.sessionId}; at least one quality metric is outside threshold.` : "Inspect failing session quality reports.",
|
|
11141
|
+
href: input.links?.sessions,
|
|
11142
|
+
label: "Review failing session quality",
|
|
11143
|
+
section: "sessions",
|
|
11144
|
+
severity: "error"
|
|
11145
|
+
});
|
|
11146
|
+
}
|
|
11147
|
+
for (const scenario of input.scenarios?.scenarios ?? []) {
|
|
11148
|
+
if (scenario.status !== "fail") {
|
|
11149
|
+
continue;
|
|
11150
|
+
}
|
|
11151
|
+
const issue = scenario.issues[0] ?? scenario.sessions.find((session) => session.issues.length > 0)?.issues[0] ?? "Scenario did not meet its expected trace conditions.";
|
|
11152
|
+
actions.push({
|
|
11153
|
+
description: `${scenario.label}: ${issue}`,
|
|
11154
|
+
href: input.links?.scenarios,
|
|
11155
|
+
label: `Fix scenario ${scenario.label}`,
|
|
11156
|
+
section: "scenarios",
|
|
11157
|
+
severity: "error"
|
|
11158
|
+
});
|
|
11159
|
+
}
|
|
11160
|
+
for (const fixture of input.fixtures?.fixtures ?? []) {
|
|
11161
|
+
if (fixture.status !== "fail") {
|
|
11162
|
+
continue;
|
|
11163
|
+
}
|
|
11164
|
+
const failedScenario = fixture.report.scenarios.find((scenario) => scenario.status === "fail");
|
|
11165
|
+
actions.push({
|
|
11166
|
+
description: failedScenario ? `${fixture.label}: ${failedScenario.label} failed.` : `${fixture.label}: fixture simulation failed.`,
|
|
11167
|
+
href: input.links?.fixtures,
|
|
11168
|
+
label: `Update fixture ${fixture.label}`,
|
|
11169
|
+
section: "fixtures",
|
|
11170
|
+
severity: "error"
|
|
11171
|
+
});
|
|
11172
|
+
}
|
|
11173
|
+
for (const tool of input.tools?.contracts ?? []) {
|
|
11174
|
+
if (tool.pass) {
|
|
11175
|
+
continue;
|
|
11176
|
+
}
|
|
11177
|
+
const issue = tool.issues[0] ?? tool.cases.find((testCase) => !testCase.pass)?.issues[0];
|
|
11178
|
+
actions.push({
|
|
11179
|
+
description: issue?.message ?? `${tool.toolName} contract failed.`,
|
|
11180
|
+
href: input.links?.tools,
|
|
11181
|
+
label: `Fix tool contract ${tool.label ?? tool.contractId}`,
|
|
11182
|
+
section: "tools",
|
|
11183
|
+
severity: "error"
|
|
11184
|
+
});
|
|
11185
|
+
}
|
|
11186
|
+
for (const outcome of input.outcomes?.contracts ?? []) {
|
|
11187
|
+
if (outcome.pass) {
|
|
11188
|
+
continue;
|
|
11189
|
+
}
|
|
11190
|
+
actions.push({
|
|
11191
|
+
description: outcome.issues[0]?.message ?? `${outcome.label ?? outcome.contractId} is missing required outcome evidence.`,
|
|
11192
|
+
href: input.links?.outcomes,
|
|
11193
|
+
label: `Fix outcome ${outcome.label ?? outcome.contractId}`,
|
|
11194
|
+
section: "outcomes",
|
|
11195
|
+
severity: "error"
|
|
11196
|
+
});
|
|
11197
|
+
}
|
|
11198
|
+
return actions;
|
|
11199
|
+
};
|
|
11200
|
+
var runVoiceSimulationSuite = async (options) => {
|
|
11201
|
+
const include = options.include ?? {};
|
|
11202
|
+
const shouldRunSessions = include.sessions ?? Boolean(options.store || !hasWork(options));
|
|
11203
|
+
const shouldRunScenarios = include.scenarios ?? Boolean(options.scenarios?.length);
|
|
11204
|
+
const shouldRunFixtures = include.fixtures ?? Boolean((options.fixtures?.length ?? 0) > 0 || options.fixtureStore);
|
|
11205
|
+
const shouldRunTools = include.tools ?? Boolean(options.tools?.length);
|
|
11206
|
+
const shouldRunOutcomes = include.outcomes ?? Boolean(options.outcomes?.contracts.length);
|
|
11207
|
+
const [sessions, scenarios, fixtures, tools, outcomes] = await Promise.all([
|
|
11208
|
+
shouldRunSessions ? runVoiceSessionEvals({
|
|
11209
|
+
limit: options.limit,
|
|
11210
|
+
store: options.store,
|
|
11211
|
+
thresholds: options.thresholds
|
|
11212
|
+
}) : undefined,
|
|
11213
|
+
shouldRunScenarios ? runVoiceScenarioEvals({
|
|
11214
|
+
scenarios: options.scenarios,
|
|
11215
|
+
store: options.store
|
|
11216
|
+
}) : undefined,
|
|
11217
|
+
shouldRunFixtures ? runVoiceScenarioFixtureEvals({
|
|
11218
|
+
fixtures: options.fixtures,
|
|
11219
|
+
fixtureStore: options.fixtureStore,
|
|
11220
|
+
scenarios: options.scenarios
|
|
11221
|
+
}) : undefined,
|
|
11222
|
+
shouldRunTools ? runVoiceToolContractSuite({
|
|
11223
|
+
contracts: options.tools ?? []
|
|
11224
|
+
}) : undefined,
|
|
11225
|
+
shouldRunOutcomes && options.outcomes ? runVoiceOutcomeContractSuite(options.outcomes) : undefined
|
|
11226
|
+
]);
|
|
11227
|
+
const sections = [sessions, scenarios, fixtures, tools, outcomes].filter((report) => Boolean(report));
|
|
11228
|
+
const failed = sections.filter((section) => section.status === "fail").length;
|
|
11229
|
+
const passed = sections.length - failed;
|
|
11230
|
+
const actions = collectSimulationActions({
|
|
11231
|
+
fixtures,
|
|
11232
|
+
links: options.actionLinks,
|
|
11233
|
+
outcomes,
|
|
11234
|
+
scenarios,
|
|
11235
|
+
sessions,
|
|
11236
|
+
tools
|
|
11237
|
+
});
|
|
11238
|
+
return {
|
|
11239
|
+
actions,
|
|
11240
|
+
checkedAt: Date.now(),
|
|
11241
|
+
failed,
|
|
11242
|
+
fixtures,
|
|
11243
|
+
outcomes,
|
|
11244
|
+
passed,
|
|
11245
|
+
scenarios,
|
|
11246
|
+
sessions,
|
|
11247
|
+
status: failed > 0 ? "fail" : "pass",
|
|
11248
|
+
summary: {
|
|
11249
|
+
fixtures: fixtures && summarizeSection(fixtures),
|
|
11250
|
+
outcomes: outcomes && summarizeSection(outcomes),
|
|
11251
|
+
scenarios: scenarios && summarizeSection(scenarios),
|
|
11252
|
+
sessions: sessions && summarizeSection(sessions),
|
|
11253
|
+
tools: tools && summarizeSection(tools)
|
|
11254
|
+
},
|
|
11255
|
+
tools,
|
|
11256
|
+
total: sections.length
|
|
11257
|
+
};
|
|
11258
|
+
};
|
|
11259
|
+
var renderSection = (label, summary) => {
|
|
11260
|
+
if (!summary) {
|
|
11261
|
+
return "";
|
|
11262
|
+
}
|
|
11263
|
+
return `<article class="${escapeHtml20(summary.status)}"><span>${escapeHtml20(label)}</span><strong>${escapeHtml20(summary.status)}</strong><p>${summary.passed}/${summary.total} passed, ${summary.failed} failed.</p></article>`;
|
|
11264
|
+
};
|
|
11265
|
+
var renderAction = (action) => {
|
|
11266
|
+
const content = `<strong>${escapeHtml20(action.label)}</strong><p>${escapeHtml20(action.description)}</p><span>${escapeHtml20(action.section)} / ${escapeHtml20(action.severity)}</span>`;
|
|
11267
|
+
return action.href ? `<a class="action" href="${escapeHtml20(action.href)}">${content}</a>` : `<article class="action">${content}</article>`;
|
|
11268
|
+
};
|
|
11269
|
+
var renderVoiceSimulationSuiteHTML = (report, options = {}) => {
|
|
11270
|
+
const title = options.title ?? "Voice Simulation Suite";
|
|
11271
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml20(title)}</title><style>body{background:#10151c;color:#f8f3e7;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1080px;padding:32px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.18),rgba(59,130,246,.12));border:1px solid #283544;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#93c5fd;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.badge{border:1px solid #3f3f46;border-radius:999px;display:inline-flex;padding:8px 12px}.pass{color:#86efac}.fail{color:#fca5a5}.grid,.actions{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));margin:18px 0}.grid article,.action{background:#151d27;border:1px solid #283544;border-radius:18px;color:inherit;padding:16px;text-decoration:none}.grid span,.action span{color:#aab5c0}.grid strong{display:block;font-size:2rem;margin:.25rem 0;text-transform:uppercase}.action strong{display:block;color:#f8f3e7;margin-bottom:.35rem}.action p{color:#d8dee6;margin:.3rem 0 .6rem}pre{background:#151d27;border:1px solid #283544;border-radius:18px;overflow:auto;padding:16px}</style></head><body><main><section class="hero"><p class="eyebrow">Pre-production proof</p><h1>${escapeHtml20(title)}</h1><p>One report for session quality, scenario evals, fixture simulations, tool contracts, and outcome contracts.</p><p class="badge ${escapeHtml20(report.status)}">Status: ${escapeHtml20(report.status)}</p><section class="grid">${renderSection("Sessions", report.summary.sessions)}${renderSection("Scenarios", report.summary.scenarios)}${renderSection("Fixtures", report.summary.fixtures)}${renderSection("Tools", report.summary.tools)}${renderSection("Outcomes", report.summary.outcomes)}</section></section><h2>Actions</h2><section class="actions">${report.actions.length > 0 ? report.actions.map(renderAction).join("") : '<article class="action"><strong>No action required</strong><p>All enabled simulation sections are passing.</p></article>'}</section><pre>${escapeHtml20(JSON.stringify({ summary: report.summary, actions: report.actions }, null, 2))}</pre></main></body></html>`;
|
|
11272
|
+
};
|
|
11273
|
+
var createVoiceSimulationSuiteRoutes = (options) => {
|
|
11274
|
+
const path = options.path ?? "/api/voice/simulations";
|
|
11275
|
+
const htmlPath = options.htmlPath === undefined ? "/voice/simulations" : options.htmlPath;
|
|
11276
|
+
const app = new Elysia19({
|
|
11277
|
+
name: options.name ?? "absolutejs-voice-simulation-suite"
|
|
11278
|
+
}).get(path, () => runVoiceSimulationSuite(options));
|
|
11279
|
+
if (htmlPath) {
|
|
11280
|
+
app.get(htmlPath, async () => {
|
|
11281
|
+
const report = await runVoiceSimulationSuite(options);
|
|
11282
|
+
const html = options.render ? await options.render(report) : renderVoiceSimulationSuiteHTML(report, options);
|
|
11283
|
+
return new Response(html, {
|
|
11284
|
+
headers: {
|
|
11285
|
+
"content-type": "text/html; charset=utf-8",
|
|
11286
|
+
...options.headers
|
|
11287
|
+
}
|
|
11288
|
+
});
|
|
11289
|
+
});
|
|
11290
|
+
}
|
|
11291
|
+
return app;
|
|
11292
|
+
};
|
|
11293
|
+
// src/workflowContract.ts
|
|
11294
|
+
var getObject2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
11295
|
+
var getPathValue2 = (value, path) => {
|
|
11296
|
+
let current = value;
|
|
11297
|
+
for (const part of path.split(".").filter(Boolean)) {
|
|
11298
|
+
const record = getObject2(current);
|
|
11299
|
+
if (!record || !(part in record)) {
|
|
11300
|
+
return;
|
|
11301
|
+
}
|
|
11302
|
+
current = record[part];
|
|
11303
|
+
}
|
|
11304
|
+
return current;
|
|
11305
|
+
};
|
|
11306
|
+
var hasValue = (value, match) => {
|
|
11307
|
+
switch (match) {
|
|
11308
|
+
case "boolean":
|
|
11309
|
+
return typeof value === "boolean";
|
|
11310
|
+
case "number":
|
|
11311
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
11312
|
+
case "string":
|
|
11313
|
+
return typeof value === "string";
|
|
11314
|
+
case "truthy":
|
|
11315
|
+
return Boolean(value);
|
|
11316
|
+
case "non-empty":
|
|
11317
|
+
default:
|
|
11318
|
+
return Array.isArray(value) ? value.length > 0 : typeof value === "string" ? value.trim().length > 0 : value !== undefined && value !== null;
|
|
11319
|
+
}
|
|
11320
|
+
};
|
|
11321
|
+
var resolveOutcome2 = (routeResult) => {
|
|
11322
|
+
if (routeResult.complete)
|
|
11323
|
+
return "complete";
|
|
11324
|
+
if (routeResult.transfer)
|
|
11325
|
+
return "transfer";
|
|
11326
|
+
if (routeResult.escalate)
|
|
11327
|
+
return "escalate";
|
|
11328
|
+
if (routeResult.voicemail)
|
|
11329
|
+
return "voicemail";
|
|
11330
|
+
if (routeResult.noAnswer)
|
|
11331
|
+
return "no-answer";
|
|
11332
|
+
return;
|
|
11333
|
+
};
|
|
11334
|
+
var validateVoiceWorkflowRouteResult = (definition, routeResult) => {
|
|
11335
|
+
const issues = [];
|
|
11336
|
+
const requiredFields = (definition.fields ?? []).filter((field) => field.required !== false).map((field) => field.path);
|
|
11337
|
+
const missingFields = [];
|
|
11338
|
+
const outcome = resolveOutcome2(routeResult);
|
|
11339
|
+
if (definition.outcome && outcome !== definition.outcome) {
|
|
11340
|
+
issues.push({
|
|
11341
|
+
code: "workflow.outcome_mismatch",
|
|
11342
|
+
message: `Expected workflow outcome ${definition.outcome}, saw ${outcome ?? "none"}.`
|
|
11343
|
+
});
|
|
11344
|
+
}
|
|
11345
|
+
for (const field of definition.fields ?? []) {
|
|
11346
|
+
if (field.required === false)
|
|
11347
|
+
continue;
|
|
11348
|
+
const paths = [field.path, ...field.aliases ?? []];
|
|
11349
|
+
const present = paths.some((path) => hasValue(getPathValue2(routeResult.result, path), field.match ?? "non-empty"));
|
|
11350
|
+
if (!present) {
|
|
11351
|
+
missingFields.push(field.path);
|
|
11352
|
+
issues.push({
|
|
11353
|
+
code: "workflow.missing_field",
|
|
11354
|
+
field: field.path,
|
|
11355
|
+
message: `Missing required workflow field: ${field.label ?? field.path}.`
|
|
11356
|
+
});
|
|
11357
|
+
}
|
|
11358
|
+
}
|
|
11359
|
+
issues.push(...definition.validate?.({
|
|
11360
|
+
result: routeResult.result,
|
|
11361
|
+
routeResult
|
|
11362
|
+
}) ?? []);
|
|
11363
|
+
return {
|
|
11364
|
+
contractId: definition.id,
|
|
11365
|
+
issues,
|
|
11366
|
+
missingFields,
|
|
11367
|
+
outcome,
|
|
11368
|
+
pass: issues.length === 0,
|
|
11369
|
+
requiredFields
|
|
11370
|
+
};
|
|
11371
|
+
};
|
|
11372
|
+
var createVoiceWorkflowScenario = (definition, overrides = {}) => ({
|
|
11373
|
+
description: definition.description,
|
|
11374
|
+
forbiddenHandoffActions: definition.forbiddenHandoffActions,
|
|
11375
|
+
id: definition.id,
|
|
11376
|
+
label: definition.label,
|
|
11377
|
+
maxProviderErrors: definition.maxProviderErrors,
|
|
11378
|
+
maxSessionErrors: definition.maxSessionErrors,
|
|
11379
|
+
minSessions: definition.minSessions,
|
|
11380
|
+
minTurns: definition.minTurns,
|
|
11381
|
+
requiredAssistantIncludes: definition.requiredAssistantIncludes,
|
|
11382
|
+
requiredDisposition: definition.requiredDisposition,
|
|
11383
|
+
requiredHandoffActions: definition.requiredHandoffActions,
|
|
11384
|
+
requiredLifecycleTypes: definition.requiredLifecycleTypes,
|
|
11385
|
+
requiredTranscriptIncludes: definition.requiredTranscriptIncludes,
|
|
11386
|
+
requiredWorkflowContracts: [definition.id],
|
|
11387
|
+
scenarioId: definition.scenarioId,
|
|
11388
|
+
...overrides
|
|
11389
|
+
});
|
|
11390
|
+
var createVoiceWorkflowContract = (definition) => ({
|
|
11391
|
+
assertRouteResult: (routeResult) => {
|
|
11392
|
+
const validation = validateVoiceWorkflowRouteResult(definition, routeResult);
|
|
11393
|
+
if (!validation.pass) {
|
|
11394
|
+
throw new Error(`Voice workflow contract ${definition.id} failed: ${validation.issues.map((issue) => issue.message).join(" ")}`);
|
|
11395
|
+
}
|
|
11396
|
+
},
|
|
11397
|
+
definition,
|
|
11398
|
+
toScenarioEval: (overrides) => createVoiceWorkflowScenario(definition, overrides),
|
|
11399
|
+
validateRouteResult: (routeResult) => validateVoiceWorkflowRouteResult(definition, routeResult)
|
|
11400
|
+
});
|
|
11401
|
+
var presetDefinitions = {
|
|
11402
|
+
"appointment-booking": {
|
|
11403
|
+
description: "Appointment booking should complete with enough identity, appointment, and follow-up details to act on.",
|
|
11404
|
+
fields: [
|
|
11405
|
+
{ aliases: ["name", "customer.name"], label: "Caller name", path: "caller.name" },
|
|
11406
|
+
{
|
|
11407
|
+
aliases: ["phone", "customer.phone"],
|
|
11408
|
+
label: "Caller phone",
|
|
11409
|
+
path: "caller.phone"
|
|
11410
|
+
},
|
|
11411
|
+
{
|
|
11412
|
+
aliases: ["appointment.start", "appointment.time", "scheduledAt"],
|
|
11413
|
+
label: "Appointment time",
|
|
11414
|
+
path: "appointment.startsAt"
|
|
11415
|
+
},
|
|
11416
|
+
{
|
|
11417
|
+
aliases: ["summary", "assistantSummary"],
|
|
11418
|
+
label: "Summary",
|
|
11419
|
+
path: "appointment.summary"
|
|
11420
|
+
}
|
|
11421
|
+
],
|
|
11422
|
+
id: "appointment-booking",
|
|
11423
|
+
label: "Appointment booking",
|
|
11424
|
+
outcome: "complete",
|
|
11425
|
+
requiredDisposition: "completed"
|
|
11426
|
+
},
|
|
11427
|
+
"lead-qualification": {
|
|
11428
|
+
description: "Lead qualification should complete with contact, need, qualification, and next-step fields.",
|
|
11429
|
+
fields: [
|
|
11430
|
+
{ aliases: ["name", "lead.name"], label: "Lead name", path: "contact.name" },
|
|
11431
|
+
{
|
|
11432
|
+
aliases: ["email", "lead.email"],
|
|
11433
|
+
label: "Lead email",
|
|
11434
|
+
path: "contact.email"
|
|
11435
|
+
},
|
|
11436
|
+
{
|
|
11437
|
+
aliases: ["need", "pain", "summary"],
|
|
11438
|
+
label: "Need",
|
|
11439
|
+
path: "qualification.need"
|
|
11440
|
+
},
|
|
11441
|
+
{
|
|
11442
|
+
aliases: ["qualified", "qualification.qualified"],
|
|
11443
|
+
label: "Qualified",
|
|
11444
|
+
match: "boolean",
|
|
11445
|
+
path: "qualification.isQualified"
|
|
11446
|
+
},
|
|
11447
|
+
{
|
|
11448
|
+
aliases: ["nextStep", "followUp"],
|
|
11449
|
+
label: "Next step",
|
|
11450
|
+
path: "qualification.nextStep"
|
|
11451
|
+
}
|
|
11452
|
+
],
|
|
11453
|
+
id: "lead-qualification",
|
|
11454
|
+
label: "Lead qualification",
|
|
11455
|
+
outcome: "complete",
|
|
11456
|
+
requiredDisposition: "completed"
|
|
11457
|
+
},
|
|
11458
|
+
"support-triage": {
|
|
11459
|
+
description: "Support triage should capture identity, issue summary, severity, and the operational follow-up.",
|
|
11460
|
+
fields: [
|
|
11461
|
+
{
|
|
11462
|
+
aliases: ["name", "customer.name"],
|
|
11463
|
+
label: "Customer name",
|
|
11464
|
+
path: "customer.name"
|
|
11465
|
+
},
|
|
11466
|
+
{
|
|
11467
|
+
aliases: ["issue", "summary", "assistantSummary"],
|
|
11468
|
+
label: "Issue summary",
|
|
11469
|
+
path: "issue.summary"
|
|
11470
|
+
},
|
|
11471
|
+
{
|
|
11472
|
+
aliases: ["priority", "severity"],
|
|
11473
|
+
label: "Severity",
|
|
11474
|
+
path: "issue.severity"
|
|
11475
|
+
},
|
|
11476
|
+
{
|
|
11477
|
+
aliases: ["nextStep", "task.title"],
|
|
11478
|
+
label: "Next step",
|
|
11479
|
+
path: "resolution.nextStep"
|
|
11480
|
+
}
|
|
11481
|
+
],
|
|
11482
|
+
id: "support-triage",
|
|
11483
|
+
label: "Support triage",
|
|
11484
|
+
outcome: "complete",
|
|
11485
|
+
requiredDisposition: "completed"
|
|
11486
|
+
},
|
|
11487
|
+
"transfer-handoff": {
|
|
11488
|
+
description: "Transfer handoff should produce a routed transfer plus handoff evidence.",
|
|
11489
|
+
fields: [
|
|
11490
|
+
{
|
|
11491
|
+
aliases: ["target", "callTarget"],
|
|
11492
|
+
label: "Transfer target",
|
|
11493
|
+
path: "transfer.target"
|
|
11494
|
+
},
|
|
11495
|
+
{
|
|
11496
|
+
aliases: ["reason", "callReason"],
|
|
11497
|
+
label: "Transfer reason",
|
|
11498
|
+
path: "transfer.reason"
|
|
11499
|
+
},
|
|
11500
|
+
{
|
|
11501
|
+
aliases: ["summary", "assistantSummary"],
|
|
11502
|
+
label: "Transfer summary",
|
|
11503
|
+
path: "transfer.summary"
|
|
11504
|
+
}
|
|
11505
|
+
],
|
|
11506
|
+
id: "transfer-handoff",
|
|
11507
|
+
label: "Transfer handoff",
|
|
11508
|
+
outcome: "transfer",
|
|
11509
|
+
requiredDisposition: "transferred",
|
|
11510
|
+
requiredHandoffActions: ["transfer"]
|
|
11511
|
+
},
|
|
11512
|
+
"voicemail-callback": {
|
|
11513
|
+
description: "Voicemail callback should preserve enough caller and callback context for follow-up.",
|
|
11514
|
+
fields: [
|
|
11515
|
+
{
|
|
11516
|
+
aliases: ["name", "caller.name"],
|
|
11517
|
+
label: "Caller name",
|
|
11518
|
+
path: "voicemail.callerName"
|
|
11519
|
+
},
|
|
11520
|
+
{
|
|
11521
|
+
aliases: ["phone", "caller.phone"],
|
|
11522
|
+
label: "Callback phone",
|
|
11523
|
+
path: "voicemail.callbackPhone"
|
|
11524
|
+
},
|
|
11525
|
+
{
|
|
11526
|
+
aliases: ["message", "summary", "assistantSummary"],
|
|
11527
|
+
label: "Voicemail summary",
|
|
11528
|
+
path: "voicemail.summary"
|
|
11529
|
+
}
|
|
11530
|
+
],
|
|
11531
|
+
id: "voicemail-callback",
|
|
11532
|
+
label: "Voicemail callback",
|
|
11533
|
+
outcome: "voicemail",
|
|
11534
|
+
requiredDisposition: "voicemail",
|
|
11535
|
+
requiredHandoffActions: ["voicemail"]
|
|
11536
|
+
}
|
|
11537
|
+
};
|
|
11538
|
+
var createVoiceWorkflowContractPreset = (name, options = {}) => {
|
|
11539
|
+
const preset = presetDefinitions[name];
|
|
11540
|
+
return createVoiceWorkflowContract({
|
|
11541
|
+
...preset,
|
|
11542
|
+
...options,
|
|
11543
|
+
fields: options.fields ?? preset.fields,
|
|
11544
|
+
id: options.id ?? preset.id
|
|
11545
|
+
});
|
|
11546
|
+
};
|
|
11547
|
+
var recordVoiceWorkflowContractTrace = async (input) => input.store.append({
|
|
11548
|
+
at: input.at ?? Date.now(),
|
|
11549
|
+
payload: {
|
|
11550
|
+
contractId: input.contractId ?? input.validation.contractId,
|
|
11551
|
+
issues: input.validation.issues,
|
|
11552
|
+
missingFields: input.validation.missingFields,
|
|
11553
|
+
outcome: input.validation.outcome,
|
|
11554
|
+
requiredFields: input.validation.requiredFields,
|
|
11555
|
+
status: input.validation.pass ? "pass" : "fail"
|
|
11556
|
+
},
|
|
11557
|
+
scenarioId: input.scenarioId,
|
|
11558
|
+
sessionId: input.sessionId,
|
|
11559
|
+
traceId: input.traceId,
|
|
11560
|
+
turnId: input.turnId,
|
|
11561
|
+
type: "workflow.contract"
|
|
11562
|
+
});
|
|
11563
|
+
var createVoiceWorkflowContractHandler = (input) => {
|
|
11564
|
+
return async (session, turn, api, context) => {
|
|
11565
|
+
const legacyHandler = input.handler;
|
|
11566
|
+
const objectHandler = input.handler;
|
|
11567
|
+
const result = input.handler.length >= 4 ? await legacyHandler(session, turn, api, context) : await objectHandler({ api, context, session, turn });
|
|
11568
|
+
if (!result)
|
|
11569
|
+
return result;
|
|
11570
|
+
const resolved = input.resolveContract?.({ context, result, session, turn }) ?? input.contract;
|
|
11571
|
+
if (!resolved)
|
|
11572
|
+
return result;
|
|
11573
|
+
const contract = "validateRouteResult" in resolved ? resolved : createVoiceWorkflowContract(resolved);
|
|
11574
|
+
const validation = contract.validateRouteResult(result);
|
|
11575
|
+
if (input.store) {
|
|
11576
|
+
await recordVoiceWorkflowContractTrace({
|
|
11577
|
+
scenarioId: session.scenarioId,
|
|
11578
|
+
sessionId: session.id,
|
|
11579
|
+
store: input.store,
|
|
11580
|
+
turnId: turn.id,
|
|
11581
|
+
validation
|
|
11582
|
+
});
|
|
11583
|
+
}
|
|
11584
|
+
return result;
|
|
11585
|
+
};
|
|
11262
11586
|
};
|
|
11263
11587
|
// src/turnLatency.ts
|
|
11264
|
-
import { Elysia as
|
|
11588
|
+
import { Elysia as Elysia20 } from "elysia";
|
|
11265
11589
|
var DEFAULT_WARN_AFTER_MS = 1800;
|
|
11266
11590
|
var DEFAULT_FAIL_AFTER_MS = 3200;
|
|
11267
|
-
var
|
|
11591
|
+
var escapeHtml21 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11268
11592
|
var firstNumber2 = (values) => values.filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
|
|
11269
11593
|
var getString10 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
11270
11594
|
var createTraceStageIndex = (events) => {
|
|
@@ -11378,11 +11702,11 @@ var summarizeVoiceTurnLatency = async (options) => {
|
|
|
11378
11702
|
var formatMs2 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
|
|
11379
11703
|
var renderVoiceTurnLatencyHTML = (report, options = {}) => {
|
|
11380
11704
|
const title = options.title ?? "Voice Turn Latency";
|
|
11381
|
-
const turns = report.turns.map((turn) => `<article class="turn ${
|
|
11382
|
-
<header><div><p class="eyebrow">${
|
|
11383
|
-
<dl>${turn.stages.map((stage) => `<div><dt>${
|
|
11705
|
+
const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml21(turn.status)}">
|
|
11706
|
+
<header><div><p class="eyebrow">${escapeHtml21(turn.sessionId)} \xB7 ${escapeHtml21(turn.turnId)}</p><h2>${escapeHtml21(turn.text || "Empty turn")}</h2></div><strong>${escapeHtml21(turn.status)}</strong></header>
|
|
11707
|
+
<dl>${turn.stages.map((stage) => `<div><dt>${escapeHtml21(stage.label)}</dt><dd>${escapeHtml21(formatMs2(stage.valueMs))}</dd></div>`).join("")}</dl>
|
|
11384
11708
|
</article>`).join("");
|
|
11385
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
11709
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml21(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(251,191,36,.1))}.eyebrow{color:#5eead4;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.empty{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{font-weight:900;margin:0}@media(max-width:800px){main{padding:18px}.turn header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">End-to-end responsiveness</p><h1>${escapeHtml21(title)}</h1><div class="summary"><span class="pill ${escapeHtml21(report.status)}">${escapeHtml21(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">avg ${escapeHtml21(formatMs2(report.averageTotalMs))}</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
|
|
11386
11710
|
};
|
|
11387
11711
|
var createVoiceTurnLatencyJSONHandler = (options) => async () => summarizeVoiceTurnLatency(options);
|
|
11388
11712
|
var createVoiceTurnLatencyHTMLHandler = (options) => async () => {
|
|
@@ -11399,7 +11723,7 @@ var createVoiceTurnLatencyHTMLHandler = (options) => async () => {
|
|
|
11399
11723
|
var createVoiceTurnLatencyRoutes = (options) => {
|
|
11400
11724
|
const path = options.path ?? "/api/turn-latency";
|
|
11401
11725
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
11402
|
-
const routes = new
|
|
11726
|
+
const routes = new Elysia20({
|
|
11403
11727
|
name: options.name ?? "absolutejs-voice-turn-latency"
|
|
11404
11728
|
}).get(path, createVoiceTurnLatencyJSONHandler(options));
|
|
11405
11729
|
if (htmlPath) {
|
|
@@ -11408,8 +11732,8 @@ var createVoiceTurnLatencyRoutes = (options) => {
|
|
|
11408
11732
|
return routes;
|
|
11409
11733
|
};
|
|
11410
11734
|
// src/liveLatency.ts
|
|
11411
|
-
import { Elysia as
|
|
11412
|
-
var
|
|
11735
|
+
import { Elysia as Elysia21 } from "elysia";
|
|
11736
|
+
var escapeHtml22 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11413
11737
|
var percentile = (values, percentileValue) => {
|
|
11414
11738
|
if (values.length === 0) {
|
|
11415
11739
|
return;
|
|
@@ -11457,13 +11781,13 @@ var summarizeVoiceLiveLatency = async (options) => {
|
|
|
11457
11781
|
var formatMs3 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
|
|
11458
11782
|
var renderVoiceLiveLatencyHTML = (report, options = {}) => {
|
|
11459
11783
|
const title = options.title ?? "Voice Live Latency";
|
|
11460
|
-
const rows = report.recent.map((sample) => `<tr><td>${
|
|
11461
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
11784
|
+
const rows = report.recent.map((sample) => `<tr><td>${escapeHtml22(sample.sessionId)}</td><td>${escapeHtml22(formatMs3(sample.latencyMs))}</td><td>${escapeHtml22(sample.status ?? "unknown")}</td><td>${escapeHtml22(new Date(sample.at).toLocaleString())}</td></tr>`).join("");
|
|
11785
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml22(title)}</title><style>body{background:#0c0f14;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1060px;padding:32px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(245,158,11,.1));border:1px solid #26313d;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #3f3f46;border-radius:999px;display:inline-flex;padding:8px 12px}.pass{color:#86efac}.warn,.empty{color:#fbbf24}.fail{color:#fca5a5}.metrics{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:18px 0}.metrics article,table{background:#141922;border:1px solid #26313d;border-radius:18px}.metrics article{padding:16px}.metrics span{color:#a8b0b8}.metrics strong{display:block;font-size:2rem;margin-top:.25rem}table{border-collapse:collapse;overflow:hidden;width:100%}td,th{border-bottom:1px solid #26313d;padding:12px;text-align:left}@media(max-width:760px){main{padding:20px}}</style></head><body><main><section class="hero"><p class="eyebrow">Browser proof</p><h1>${escapeHtml22(title)}</h1><p>Recent real browser speech-to-assistant response measurements from persisted <code>client.live_latency</code> traces.</p><p class="status ${escapeHtml22(report.status)}">Status: ${escapeHtml22(report.status)}</p><section class="metrics"><article><span>p50</span><strong>${escapeHtml22(formatMs3(report.p50LatencyMs))}</strong></article><article><span>p95</span><strong>${escapeHtml22(formatMs3(report.p95LatencyMs))}</strong></article><article><span>Average</span><strong>${escapeHtml22(formatMs3(report.averageLatencyMs))}</strong></article><article><span>Samples</span><strong>${String(report.total)}</strong></article></section></section><table><thead><tr><th>Session</th><th>Latency</th><th>Status</th><th>Measured</th></tr></thead><tbody>${rows || '<tr><td colspan="4">No live latency samples yet.</td></tr>'}</tbody></table></main></body></html>`;
|
|
11462
11786
|
};
|
|
11463
11787
|
var createVoiceLiveLatencyRoutes = (options) => {
|
|
11464
11788
|
const path = options.path ?? "/api/live-latency";
|
|
11465
11789
|
const htmlPath = options.htmlPath === undefined ? "/live-latency" : options.htmlPath;
|
|
11466
|
-
const routes = new
|
|
11790
|
+
const routes = new Elysia21({
|
|
11467
11791
|
name: options.name ?? "absolutejs-voice-live-latency"
|
|
11468
11792
|
}).get(path, () => summarizeVoiceLiveLatency(options));
|
|
11469
11793
|
if (htmlPath) {
|
|
@@ -11480,9 +11804,9 @@ var createVoiceLiveLatencyRoutes = (options) => {
|
|
|
11480
11804
|
return routes;
|
|
11481
11805
|
};
|
|
11482
11806
|
// src/turnQuality.ts
|
|
11483
|
-
import { Elysia as
|
|
11807
|
+
import { Elysia as Elysia22 } from "elysia";
|
|
11484
11808
|
var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
|
|
11485
|
-
var
|
|
11809
|
+
var escapeHtml23 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11486
11810
|
var getTurnLatencyMs = (turn) => {
|
|
11487
11811
|
const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
|
|
11488
11812
|
if (firstTranscriptAt === undefined) {
|
|
@@ -11553,24 +11877,24 @@ var summarizeVoiceTurnQuality = async (options) => {
|
|
|
11553
11877
|
};
|
|
11554
11878
|
var renderVoiceTurnQualityHTML = (report, options = {}) => {
|
|
11555
11879
|
const title = options.title ?? "Voice Turn Quality";
|
|
11556
|
-
const turns = report.turns.map((turn) => `<article class="turn ${
|
|
11880
|
+
const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml23(turn.status)}">
|
|
11557
11881
|
<div class="turn-header">
|
|
11558
11882
|
<div>
|
|
11559
|
-
<p class="eyebrow">${
|
|
11560
|
-
<h2>${
|
|
11883
|
+
<p class="eyebrow">${escapeHtml23(turn.sessionId)} \xB7 ${escapeHtml23(turn.turnId)}</p>
|
|
11884
|
+
<h2>${escapeHtml23(turn.text || "Empty turn")}</h2>
|
|
11561
11885
|
</div>
|
|
11562
|
-
<strong>${
|
|
11886
|
+
<strong>${escapeHtml23(turn.status)}</strong>
|
|
11563
11887
|
</div>
|
|
11564
11888
|
<dl>
|
|
11565
|
-
<div><dt>Source</dt><dd>${
|
|
11889
|
+
<div><dt>Source</dt><dd>${escapeHtml23(turn.source ?? "unknown")}</dd></div>
|
|
11566
11890
|
<div><dt>Confidence</dt><dd>${turn.averageConfidence === undefined ? "n/a" : `${Math.round(turn.averageConfidence * 100)}%`}</dd></div>
|
|
11567
|
-
<div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${
|
|
11568
|
-
<div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${
|
|
11891
|
+
<div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml23(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
|
|
11892
|
+
<div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml23(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
|
|
11569
11893
|
<div><dt>Transcripts</dt><dd>${String(turn.selectedTranscriptCount)} selected \xB7 ${String(turn.finalTranscriptCount)} final \xB7 ${String(turn.partialTranscriptCount)} partial</dd></div>
|
|
11570
11894
|
<div><dt>Cost</dt><dd>${turn.costUnits === undefined ? "n/a" : String(turn.costUnits)}</dd></div>
|
|
11571
11895
|
</dl>
|
|
11572
11896
|
</article>`).join("");
|
|
11573
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
11897
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml23(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(251,191,36,.16),rgba(34,197,94,.1))}.eyebrow{color:#fbbf24;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.unknown{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{margin:0}@media(max-width:800px){main{padding:18px}.turn-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Realtime STT Debugging</p><h1>${escapeHtml23(title)}</h1><div class="summary"><span class="pill ${escapeHtml23(report.status)}">${escapeHtml23(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span><span class="pill">${String(report.sessions)} sessions</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
|
|
11574
11898
|
};
|
|
11575
11899
|
var createVoiceTurnQualityJSONHandler = (options) => async () => summarizeVoiceTurnQuality(options);
|
|
11576
11900
|
var createVoiceTurnQualityHTMLHandler = (options) => async () => {
|
|
@@ -11587,7 +11911,7 @@ var createVoiceTurnQualityHTMLHandler = (options) => async () => {
|
|
|
11587
11911
|
var createVoiceTurnQualityRoutes = (options) => {
|
|
11588
11912
|
const path = options.path ?? "/api/turn-quality";
|
|
11589
11913
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
11590
|
-
const routes = new
|
|
11914
|
+
const routes = new Elysia22({
|
|
11591
11915
|
name: options.name ?? "absolutejs-voice-turn-quality"
|
|
11592
11916
|
}).get(path, createVoiceTurnQualityJSONHandler(options));
|
|
11593
11917
|
if (htmlPath) {
|
|
@@ -11595,157 +11919,8 @@ var createVoiceTurnQualityRoutes = (options) => {
|
|
|
11595
11919
|
}
|
|
11596
11920
|
return routes;
|
|
11597
11921
|
};
|
|
11598
|
-
// src/outcomeContract.ts
|
|
11599
|
-
import { Elysia as Elysia21 } from "elysia";
|
|
11600
|
-
var escapeHtml22 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11601
|
-
var getPayloadString = (event, key) => typeof event.payload[key] === "string" ? event.payload[key] : undefined;
|
|
11602
|
-
var toList = async (input) => Array.isArray(input) ? input : await input?.list() ?? [];
|
|
11603
|
-
var hydrateSessions = async (input) => {
|
|
11604
|
-
if (!input)
|
|
11605
|
-
return [];
|
|
11606
|
-
if (Array.isArray(input))
|
|
11607
|
-
return input;
|
|
11608
|
-
const summaries = await input.list();
|
|
11609
|
-
const sessions = await Promise.all(summaries.map((summary) => input.get(summary.id)));
|
|
11610
|
-
const hydrated = [];
|
|
11611
|
-
for (const session of sessions) {
|
|
11612
|
-
if (session) {
|
|
11613
|
-
hydrated.push(session);
|
|
11614
|
-
}
|
|
11615
|
-
}
|
|
11616
|
-
return hydrated;
|
|
11617
|
-
};
|
|
11618
|
-
var dispositionForSession = (session) => session.call?.disposition ?? (session.status === "completed" ? "completed" : undefined);
|
|
11619
|
-
var matchesDisposition = (disposition, expected) => expected === undefined || disposition === expected;
|
|
11620
|
-
var reportContract = (input) => {
|
|
11621
|
-
const { contract } = input;
|
|
11622
|
-
const sessions = input.sessions.filter((session) => (!contract.scenarioId || session.scenarioId === contract.scenarioId) && matchesDisposition(dispositionForSession(session), contract.expectedDisposition));
|
|
11623
|
-
const sessionIds = new Set(sessions.map((session) => session.id));
|
|
11624
|
-
const reviews = input.reviews.filter((review) => matchesDisposition(review.summary.outcome, contract.expectedDisposition));
|
|
11625
|
-
const tasks = input.tasks.filter((task) => matchesDisposition(task.outcome, contract.expectedDisposition));
|
|
11626
|
-
const handoffs = input.handoffs.filter((handoff) => (!contract.expectedDisposition || handoff.action === contract.expectedDisposition || contract.expectedDisposition === "transferred" && handoff.action === "transfer" || contract.expectedDisposition === "escalated" && handoff.action === "escalate") && (sessionIds.size === 0 || sessionIds.has(handoff.sessionId)));
|
|
11627
|
-
const events = input.events.filter((event) => {
|
|
11628
|
-
const eventSessionId = getPayloadString(event, "sessionId");
|
|
11629
|
-
const eventOutcome = getPayloadString(event, "outcome") ?? getPayloadString(event, "disposition");
|
|
11630
|
-
return (sessionIds.size === 0 || !eventSessionId || sessionIds.has(eventSessionId)) && (!contract.expectedDisposition || eventOutcome === contract.expectedDisposition);
|
|
11631
|
-
});
|
|
11632
|
-
const issues = [];
|
|
11633
|
-
const minSessions = contract.minSessions ?? 1;
|
|
11634
|
-
if (sessions.length < minSessions) {
|
|
11635
|
-
issues.push({
|
|
11636
|
-
code: "outcome.sessions_missing",
|
|
11637
|
-
message: `Expected at least ${minSessions} matching session(s), saw ${sessions.length}.`
|
|
11638
|
-
});
|
|
11639
|
-
}
|
|
11640
|
-
if (contract.requireReview !== false && reviews.length === 0) {
|
|
11641
|
-
issues.push({
|
|
11642
|
-
code: "outcome.review_missing",
|
|
11643
|
-
message: "Expected at least one matching review artifact."
|
|
11644
|
-
});
|
|
11645
|
-
}
|
|
11646
|
-
if (contract.requireTask && tasks.length < (contract.minTasks ?? 1)) {
|
|
11647
|
-
issues.push({
|
|
11648
|
-
code: "outcome.task_missing",
|
|
11649
|
-
message: `Expected at least ${contract.minTasks ?? 1} matching task(s), saw ${tasks.length}.`
|
|
11650
|
-
});
|
|
11651
|
-
}
|
|
11652
|
-
for (const action of contract.requireHandoffActions ?? []) {
|
|
11653
|
-
if (!handoffs.some((handoff) => handoff.action === action)) {
|
|
11654
|
-
issues.push({
|
|
11655
|
-
code: "outcome.handoff_missing",
|
|
11656
|
-
message: `Expected handoff action ${action}.`
|
|
11657
|
-
});
|
|
11658
|
-
}
|
|
11659
|
-
}
|
|
11660
|
-
for (const type of contract.requireIntegrationEvents ?? []) {
|
|
11661
|
-
if (!events.some((event) => event.type === type)) {
|
|
11662
|
-
issues.push({
|
|
11663
|
-
code: "outcome.integration_event_missing",
|
|
11664
|
-
message: `Expected integration event ${type}.`
|
|
11665
|
-
});
|
|
11666
|
-
}
|
|
11667
|
-
}
|
|
11668
|
-
return {
|
|
11669
|
-
contractId: contract.id,
|
|
11670
|
-
description: contract.description,
|
|
11671
|
-
issues,
|
|
11672
|
-
label: contract.label,
|
|
11673
|
-
matched: {
|
|
11674
|
-
handoffs: handoffs.length,
|
|
11675
|
-
integrationEvents: events.length,
|
|
11676
|
-
reviews: reviews.length,
|
|
11677
|
-
sessions: sessions.length,
|
|
11678
|
-
tasks: tasks.length
|
|
11679
|
-
},
|
|
11680
|
-
pass: issues.length === 0
|
|
11681
|
-
};
|
|
11682
|
-
};
|
|
11683
|
-
var runVoiceOutcomeContractSuite = async (options) => {
|
|
11684
|
-
const [sessions, reviews, tasks, events, handoffs] = await Promise.all([
|
|
11685
|
-
hydrateSessions(options.sessions),
|
|
11686
|
-
toList(options.reviews),
|
|
11687
|
-
toList(options.tasks),
|
|
11688
|
-
toList(options.events),
|
|
11689
|
-
toList(options.handoffs)
|
|
11690
|
-
]);
|
|
11691
|
-
const contracts = options.contracts.map((contract) => reportContract({ contract, events, handoffs, reviews, sessions, tasks }));
|
|
11692
|
-
const passed = contracts.filter((contract) => contract.pass).length;
|
|
11693
|
-
const failed = contracts.length - passed;
|
|
11694
|
-
return {
|
|
11695
|
-
checkedAt: Date.now(),
|
|
11696
|
-
contracts,
|
|
11697
|
-
failed,
|
|
11698
|
-
passed,
|
|
11699
|
-
status: failed > 0 ? "fail" : "pass",
|
|
11700
|
-
total: contracts.length
|
|
11701
|
-
};
|
|
11702
|
-
};
|
|
11703
|
-
var renderVoiceOutcomeContractHTML = (report, options = {}) => {
|
|
11704
|
-
const title = options.title ?? "Voice Outcome Contracts";
|
|
11705
|
-
const contracts = report.contracts.map((contract) => `<section class="contract ${contract.pass ? "pass" : "fail"}">
|
|
11706
|
-
<div class="contract-header">
|
|
11707
|
-
<div>
|
|
11708
|
-
<p class="eyebrow">${escapeHtml22(contract.contractId)}</p>
|
|
11709
|
-
<h2>${escapeHtml22(contract.label ?? contract.contractId)}</h2>
|
|
11710
|
-
${contract.description ? `<p>${escapeHtml22(contract.description)}</p>` : ""}
|
|
11711
|
-
</div>
|
|
11712
|
-
<strong>${contract.pass ? "pass" : "fail"}</strong>
|
|
11713
|
-
</div>
|
|
11714
|
-
<div class="grid">
|
|
11715
|
-
<span>sessions ${String(contract.matched.sessions)}</span>
|
|
11716
|
-
<span>reviews ${String(contract.matched.reviews)}</span>
|
|
11717
|
-
<span>tasks ${String(contract.matched.tasks)}</span>
|
|
11718
|
-
<span>handoffs ${String(contract.matched.handoffs)}</span>
|
|
11719
|
-
<span>events ${String(contract.matched.integrationEvents)}</span>
|
|
11720
|
-
</div>
|
|
11721
|
-
${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml22(issue.message)}</li>`).join("")}</ul>` : ""}
|
|
11722
|
-
</section>`).join("");
|
|
11723
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml22(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.contract{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.14),rgba(14,165,233,.12))}.eyebrow{color:#7dd3fc;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0}.summary,.grid{display:flex;flex-wrap:wrap;gap:10px}.pill,.grid span{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}li{margin:8px 0}@media(max-width:800px){main{padding:18px}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Business Outcome Verification</p><h1>${escapeHtml22(title)}</h1><div class="summary"><span class="pill ${report.status}">${report.status}</span><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} contracts</span></div></section>${contracts || '<section class="contract"><p>No outcome contracts configured.</p></section>'}</main></body></html>`;
|
|
11724
|
-
};
|
|
11725
|
-
var createVoiceOutcomeContractJSONHandler = (options) => async () => runVoiceOutcomeContractSuite(options);
|
|
11726
|
-
var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
|
|
11727
|
-
const report = await runVoiceOutcomeContractSuite(options);
|
|
11728
|
-
const render = options.render ?? ((input) => renderVoiceOutcomeContractHTML(input, options));
|
|
11729
|
-
return new Response(await render(report), {
|
|
11730
|
-
headers: {
|
|
11731
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
11732
|
-
...options.headers
|
|
11733
|
-
}
|
|
11734
|
-
});
|
|
11735
|
-
};
|
|
11736
|
-
var createVoiceOutcomeContractRoutes = (options) => {
|
|
11737
|
-
const path = options.path ?? "/api/outcome-contracts";
|
|
11738
|
-
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
11739
|
-
const routes = new Elysia21({
|
|
11740
|
-
name: options.name ?? "absolutejs-voice-outcome-contracts"
|
|
11741
|
-
}).get(path, createVoiceOutcomeContractJSONHandler(options));
|
|
11742
|
-
if (htmlPath) {
|
|
11743
|
-
routes.get(htmlPath, createVoiceOutcomeContractHTMLHandler(options));
|
|
11744
|
-
}
|
|
11745
|
-
return routes;
|
|
11746
|
-
};
|
|
11747
11922
|
// src/telephonyOutcome.ts
|
|
11748
|
-
import { Elysia as
|
|
11923
|
+
import { Elysia as Elysia23 } from "elysia";
|
|
11749
11924
|
var DEFAULT_COMPLETED_STATUSES = [
|
|
11750
11925
|
"answered",
|
|
11751
11926
|
"completed",
|
|
@@ -12392,7 +12567,7 @@ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
|
|
|
12392
12567
|
var createVoiceTelephonyWebhookRoutes = (options = {}) => {
|
|
12393
12568
|
const path = options.path ?? "/api/voice/telephony/webhook";
|
|
12394
12569
|
const handler = createVoiceTelephonyWebhookHandler(options);
|
|
12395
|
-
return new
|
|
12570
|
+
return new Elysia23({
|
|
12396
12571
|
name: options.name ?? "absolutejs-voice-telephony-webhooks"
|
|
12397
12572
|
}).post(path, async ({ query, request }) => {
|
|
12398
12573
|
try {
|
|
@@ -12413,15 +12588,15 @@ var createVoiceTelephonyWebhookRoutes = (options = {}) => {
|
|
|
12413
12588
|
});
|
|
12414
12589
|
};
|
|
12415
12590
|
// src/phoneAgent.ts
|
|
12416
|
-
import { Elysia as
|
|
12591
|
+
import { Elysia as Elysia27 } from "elysia";
|
|
12417
12592
|
|
|
12418
12593
|
// src/telephony/plivo.ts
|
|
12419
12594
|
import { Buffer as Buffer4 } from "buffer";
|
|
12420
|
-
import { Elysia as
|
|
12595
|
+
import { Elysia as Elysia25 } from "elysia";
|
|
12421
12596
|
|
|
12422
12597
|
// src/telephony/twilio.ts
|
|
12423
12598
|
import { Buffer as Buffer3 } from "buffer";
|
|
12424
|
-
import { Elysia as
|
|
12599
|
+
import { Elysia as Elysia24 } from "elysia";
|
|
12425
12600
|
var TWILIO_MULAW_SAMPLE_RATE = 8000;
|
|
12426
12601
|
var VOICE_PCM_SAMPLE_RATE = 16000;
|
|
12427
12602
|
var escapeXml2 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
@@ -12451,7 +12626,7 @@ var resolveTwilioStreamParameters = async (parameters, input) => {
|
|
|
12451
12626
|
return parameters;
|
|
12452
12627
|
};
|
|
12453
12628
|
var joinUrlPath = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
12454
|
-
var
|
|
12629
|
+
var escapeHtml24 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
12455
12630
|
var getWebhookVerificationUrl = (webhook, input) => {
|
|
12456
12631
|
if (!webhook?.verificationUrl) {
|
|
12457
12632
|
return;
|
|
@@ -12494,23 +12669,23 @@ var buildTwilioVoiceSetupStatus = async (options, input) => {
|
|
|
12494
12669
|
};
|
|
12495
12670
|
var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
12496
12671
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
|
|
12497
|
-
<h1>${
|
|
12672
|
+
<h1>${escapeHtml24(title)}</h1>
|
|
12498
12673
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
12499
12674
|
<section>
|
|
12500
12675
|
<h2>URLs</h2>
|
|
12501
12676
|
<ul>
|
|
12502
|
-
<li><strong>TwiML:</strong> <code>${
|
|
12503
|
-
<li><strong>Media stream:</strong> <code>${
|
|
12504
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
12677
|
+
<li><strong>TwiML:</strong> <code>${escapeHtml24(status.urls.twiml)}</code></li>
|
|
12678
|
+
<li><strong>Media stream:</strong> <code>${escapeHtml24(status.urls.stream)}</code></li>
|
|
12679
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml24(status.urls.webhook)}</code></li>
|
|
12505
12680
|
</ul>
|
|
12506
12681
|
</section>
|
|
12507
12682
|
<section>
|
|
12508
12683
|
<h2>Signing</h2>
|
|
12509
12684
|
<p>Mode: <code>${status.signing.mode}</code></p>
|
|
12510
|
-
${status.signing.verificationUrl ? `<p>Verification URL: <code>${
|
|
12685
|
+
${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml24(status.signing.verificationUrl)}</code></p>` : ""}
|
|
12511
12686
|
</section>
|
|
12512
|
-
${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${
|
|
12513
|
-
${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
12687
|
+
${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml24(name)}</code></li>`).join("")}</ul></section>` : ""}
|
|
12688
|
+
${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml24(warning)}</li>`).join("")}</ul></section>` : ""}
|
|
12514
12689
|
</main>`;
|
|
12515
12690
|
var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&", "&");
|
|
12516
12691
|
var createSmokeCheck = (name, status, message, details) => ({
|
|
@@ -12521,20 +12696,20 @@ var createSmokeCheck = (name, status, message, details) => ({
|
|
|
12521
12696
|
});
|
|
12522
12697
|
var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
12523
12698
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
|
|
12524
|
-
<h1>${
|
|
12699
|
+
<h1>${escapeHtml24(title)}</h1>
|
|
12525
12700
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
12526
12701
|
<section>
|
|
12527
12702
|
<h2>Checks</h2>
|
|
12528
12703
|
<ul>
|
|
12529
|
-
${report.checks.map((check) => `<li><strong>${
|
|
12704
|
+
${report.checks.map((check) => `<li><strong>${escapeHtml24(check.name)}</strong>: ${escapeHtml24(check.status)}${check.message ? ` - ${escapeHtml24(check.message)}` : ""}</li>`).join("")}
|
|
12530
12705
|
</ul>
|
|
12531
12706
|
</section>
|
|
12532
12707
|
<section>
|
|
12533
12708
|
<h2>Observed URLs</h2>
|
|
12534
12709
|
<ul>
|
|
12535
|
-
<li><strong>TwiML:</strong> <code>${
|
|
12536
|
-
<li><strong>Stream:</strong> <code>${
|
|
12537
|
-
<li><strong>Webhook:</strong> <code>${
|
|
12710
|
+
<li><strong>TwiML:</strong> <code>${escapeHtml24(report.setup.urls.twiml)}</code></li>
|
|
12711
|
+
<li><strong>Stream:</strong> <code>${escapeHtml24(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
|
|
12712
|
+
<li><strong>Webhook:</strong> <code>${escapeHtml24(report.setup.urls.webhook)}</code></li>
|
|
12538
12713
|
</ul>
|
|
12539
12714
|
</section>
|
|
12540
12715
|
</main>`;
|
|
@@ -12994,7 +13169,7 @@ var createTwilioVoiceRoutes = (options) => {
|
|
|
12994
13169
|
const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
|
|
12995
13170
|
const bridges = new WeakMap;
|
|
12996
13171
|
const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
|
|
12997
|
-
const app = new
|
|
13172
|
+
const app = new Elysia24({
|
|
12998
13173
|
name: options.name ?? "absolutejs-voice-twilio"
|
|
12999
13174
|
}).get(twimlPath, async ({ query, request }) => {
|
|
13000
13175
|
const streamUrl = await resolveTwilioStreamUrl(options, {
|
|
@@ -13131,7 +13306,7 @@ var createTwilioVoiceRoutes = (options) => {
|
|
|
13131
13306
|
|
|
13132
13307
|
// src/telephony/plivo.ts
|
|
13133
13308
|
var escapeXml3 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
13134
|
-
var
|
|
13309
|
+
var escapeHtml25 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
13135
13310
|
var joinUrlPath2 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
13136
13311
|
var resolveRequestOrigin2 = (request) => {
|
|
13137
13312
|
const url = new URL(request.url);
|
|
@@ -13382,21 +13557,21 @@ var buildPlivoVoiceSetupStatus = async (options, input) => {
|
|
|
13382
13557
|
};
|
|
13383
13558
|
var renderPlivoSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
13384
13559
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo setup</p>
|
|
13385
|
-
<h1>${
|
|
13560
|
+
<h1>${escapeHtml25(title)}</h1>
|
|
13386
13561
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
13387
13562
|
<ul>
|
|
13388
|
-
<li><strong>Answer XML:</strong> <code>${
|
|
13389
|
-
<li><strong>Audio stream:</strong> <code>${
|
|
13390
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
13563
|
+
<li><strong>Answer XML:</strong> <code>${escapeHtml25(status.urls.answer)}</code></li>
|
|
13564
|
+
<li><strong>Audio stream:</strong> <code>${escapeHtml25(status.urls.stream)}</code></li>
|
|
13565
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml25(status.urls.webhook)}</code></li>
|
|
13391
13566
|
</ul>
|
|
13392
|
-
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${
|
|
13393
|
-
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
13567
|
+
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml25(name)}</code></li>`).join("")}</ul>` : ""}
|
|
13568
|
+
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml25(warning)}</li>`).join("")}</ul>` : ""}
|
|
13394
13569
|
</main>`;
|
|
13395
13570
|
var renderPlivoSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
13396
13571
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo smoke test</p>
|
|
13397
|
-
<h1>${
|
|
13572
|
+
<h1>${escapeHtml25(title)}</h1>
|
|
13398
13573
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
13399
|
-
<ul>${report.checks.map((check) => `<li><strong>${
|
|
13574
|
+
<ul>${report.checks.map((check) => `<li><strong>${escapeHtml25(check.name)}</strong>: ${escapeHtml25(check.status)}${check.message ? ` - ${escapeHtml25(check.message)}` : ""}</li>`).join("")}</ul>
|
|
13400
13575
|
</main>`;
|
|
13401
13576
|
var runPlivoSmokeTest = async (input) => {
|
|
13402
13577
|
const setup = await buildPlivoVoiceSetupStatus(input.options, input);
|
|
@@ -13491,7 +13666,7 @@ var createPlivoVoiceRoutes = (options = {}) => {
|
|
|
13491
13666
|
request: input.request
|
|
13492
13667
|
}) : verificationUrl ?? input.request.url
|
|
13493
13668
|
}) : undefined);
|
|
13494
|
-
const app = new
|
|
13669
|
+
const app = new Elysia25({
|
|
13495
13670
|
name: options.name ?? "absolutejs-voice-plivo"
|
|
13496
13671
|
}).get(answerPath, async ({ query, request }) => {
|
|
13497
13672
|
const streamUrl = await resolvePlivoStreamUrl(options, {
|
|
@@ -13602,9 +13777,9 @@ var createPlivoVoiceRoutes = (options = {}) => {
|
|
|
13602
13777
|
|
|
13603
13778
|
// src/telephony/telnyx.ts
|
|
13604
13779
|
import { Buffer as Buffer5 } from "buffer";
|
|
13605
|
-
import { Elysia as
|
|
13780
|
+
import { Elysia as Elysia26 } from "elysia";
|
|
13606
13781
|
var escapeXml4 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
13607
|
-
var
|
|
13782
|
+
var escapeHtml26 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
13608
13783
|
var joinUrlPath3 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
13609
13784
|
var resolveRequestOrigin3 = (request) => {
|
|
13610
13785
|
const url = new URL(request.url);
|
|
@@ -13805,21 +13980,21 @@ var buildTelnyxVoiceSetupStatus = async (options, input) => {
|
|
|
13805
13980
|
};
|
|
13806
13981
|
var renderTelnyxSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
13807
13982
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx setup</p>
|
|
13808
|
-
<h1>${
|
|
13983
|
+
<h1>${escapeHtml26(title)}</h1>
|
|
13809
13984
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
13810
13985
|
<ul>
|
|
13811
|
-
<li><strong>TeXML:</strong> <code>${
|
|
13812
|
-
<li><strong>Media stream:</strong> <code>${
|
|
13813
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
13986
|
+
<li><strong>TeXML:</strong> <code>${escapeHtml26(status.urls.texml)}</code></li>
|
|
13987
|
+
<li><strong>Media stream:</strong> <code>${escapeHtml26(status.urls.stream)}</code></li>
|
|
13988
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml26(status.urls.webhook)}</code></li>
|
|
13814
13989
|
</ul>
|
|
13815
|
-
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${
|
|
13816
|
-
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
13990
|
+
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml26(name)}</code></li>`).join("")}</ul>` : ""}
|
|
13991
|
+
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml26(warning)}</li>`).join("")}</ul>` : ""}
|
|
13817
13992
|
</main>`;
|
|
13818
13993
|
var renderTelnyxSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
13819
13994
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx smoke test</p>
|
|
13820
|
-
<h1>${
|
|
13995
|
+
<h1>${escapeHtml26(title)}</h1>
|
|
13821
13996
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
13822
|
-
<ul>${report.checks.map((check) => `<li><strong>${
|
|
13997
|
+
<ul>${report.checks.map((check) => `<li><strong>${escapeHtml26(check.name)}</strong>: ${escapeHtml26(check.status)}${check.message ? ` - ${escapeHtml26(check.message)}` : ""}</li>`).join("")}</ul>
|
|
13823
13998
|
</main>`;
|
|
13824
13999
|
var runTelnyxSmokeTest = async (input) => {
|
|
13825
14000
|
const setup = await buildTelnyxVoiceSetupStatus(input.options, input);
|
|
@@ -13913,7 +14088,7 @@ var createTelnyxVoiceRoutes = (options = {}) => {
|
|
|
13913
14088
|
publicKey: options.webhook?.publicKey,
|
|
13914
14089
|
toleranceSeconds: options.webhook?.toleranceSeconds
|
|
13915
14090
|
}) : undefined);
|
|
13916
|
-
const app = new
|
|
14091
|
+
const app = new Elysia26({
|
|
13917
14092
|
name: options.name ?? "absolutejs-voice-telnyx"
|
|
13918
14093
|
}).get(texmlPath, async ({ query, request }) => {
|
|
13919
14094
|
const streamUrl = await resolveTelnyxStreamUrl(options, {
|
|
@@ -14083,7 +14258,7 @@ var createVoicePhoneAgent = (options) => {
|
|
|
14083
14258
|
setupPath: resolveSetupPath(carrier),
|
|
14084
14259
|
smokePath: resolveSmokePath(carrier)
|
|
14085
14260
|
}));
|
|
14086
|
-
const app = new
|
|
14261
|
+
const app = new Elysia27({
|
|
14087
14262
|
name: options.name ?? "absolutejs-voice-phone-agent"
|
|
14088
14263
|
});
|
|
14089
14264
|
for (const carrier of options.carriers) {
|
|
@@ -16364,7 +16539,7 @@ var createVoiceMemoryStore = () => {
|
|
|
16364
16539
|
return { get, getOrCreate, list, remove, set };
|
|
16365
16540
|
};
|
|
16366
16541
|
// src/opsWebhook.ts
|
|
16367
|
-
import { Elysia as
|
|
16542
|
+
import { Elysia as Elysia28 } from "elysia";
|
|
16368
16543
|
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
16369
16544
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
16370
16545
|
const encoder = new TextEncoder;
|
|
@@ -16494,7 +16669,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
|
16494
16669
|
};
|
|
16495
16670
|
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
16496
16671
|
const path = options.path ?? "/api/voice-ops/webhook";
|
|
16497
|
-
return new
|
|
16672
|
+
return new Elysia28().post(path, async ({ body, request, set }) => {
|
|
16498
16673
|
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
16499
16674
|
if (options.signingSecret) {
|
|
16500
16675
|
const verification = await verifyVoiceOpsWebhookSignature({
|
|
@@ -18314,6 +18489,7 @@ export {
|
|
|
18314
18489
|
selectVoiceTraceEventsForPrune,
|
|
18315
18490
|
runVoiceToolContractSuite,
|
|
18316
18491
|
runVoiceToolContract,
|
|
18492
|
+
runVoiceSimulationSuite,
|
|
18317
18493
|
runVoiceSessionEvals,
|
|
18318
18494
|
runVoiceScenarioFixtureEvals,
|
|
18319
18495
|
runVoiceScenarioEvals,
|
|
@@ -18342,6 +18518,7 @@ export {
|
|
|
18342
18518
|
renderVoiceTraceHTML,
|
|
18343
18519
|
renderVoiceToolContractHTML,
|
|
18344
18520
|
renderVoiceTelephonyCarrierMatrixHTML,
|
|
18521
|
+
renderVoiceSimulationSuiteHTML,
|
|
18345
18522
|
renderVoiceSessionsHTML,
|
|
18346
18523
|
renderVoiceScenarioFixtureEvalHTML,
|
|
18347
18524
|
renderVoiceScenarioEvalHTML,
|
|
@@ -18430,6 +18607,7 @@ export {
|
|
|
18430
18607
|
createVoiceTaskSLABreachedEvent,
|
|
18431
18608
|
createVoiceTaskCreatedEvent,
|
|
18432
18609
|
createVoiceTTSProviderRouter,
|
|
18610
|
+
createVoiceSimulationSuiteRoutes,
|
|
18433
18611
|
createVoiceSessionsJSONHandler,
|
|
18434
18612
|
createVoiceSessionsHTMLHandler,
|
|
18435
18613
|
createVoiceSessionReplayRoutes,
|