@absolutejs/voice 0.0.22-beta.105 → 0.0.22-beta.106
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 +615 -515
- package/dist/simulationSuite.d.ts +105 -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,406 @@ 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 runVoiceSimulationSuite = async (options) => {
|
|
11136
|
+
const include = options.include ?? {};
|
|
11137
|
+
const shouldRunSessions = include.sessions ?? Boolean(options.store || !hasWork(options));
|
|
11138
|
+
const shouldRunScenarios = include.scenarios ?? Boolean(options.scenarios?.length);
|
|
11139
|
+
const shouldRunFixtures = include.fixtures ?? Boolean((options.fixtures?.length ?? 0) > 0 || options.fixtureStore);
|
|
11140
|
+
const shouldRunTools = include.tools ?? Boolean(options.tools?.length);
|
|
11141
|
+
const shouldRunOutcomes = include.outcomes ?? Boolean(options.outcomes?.contracts.length);
|
|
11142
|
+
const [sessions, scenarios, fixtures, tools, outcomes] = await Promise.all([
|
|
11143
|
+
shouldRunSessions ? runVoiceSessionEvals({
|
|
11144
|
+
limit: options.limit,
|
|
11145
|
+
store: options.store,
|
|
11146
|
+
thresholds: options.thresholds
|
|
11147
|
+
}) : undefined,
|
|
11148
|
+
shouldRunScenarios ? runVoiceScenarioEvals({
|
|
11149
|
+
scenarios: options.scenarios,
|
|
11150
|
+
store: options.store
|
|
11151
|
+
}) : undefined,
|
|
11152
|
+
shouldRunFixtures ? runVoiceScenarioFixtureEvals({
|
|
11153
|
+
fixtures: options.fixtures,
|
|
11154
|
+
fixtureStore: options.fixtureStore,
|
|
11155
|
+
scenarios: options.scenarios
|
|
11156
|
+
}) : undefined,
|
|
11157
|
+
shouldRunTools ? runVoiceToolContractSuite({
|
|
11158
|
+
contracts: options.tools ?? []
|
|
11159
|
+
}) : undefined,
|
|
11160
|
+
shouldRunOutcomes && options.outcomes ? runVoiceOutcomeContractSuite(options.outcomes) : undefined
|
|
11161
|
+
]);
|
|
11162
|
+
const sections = [sessions, scenarios, fixtures, tools, outcomes].filter((report) => Boolean(report));
|
|
11163
|
+
const failed = sections.filter((section) => section.status === "fail").length;
|
|
11164
|
+
const passed = sections.length - failed;
|
|
11165
|
+
return {
|
|
11166
|
+
checkedAt: Date.now(),
|
|
11167
|
+
failed,
|
|
11168
|
+
fixtures,
|
|
11169
|
+
outcomes,
|
|
11170
|
+
passed,
|
|
11171
|
+
scenarios,
|
|
11172
|
+
sessions,
|
|
11173
|
+
status: failed > 0 ? "fail" : "pass",
|
|
11174
|
+
summary: {
|
|
11175
|
+
fixtures: fixtures && summarizeSection(fixtures),
|
|
11176
|
+
outcomes: outcomes && summarizeSection(outcomes),
|
|
11177
|
+
scenarios: scenarios && summarizeSection(scenarios),
|
|
11178
|
+
sessions: sessions && summarizeSection(sessions),
|
|
11179
|
+
tools: tools && summarizeSection(tools)
|
|
11180
|
+
},
|
|
11181
|
+
tools,
|
|
11182
|
+
total: sections.length
|
|
11183
|
+
};
|
|
11184
|
+
};
|
|
11185
|
+
var renderSection = (label, summary) => {
|
|
11186
|
+
if (!summary) {
|
|
11187
|
+
return "";
|
|
11188
|
+
}
|
|
11189
|
+
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>`;
|
|
11190
|
+
};
|
|
11191
|
+
var renderVoiceSimulationSuiteHTML = (report, options = {}) => {
|
|
11192
|
+
const title = options.title ?? "Voice Simulation Suite";
|
|
11193
|
+
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{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));margin:18px 0}.grid article{background:#151d27;border:1px solid #283544;border-radius:18px;padding:16px}.grid span{color:#aab5c0}.grid strong{display:block;font-size:2rem;margin:.25rem 0;text-transform:uppercase}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><pre>${escapeHtml20(JSON.stringify(report.summary, null, 2))}</pre></main></body></html>`;
|
|
11194
|
+
};
|
|
11195
|
+
var createVoiceSimulationSuiteRoutes = (options) => {
|
|
11196
|
+
const path = options.path ?? "/api/voice/simulations";
|
|
11197
|
+
const htmlPath = options.htmlPath === undefined ? "/voice/simulations" : options.htmlPath;
|
|
11198
|
+
const app = new Elysia19({
|
|
11199
|
+
name: options.name ?? "absolutejs-voice-simulation-suite"
|
|
11200
|
+
}).get(path, () => runVoiceSimulationSuite(options));
|
|
11201
|
+
if (htmlPath) {
|
|
11202
|
+
app.get(htmlPath, async () => {
|
|
11203
|
+
const report = await runVoiceSimulationSuite(options);
|
|
11204
|
+
const html = options.render ? await options.render(report) : renderVoiceSimulationSuiteHTML(report, options);
|
|
11205
|
+
return new Response(html, {
|
|
11206
|
+
headers: {
|
|
11207
|
+
"content-type": "text/html; charset=utf-8",
|
|
11208
|
+
...options.headers
|
|
11209
|
+
}
|
|
11210
|
+
});
|
|
11211
|
+
});
|
|
11212
|
+
}
|
|
11213
|
+
return app;
|
|
11214
|
+
};
|
|
11215
|
+
// src/workflowContract.ts
|
|
11216
|
+
var getObject2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
11217
|
+
var getPathValue2 = (value, path) => {
|
|
11218
|
+
let current = value;
|
|
11219
|
+
for (const part of path.split(".").filter(Boolean)) {
|
|
11220
|
+
const record = getObject2(current);
|
|
11221
|
+
if (!record || !(part in record)) {
|
|
11222
|
+
return;
|
|
11223
|
+
}
|
|
11224
|
+
current = record[part];
|
|
11225
|
+
}
|
|
11226
|
+
return current;
|
|
11227
|
+
};
|
|
11228
|
+
var hasValue = (value, match) => {
|
|
11229
|
+
switch (match) {
|
|
11230
|
+
case "boolean":
|
|
11231
|
+
return typeof value === "boolean";
|
|
11232
|
+
case "number":
|
|
11233
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
11234
|
+
case "string":
|
|
11235
|
+
return typeof value === "string";
|
|
11236
|
+
case "truthy":
|
|
11237
|
+
return Boolean(value);
|
|
11238
|
+
case "non-empty":
|
|
11239
|
+
default:
|
|
11240
|
+
return Array.isArray(value) ? value.length > 0 : typeof value === "string" ? value.trim().length > 0 : value !== undefined && value !== null;
|
|
11241
|
+
}
|
|
11242
|
+
};
|
|
11243
|
+
var resolveOutcome2 = (routeResult) => {
|
|
11244
|
+
if (routeResult.complete)
|
|
11245
|
+
return "complete";
|
|
11246
|
+
if (routeResult.transfer)
|
|
11247
|
+
return "transfer";
|
|
11248
|
+
if (routeResult.escalate)
|
|
11249
|
+
return "escalate";
|
|
11250
|
+
if (routeResult.voicemail)
|
|
11251
|
+
return "voicemail";
|
|
11252
|
+
if (routeResult.noAnswer)
|
|
11253
|
+
return "no-answer";
|
|
11254
|
+
return;
|
|
11255
|
+
};
|
|
11256
|
+
var validateVoiceWorkflowRouteResult = (definition, routeResult) => {
|
|
11257
|
+
const issues = [];
|
|
11258
|
+
const requiredFields = (definition.fields ?? []).filter((field) => field.required !== false).map((field) => field.path);
|
|
11259
|
+
const missingFields = [];
|
|
11260
|
+
const outcome = resolveOutcome2(routeResult);
|
|
11261
|
+
if (definition.outcome && outcome !== definition.outcome) {
|
|
11262
|
+
issues.push({
|
|
11263
|
+
code: "workflow.outcome_mismatch",
|
|
11264
|
+
message: `Expected workflow outcome ${definition.outcome}, saw ${outcome ?? "none"}.`
|
|
11265
|
+
});
|
|
11266
|
+
}
|
|
11267
|
+
for (const field of definition.fields ?? []) {
|
|
11268
|
+
if (field.required === false)
|
|
11269
|
+
continue;
|
|
11270
|
+
const paths = [field.path, ...field.aliases ?? []];
|
|
11271
|
+
const present = paths.some((path) => hasValue(getPathValue2(routeResult.result, path), field.match ?? "non-empty"));
|
|
11272
|
+
if (!present) {
|
|
11273
|
+
missingFields.push(field.path);
|
|
11274
|
+
issues.push({
|
|
11275
|
+
code: "workflow.missing_field",
|
|
11276
|
+
field: field.path,
|
|
11277
|
+
message: `Missing required workflow field: ${field.label ?? field.path}.`
|
|
11278
|
+
});
|
|
11279
|
+
}
|
|
11280
|
+
}
|
|
11281
|
+
issues.push(...definition.validate?.({
|
|
11282
|
+
result: routeResult.result,
|
|
11283
|
+
routeResult
|
|
11284
|
+
}) ?? []);
|
|
11285
|
+
return {
|
|
11286
|
+
contractId: definition.id,
|
|
11287
|
+
issues,
|
|
11288
|
+
missingFields,
|
|
11289
|
+
outcome,
|
|
11290
|
+
pass: issues.length === 0,
|
|
11291
|
+
requiredFields
|
|
11292
|
+
};
|
|
11293
|
+
};
|
|
11294
|
+
var createVoiceWorkflowScenario = (definition, overrides = {}) => ({
|
|
11295
|
+
description: definition.description,
|
|
11296
|
+
forbiddenHandoffActions: definition.forbiddenHandoffActions,
|
|
11297
|
+
id: definition.id,
|
|
11298
|
+
label: definition.label,
|
|
11299
|
+
maxProviderErrors: definition.maxProviderErrors,
|
|
11300
|
+
maxSessionErrors: definition.maxSessionErrors,
|
|
11301
|
+
minSessions: definition.minSessions,
|
|
11302
|
+
minTurns: definition.minTurns,
|
|
11303
|
+
requiredAssistantIncludes: definition.requiredAssistantIncludes,
|
|
11304
|
+
requiredDisposition: definition.requiredDisposition,
|
|
11305
|
+
requiredHandoffActions: definition.requiredHandoffActions,
|
|
11306
|
+
requiredLifecycleTypes: definition.requiredLifecycleTypes,
|
|
11307
|
+
requiredTranscriptIncludes: definition.requiredTranscriptIncludes,
|
|
11308
|
+
requiredWorkflowContracts: [definition.id],
|
|
11309
|
+
scenarioId: definition.scenarioId,
|
|
11310
|
+
...overrides
|
|
11311
|
+
});
|
|
11312
|
+
var createVoiceWorkflowContract = (definition) => ({
|
|
11313
|
+
assertRouteResult: (routeResult) => {
|
|
11314
|
+
const validation = validateVoiceWorkflowRouteResult(definition, routeResult);
|
|
11315
|
+
if (!validation.pass) {
|
|
11316
|
+
throw new Error(`Voice workflow contract ${definition.id} failed: ${validation.issues.map((issue) => issue.message).join(" ")}`);
|
|
11317
|
+
}
|
|
11318
|
+
},
|
|
11319
|
+
definition,
|
|
11320
|
+
toScenarioEval: (overrides) => createVoiceWorkflowScenario(definition, overrides),
|
|
11321
|
+
validateRouteResult: (routeResult) => validateVoiceWorkflowRouteResult(definition, routeResult)
|
|
11322
|
+
});
|
|
11323
|
+
var presetDefinitions = {
|
|
11324
|
+
"appointment-booking": {
|
|
11325
|
+
description: "Appointment booking should complete with enough identity, appointment, and follow-up details to act on.",
|
|
11326
|
+
fields: [
|
|
11327
|
+
{ aliases: ["name", "customer.name"], label: "Caller name", path: "caller.name" },
|
|
11328
|
+
{
|
|
11329
|
+
aliases: ["phone", "customer.phone"],
|
|
11330
|
+
label: "Caller phone",
|
|
11331
|
+
path: "caller.phone"
|
|
11332
|
+
},
|
|
11333
|
+
{
|
|
11334
|
+
aliases: ["appointment.start", "appointment.time", "scheduledAt"],
|
|
11335
|
+
label: "Appointment time",
|
|
11336
|
+
path: "appointment.startsAt"
|
|
11337
|
+
},
|
|
11338
|
+
{
|
|
11339
|
+
aliases: ["summary", "assistantSummary"],
|
|
11340
|
+
label: "Summary",
|
|
11341
|
+
path: "appointment.summary"
|
|
11342
|
+
}
|
|
11343
|
+
],
|
|
11344
|
+
id: "appointment-booking",
|
|
11345
|
+
label: "Appointment booking",
|
|
11346
|
+
outcome: "complete",
|
|
11347
|
+
requiredDisposition: "completed"
|
|
11348
|
+
},
|
|
11349
|
+
"lead-qualification": {
|
|
11350
|
+
description: "Lead qualification should complete with contact, need, qualification, and next-step fields.",
|
|
11351
|
+
fields: [
|
|
11352
|
+
{ aliases: ["name", "lead.name"], label: "Lead name", path: "contact.name" },
|
|
11353
|
+
{
|
|
11354
|
+
aliases: ["email", "lead.email"],
|
|
11355
|
+
label: "Lead email",
|
|
11356
|
+
path: "contact.email"
|
|
11357
|
+
},
|
|
11358
|
+
{
|
|
11359
|
+
aliases: ["need", "pain", "summary"],
|
|
11360
|
+
label: "Need",
|
|
11361
|
+
path: "qualification.need"
|
|
11362
|
+
},
|
|
11363
|
+
{
|
|
11364
|
+
aliases: ["qualified", "qualification.qualified"],
|
|
11365
|
+
label: "Qualified",
|
|
11366
|
+
match: "boolean",
|
|
11367
|
+
path: "qualification.isQualified"
|
|
11368
|
+
},
|
|
11369
|
+
{
|
|
11370
|
+
aliases: ["nextStep", "followUp"],
|
|
11371
|
+
label: "Next step",
|
|
11372
|
+
path: "qualification.nextStep"
|
|
11373
|
+
}
|
|
11374
|
+
],
|
|
11375
|
+
id: "lead-qualification",
|
|
11376
|
+
label: "Lead qualification",
|
|
11377
|
+
outcome: "complete",
|
|
11378
|
+
requiredDisposition: "completed"
|
|
11379
|
+
},
|
|
11380
|
+
"support-triage": {
|
|
11381
|
+
description: "Support triage should capture identity, issue summary, severity, and the operational follow-up.",
|
|
11382
|
+
fields: [
|
|
11383
|
+
{
|
|
11384
|
+
aliases: ["name", "customer.name"],
|
|
11385
|
+
label: "Customer name",
|
|
11386
|
+
path: "customer.name"
|
|
11387
|
+
},
|
|
11388
|
+
{
|
|
11389
|
+
aliases: ["issue", "summary", "assistantSummary"],
|
|
11390
|
+
label: "Issue summary",
|
|
11391
|
+
path: "issue.summary"
|
|
11392
|
+
},
|
|
11393
|
+
{
|
|
11394
|
+
aliases: ["priority", "severity"],
|
|
11395
|
+
label: "Severity",
|
|
11396
|
+
path: "issue.severity"
|
|
11397
|
+
},
|
|
11398
|
+
{
|
|
11399
|
+
aliases: ["nextStep", "task.title"],
|
|
11400
|
+
label: "Next step",
|
|
11401
|
+
path: "resolution.nextStep"
|
|
11402
|
+
}
|
|
11403
|
+
],
|
|
11404
|
+
id: "support-triage",
|
|
11405
|
+
label: "Support triage",
|
|
11406
|
+
outcome: "complete",
|
|
11407
|
+
requiredDisposition: "completed"
|
|
11408
|
+
},
|
|
11409
|
+
"transfer-handoff": {
|
|
11410
|
+
description: "Transfer handoff should produce a routed transfer plus handoff evidence.",
|
|
11411
|
+
fields: [
|
|
11412
|
+
{
|
|
11413
|
+
aliases: ["target", "callTarget"],
|
|
11414
|
+
label: "Transfer target",
|
|
11415
|
+
path: "transfer.target"
|
|
11416
|
+
},
|
|
11417
|
+
{
|
|
11418
|
+
aliases: ["reason", "callReason"],
|
|
11419
|
+
label: "Transfer reason",
|
|
11420
|
+
path: "transfer.reason"
|
|
11421
|
+
},
|
|
11422
|
+
{
|
|
11423
|
+
aliases: ["summary", "assistantSummary"],
|
|
11424
|
+
label: "Transfer summary",
|
|
11425
|
+
path: "transfer.summary"
|
|
11426
|
+
}
|
|
11427
|
+
],
|
|
11428
|
+
id: "transfer-handoff",
|
|
11429
|
+
label: "Transfer handoff",
|
|
11430
|
+
outcome: "transfer",
|
|
11431
|
+
requiredDisposition: "transferred",
|
|
11432
|
+
requiredHandoffActions: ["transfer"]
|
|
11433
|
+
},
|
|
11434
|
+
"voicemail-callback": {
|
|
11435
|
+
description: "Voicemail callback should preserve enough caller and callback context for follow-up.",
|
|
11436
|
+
fields: [
|
|
11437
|
+
{
|
|
11438
|
+
aliases: ["name", "caller.name"],
|
|
11439
|
+
label: "Caller name",
|
|
11440
|
+
path: "voicemail.callerName"
|
|
11441
|
+
},
|
|
11442
|
+
{
|
|
11443
|
+
aliases: ["phone", "caller.phone"],
|
|
11444
|
+
label: "Callback phone",
|
|
11445
|
+
path: "voicemail.callbackPhone"
|
|
11446
|
+
},
|
|
11447
|
+
{
|
|
11448
|
+
aliases: ["message", "summary", "assistantSummary"],
|
|
11449
|
+
label: "Voicemail summary",
|
|
11450
|
+
path: "voicemail.summary"
|
|
11451
|
+
}
|
|
11452
|
+
],
|
|
11453
|
+
id: "voicemail-callback",
|
|
11454
|
+
label: "Voicemail callback",
|
|
11455
|
+
outcome: "voicemail",
|
|
11456
|
+
requiredDisposition: "voicemail",
|
|
11457
|
+
requiredHandoffActions: ["voicemail"]
|
|
11458
|
+
}
|
|
11459
|
+
};
|
|
11460
|
+
var createVoiceWorkflowContractPreset = (name, options = {}) => {
|
|
11461
|
+
const preset = presetDefinitions[name];
|
|
11462
|
+
return createVoiceWorkflowContract({
|
|
11463
|
+
...preset,
|
|
11464
|
+
...options,
|
|
11465
|
+
fields: options.fields ?? preset.fields,
|
|
11466
|
+
id: options.id ?? preset.id
|
|
11467
|
+
});
|
|
11468
|
+
};
|
|
11469
|
+
var recordVoiceWorkflowContractTrace = async (input) => input.store.append({
|
|
11470
|
+
at: input.at ?? Date.now(),
|
|
11471
|
+
payload: {
|
|
11472
|
+
contractId: input.contractId ?? input.validation.contractId,
|
|
11473
|
+
issues: input.validation.issues,
|
|
11474
|
+
missingFields: input.validation.missingFields,
|
|
11475
|
+
outcome: input.validation.outcome,
|
|
11476
|
+
requiredFields: input.validation.requiredFields,
|
|
11477
|
+
status: input.validation.pass ? "pass" : "fail"
|
|
11478
|
+
},
|
|
11479
|
+
scenarioId: input.scenarioId,
|
|
11480
|
+
sessionId: input.sessionId,
|
|
11481
|
+
traceId: input.traceId,
|
|
11482
|
+
turnId: input.turnId,
|
|
11483
|
+
type: "workflow.contract"
|
|
11484
|
+
});
|
|
11485
|
+
var createVoiceWorkflowContractHandler = (input) => {
|
|
11486
|
+
return async (session, turn, api, context) => {
|
|
11487
|
+
const legacyHandler = input.handler;
|
|
11488
|
+
const objectHandler = input.handler;
|
|
11489
|
+
const result = input.handler.length >= 4 ? await legacyHandler(session, turn, api, context) : await objectHandler({ api, context, session, turn });
|
|
11490
|
+
if (!result)
|
|
11491
|
+
return result;
|
|
11492
|
+
const resolved = input.resolveContract?.({ context, result, session, turn }) ?? input.contract;
|
|
11493
|
+
if (!resolved)
|
|
11494
|
+
return result;
|
|
11495
|
+
const contract = "validateRouteResult" in resolved ? resolved : createVoiceWorkflowContract(resolved);
|
|
11496
|
+
const validation = contract.validateRouteResult(result);
|
|
11497
|
+
if (input.store) {
|
|
11498
|
+
await recordVoiceWorkflowContractTrace({
|
|
11499
|
+
scenarioId: session.scenarioId,
|
|
11500
|
+
sessionId: session.id,
|
|
11501
|
+
store: input.store,
|
|
11502
|
+
turnId: turn.id,
|
|
11503
|
+
validation
|
|
11504
|
+
});
|
|
11505
|
+
}
|
|
11506
|
+
return result;
|
|
11507
|
+
};
|
|
11262
11508
|
};
|
|
11263
11509
|
// src/turnLatency.ts
|
|
11264
|
-
import { Elysia as
|
|
11510
|
+
import { Elysia as Elysia20 } from "elysia";
|
|
11265
11511
|
var DEFAULT_WARN_AFTER_MS = 1800;
|
|
11266
11512
|
var DEFAULT_FAIL_AFTER_MS = 3200;
|
|
11267
|
-
var
|
|
11513
|
+
var escapeHtml21 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11268
11514
|
var firstNumber2 = (values) => values.filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
|
|
11269
11515
|
var getString10 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
11270
11516
|
var createTraceStageIndex = (events) => {
|
|
@@ -11378,11 +11624,11 @@ var summarizeVoiceTurnLatency = async (options) => {
|
|
|
11378
11624
|
var formatMs2 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
|
|
11379
11625
|
var renderVoiceTurnLatencyHTML = (report, options = {}) => {
|
|
11380
11626
|
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>${
|
|
11627
|
+
const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml21(turn.status)}">
|
|
11628
|
+
<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>
|
|
11629
|
+
<dl>${turn.stages.map((stage) => `<div><dt>${escapeHtml21(stage.label)}</dt><dd>${escapeHtml21(formatMs2(stage.valueMs))}</dd></div>`).join("")}</dl>
|
|
11384
11630
|
</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>${
|
|
11631
|
+
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
11632
|
};
|
|
11387
11633
|
var createVoiceTurnLatencyJSONHandler = (options) => async () => summarizeVoiceTurnLatency(options);
|
|
11388
11634
|
var createVoiceTurnLatencyHTMLHandler = (options) => async () => {
|
|
@@ -11399,7 +11645,7 @@ var createVoiceTurnLatencyHTMLHandler = (options) => async () => {
|
|
|
11399
11645
|
var createVoiceTurnLatencyRoutes = (options) => {
|
|
11400
11646
|
const path = options.path ?? "/api/turn-latency";
|
|
11401
11647
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
11402
|
-
const routes = new
|
|
11648
|
+
const routes = new Elysia20({
|
|
11403
11649
|
name: options.name ?? "absolutejs-voice-turn-latency"
|
|
11404
11650
|
}).get(path, createVoiceTurnLatencyJSONHandler(options));
|
|
11405
11651
|
if (htmlPath) {
|
|
@@ -11408,8 +11654,8 @@ var createVoiceTurnLatencyRoutes = (options) => {
|
|
|
11408
11654
|
return routes;
|
|
11409
11655
|
};
|
|
11410
11656
|
// src/liveLatency.ts
|
|
11411
|
-
import { Elysia as
|
|
11412
|
-
var
|
|
11657
|
+
import { Elysia as Elysia21 } from "elysia";
|
|
11658
|
+
var escapeHtml22 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11413
11659
|
var percentile = (values, percentileValue) => {
|
|
11414
11660
|
if (values.length === 0) {
|
|
11415
11661
|
return;
|
|
@@ -11457,13 +11703,13 @@ var summarizeVoiceLiveLatency = async (options) => {
|
|
|
11457
11703
|
var formatMs3 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
|
|
11458
11704
|
var renderVoiceLiveLatencyHTML = (report, options = {}) => {
|
|
11459
11705
|
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>${
|
|
11706
|
+
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("");
|
|
11707
|
+
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
11708
|
};
|
|
11463
11709
|
var createVoiceLiveLatencyRoutes = (options) => {
|
|
11464
11710
|
const path = options.path ?? "/api/live-latency";
|
|
11465
11711
|
const htmlPath = options.htmlPath === undefined ? "/live-latency" : options.htmlPath;
|
|
11466
|
-
const routes = new
|
|
11712
|
+
const routes = new Elysia21({
|
|
11467
11713
|
name: options.name ?? "absolutejs-voice-live-latency"
|
|
11468
11714
|
}).get(path, () => summarizeVoiceLiveLatency(options));
|
|
11469
11715
|
if (htmlPath) {
|
|
@@ -11480,9 +11726,9 @@ var createVoiceLiveLatencyRoutes = (options) => {
|
|
|
11480
11726
|
return routes;
|
|
11481
11727
|
};
|
|
11482
11728
|
// src/turnQuality.ts
|
|
11483
|
-
import { Elysia as
|
|
11729
|
+
import { Elysia as Elysia22 } from "elysia";
|
|
11484
11730
|
var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
|
|
11485
|
-
var
|
|
11731
|
+
var escapeHtml23 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11486
11732
|
var getTurnLatencyMs = (turn) => {
|
|
11487
11733
|
const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
|
|
11488
11734
|
if (firstTranscriptAt === undefined) {
|
|
@@ -11553,24 +11799,24 @@ var summarizeVoiceTurnQuality = async (options) => {
|
|
|
11553
11799
|
};
|
|
11554
11800
|
var renderVoiceTurnQualityHTML = (report, options = {}) => {
|
|
11555
11801
|
const title = options.title ?? "Voice Turn Quality";
|
|
11556
|
-
const turns = report.turns.map((turn) => `<article class="turn ${
|
|
11802
|
+
const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml23(turn.status)}">
|
|
11557
11803
|
<div class="turn-header">
|
|
11558
11804
|
<div>
|
|
11559
|
-
<p class="eyebrow">${
|
|
11560
|
-
<h2>${
|
|
11805
|
+
<p class="eyebrow">${escapeHtml23(turn.sessionId)} \xB7 ${escapeHtml23(turn.turnId)}</p>
|
|
11806
|
+
<h2>${escapeHtml23(turn.text || "Empty turn")}</h2>
|
|
11561
11807
|
</div>
|
|
11562
|
-
<strong>${
|
|
11808
|
+
<strong>${escapeHtml23(turn.status)}</strong>
|
|
11563
11809
|
</div>
|
|
11564
11810
|
<dl>
|
|
11565
|
-
<div><dt>Source</dt><dd>${
|
|
11811
|
+
<div><dt>Source</dt><dd>${escapeHtml23(turn.source ?? "unknown")}</dd></div>
|
|
11566
11812
|
<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 ${
|
|
11813
|
+
<div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml23(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
|
|
11814
|
+
<div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml23(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
|
|
11569
11815
|
<div><dt>Transcripts</dt><dd>${String(turn.selectedTranscriptCount)} selected \xB7 ${String(turn.finalTranscriptCount)} final \xB7 ${String(turn.partialTranscriptCount)} partial</dd></div>
|
|
11570
11816
|
<div><dt>Cost</dt><dd>${turn.costUnits === undefined ? "n/a" : String(turn.costUnits)}</dd></div>
|
|
11571
11817
|
</dl>
|
|
11572
11818
|
</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>${
|
|
11819
|
+
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
11820
|
};
|
|
11575
11821
|
var createVoiceTurnQualityJSONHandler = (options) => async () => summarizeVoiceTurnQuality(options);
|
|
11576
11822
|
var createVoiceTurnQualityHTMLHandler = (options) => async () => {
|
|
@@ -11587,7 +11833,7 @@ var createVoiceTurnQualityHTMLHandler = (options) => async () => {
|
|
|
11587
11833
|
var createVoiceTurnQualityRoutes = (options) => {
|
|
11588
11834
|
const path = options.path ?? "/api/turn-quality";
|
|
11589
11835
|
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
11590
|
-
const routes = new
|
|
11836
|
+
const routes = new Elysia22({
|
|
11591
11837
|
name: options.name ?? "absolutejs-voice-turn-quality"
|
|
11592
11838
|
}).get(path, createVoiceTurnQualityJSONHandler(options));
|
|
11593
11839
|
if (htmlPath) {
|
|
@@ -11595,157 +11841,8 @@ var createVoiceTurnQualityRoutes = (options) => {
|
|
|
11595
11841
|
}
|
|
11596
11842
|
return routes;
|
|
11597
11843
|
};
|
|
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
11844
|
// src/telephonyOutcome.ts
|
|
11748
|
-
import { Elysia as
|
|
11845
|
+
import { Elysia as Elysia23 } from "elysia";
|
|
11749
11846
|
var DEFAULT_COMPLETED_STATUSES = [
|
|
11750
11847
|
"answered",
|
|
11751
11848
|
"completed",
|
|
@@ -12392,7 +12489,7 @@ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
|
|
|
12392
12489
|
var createVoiceTelephonyWebhookRoutes = (options = {}) => {
|
|
12393
12490
|
const path = options.path ?? "/api/voice/telephony/webhook";
|
|
12394
12491
|
const handler = createVoiceTelephonyWebhookHandler(options);
|
|
12395
|
-
return new
|
|
12492
|
+
return new Elysia23({
|
|
12396
12493
|
name: options.name ?? "absolutejs-voice-telephony-webhooks"
|
|
12397
12494
|
}).post(path, async ({ query, request }) => {
|
|
12398
12495
|
try {
|
|
@@ -12413,15 +12510,15 @@ var createVoiceTelephonyWebhookRoutes = (options = {}) => {
|
|
|
12413
12510
|
});
|
|
12414
12511
|
};
|
|
12415
12512
|
// src/phoneAgent.ts
|
|
12416
|
-
import { Elysia as
|
|
12513
|
+
import { Elysia as Elysia27 } from "elysia";
|
|
12417
12514
|
|
|
12418
12515
|
// src/telephony/plivo.ts
|
|
12419
12516
|
import { Buffer as Buffer4 } from "buffer";
|
|
12420
|
-
import { Elysia as
|
|
12517
|
+
import { Elysia as Elysia25 } from "elysia";
|
|
12421
12518
|
|
|
12422
12519
|
// src/telephony/twilio.ts
|
|
12423
12520
|
import { Buffer as Buffer3 } from "buffer";
|
|
12424
|
-
import { Elysia as
|
|
12521
|
+
import { Elysia as Elysia24 } from "elysia";
|
|
12425
12522
|
var TWILIO_MULAW_SAMPLE_RATE = 8000;
|
|
12426
12523
|
var VOICE_PCM_SAMPLE_RATE = 16000;
|
|
12427
12524
|
var escapeXml2 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
@@ -12451,7 +12548,7 @@ var resolveTwilioStreamParameters = async (parameters, input) => {
|
|
|
12451
12548
|
return parameters;
|
|
12452
12549
|
};
|
|
12453
12550
|
var joinUrlPath = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
12454
|
-
var
|
|
12551
|
+
var escapeHtml24 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
12455
12552
|
var getWebhookVerificationUrl = (webhook, input) => {
|
|
12456
12553
|
if (!webhook?.verificationUrl) {
|
|
12457
12554
|
return;
|
|
@@ -12494,23 +12591,23 @@ var buildTwilioVoiceSetupStatus = async (options, input) => {
|
|
|
12494
12591
|
};
|
|
12495
12592
|
var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
12496
12593
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
|
|
12497
|
-
<h1>${
|
|
12594
|
+
<h1>${escapeHtml24(title)}</h1>
|
|
12498
12595
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
12499
12596
|
<section>
|
|
12500
12597
|
<h2>URLs</h2>
|
|
12501
12598
|
<ul>
|
|
12502
|
-
<li><strong>TwiML:</strong> <code>${
|
|
12503
|
-
<li><strong>Media stream:</strong> <code>${
|
|
12504
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
12599
|
+
<li><strong>TwiML:</strong> <code>${escapeHtml24(status.urls.twiml)}</code></li>
|
|
12600
|
+
<li><strong>Media stream:</strong> <code>${escapeHtml24(status.urls.stream)}</code></li>
|
|
12601
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml24(status.urls.webhook)}</code></li>
|
|
12505
12602
|
</ul>
|
|
12506
12603
|
</section>
|
|
12507
12604
|
<section>
|
|
12508
12605
|
<h2>Signing</h2>
|
|
12509
12606
|
<p>Mode: <code>${status.signing.mode}</code></p>
|
|
12510
|
-
${status.signing.verificationUrl ? `<p>Verification URL: <code>${
|
|
12607
|
+
${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml24(status.signing.verificationUrl)}</code></p>` : ""}
|
|
12511
12608
|
</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>${
|
|
12609
|
+
${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml24(name)}</code></li>`).join("")}</ul></section>` : ""}
|
|
12610
|
+
${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml24(warning)}</li>`).join("")}</ul></section>` : ""}
|
|
12514
12611
|
</main>`;
|
|
12515
12612
|
var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&", "&");
|
|
12516
12613
|
var createSmokeCheck = (name, status, message, details) => ({
|
|
@@ -12521,20 +12618,20 @@ var createSmokeCheck = (name, status, message, details) => ({
|
|
|
12521
12618
|
});
|
|
12522
12619
|
var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
12523
12620
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
|
|
12524
|
-
<h1>${
|
|
12621
|
+
<h1>${escapeHtml24(title)}</h1>
|
|
12525
12622
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
12526
12623
|
<section>
|
|
12527
12624
|
<h2>Checks</h2>
|
|
12528
12625
|
<ul>
|
|
12529
|
-
${report.checks.map((check) => `<li><strong>${
|
|
12626
|
+
${report.checks.map((check) => `<li><strong>${escapeHtml24(check.name)}</strong>: ${escapeHtml24(check.status)}${check.message ? ` - ${escapeHtml24(check.message)}` : ""}</li>`).join("")}
|
|
12530
12627
|
</ul>
|
|
12531
12628
|
</section>
|
|
12532
12629
|
<section>
|
|
12533
12630
|
<h2>Observed URLs</h2>
|
|
12534
12631
|
<ul>
|
|
12535
|
-
<li><strong>TwiML:</strong> <code>${
|
|
12536
|
-
<li><strong>Stream:</strong> <code>${
|
|
12537
|
-
<li><strong>Webhook:</strong> <code>${
|
|
12632
|
+
<li><strong>TwiML:</strong> <code>${escapeHtml24(report.setup.urls.twiml)}</code></li>
|
|
12633
|
+
<li><strong>Stream:</strong> <code>${escapeHtml24(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
|
|
12634
|
+
<li><strong>Webhook:</strong> <code>${escapeHtml24(report.setup.urls.webhook)}</code></li>
|
|
12538
12635
|
</ul>
|
|
12539
12636
|
</section>
|
|
12540
12637
|
</main>`;
|
|
@@ -12994,7 +13091,7 @@ var createTwilioVoiceRoutes = (options) => {
|
|
|
12994
13091
|
const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
|
|
12995
13092
|
const bridges = new WeakMap;
|
|
12996
13093
|
const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
|
|
12997
|
-
const app = new
|
|
13094
|
+
const app = new Elysia24({
|
|
12998
13095
|
name: options.name ?? "absolutejs-voice-twilio"
|
|
12999
13096
|
}).get(twimlPath, async ({ query, request }) => {
|
|
13000
13097
|
const streamUrl = await resolveTwilioStreamUrl(options, {
|
|
@@ -13131,7 +13228,7 @@ var createTwilioVoiceRoutes = (options) => {
|
|
|
13131
13228
|
|
|
13132
13229
|
// src/telephony/plivo.ts
|
|
13133
13230
|
var escapeXml3 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
13134
|
-
var
|
|
13231
|
+
var escapeHtml25 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
13135
13232
|
var joinUrlPath2 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
13136
13233
|
var resolveRequestOrigin2 = (request) => {
|
|
13137
13234
|
const url = new URL(request.url);
|
|
@@ -13382,21 +13479,21 @@ var buildPlivoVoiceSetupStatus = async (options, input) => {
|
|
|
13382
13479
|
};
|
|
13383
13480
|
var renderPlivoSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
13384
13481
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo setup</p>
|
|
13385
|
-
<h1>${
|
|
13482
|
+
<h1>${escapeHtml25(title)}</h1>
|
|
13386
13483
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
13387
13484
|
<ul>
|
|
13388
|
-
<li><strong>Answer XML:</strong> <code>${
|
|
13389
|
-
<li><strong>Audio stream:</strong> <code>${
|
|
13390
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
13485
|
+
<li><strong>Answer XML:</strong> <code>${escapeHtml25(status.urls.answer)}</code></li>
|
|
13486
|
+
<li><strong>Audio stream:</strong> <code>${escapeHtml25(status.urls.stream)}</code></li>
|
|
13487
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml25(status.urls.webhook)}</code></li>
|
|
13391
13488
|
</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>${
|
|
13489
|
+
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml25(name)}</code></li>`).join("")}</ul>` : ""}
|
|
13490
|
+
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml25(warning)}</li>`).join("")}</ul>` : ""}
|
|
13394
13491
|
</main>`;
|
|
13395
13492
|
var renderPlivoSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
13396
13493
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo smoke test</p>
|
|
13397
|
-
<h1>${
|
|
13494
|
+
<h1>${escapeHtml25(title)}</h1>
|
|
13398
13495
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
13399
|
-
<ul>${report.checks.map((check) => `<li><strong>${
|
|
13496
|
+
<ul>${report.checks.map((check) => `<li><strong>${escapeHtml25(check.name)}</strong>: ${escapeHtml25(check.status)}${check.message ? ` - ${escapeHtml25(check.message)}` : ""}</li>`).join("")}</ul>
|
|
13400
13497
|
</main>`;
|
|
13401
13498
|
var runPlivoSmokeTest = async (input) => {
|
|
13402
13499
|
const setup = await buildPlivoVoiceSetupStatus(input.options, input);
|
|
@@ -13491,7 +13588,7 @@ var createPlivoVoiceRoutes = (options = {}) => {
|
|
|
13491
13588
|
request: input.request
|
|
13492
13589
|
}) : verificationUrl ?? input.request.url
|
|
13493
13590
|
}) : undefined);
|
|
13494
|
-
const app = new
|
|
13591
|
+
const app = new Elysia25({
|
|
13495
13592
|
name: options.name ?? "absolutejs-voice-plivo"
|
|
13496
13593
|
}).get(answerPath, async ({ query, request }) => {
|
|
13497
13594
|
const streamUrl = await resolvePlivoStreamUrl(options, {
|
|
@@ -13602,9 +13699,9 @@ var createPlivoVoiceRoutes = (options = {}) => {
|
|
|
13602
13699
|
|
|
13603
13700
|
// src/telephony/telnyx.ts
|
|
13604
13701
|
import { Buffer as Buffer5 } from "buffer";
|
|
13605
|
-
import { Elysia as
|
|
13702
|
+
import { Elysia as Elysia26 } from "elysia";
|
|
13606
13703
|
var escapeXml4 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
13607
|
-
var
|
|
13704
|
+
var escapeHtml26 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
13608
13705
|
var joinUrlPath3 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
13609
13706
|
var resolveRequestOrigin3 = (request) => {
|
|
13610
13707
|
const url = new URL(request.url);
|
|
@@ -13805,21 +13902,21 @@ var buildTelnyxVoiceSetupStatus = async (options, input) => {
|
|
|
13805
13902
|
};
|
|
13806
13903
|
var renderTelnyxSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
13807
13904
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx setup</p>
|
|
13808
|
-
<h1>${
|
|
13905
|
+
<h1>${escapeHtml26(title)}</h1>
|
|
13809
13906
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
13810
13907
|
<ul>
|
|
13811
|
-
<li><strong>TeXML:</strong> <code>${
|
|
13812
|
-
<li><strong>Media stream:</strong> <code>${
|
|
13813
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
13908
|
+
<li><strong>TeXML:</strong> <code>${escapeHtml26(status.urls.texml)}</code></li>
|
|
13909
|
+
<li><strong>Media stream:</strong> <code>${escapeHtml26(status.urls.stream)}</code></li>
|
|
13910
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml26(status.urls.webhook)}</code></li>
|
|
13814
13911
|
</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>${
|
|
13912
|
+
${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml26(name)}</code></li>`).join("")}</ul>` : ""}
|
|
13913
|
+
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml26(warning)}</li>`).join("")}</ul>` : ""}
|
|
13817
13914
|
</main>`;
|
|
13818
13915
|
var renderTelnyxSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
13819
13916
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx smoke test</p>
|
|
13820
|
-
<h1>${
|
|
13917
|
+
<h1>${escapeHtml26(title)}</h1>
|
|
13821
13918
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
13822
|
-
<ul>${report.checks.map((check) => `<li><strong>${
|
|
13919
|
+
<ul>${report.checks.map((check) => `<li><strong>${escapeHtml26(check.name)}</strong>: ${escapeHtml26(check.status)}${check.message ? ` - ${escapeHtml26(check.message)}` : ""}</li>`).join("")}</ul>
|
|
13823
13920
|
</main>`;
|
|
13824
13921
|
var runTelnyxSmokeTest = async (input) => {
|
|
13825
13922
|
const setup = await buildTelnyxVoiceSetupStatus(input.options, input);
|
|
@@ -13913,7 +14010,7 @@ var createTelnyxVoiceRoutes = (options = {}) => {
|
|
|
13913
14010
|
publicKey: options.webhook?.publicKey,
|
|
13914
14011
|
toleranceSeconds: options.webhook?.toleranceSeconds
|
|
13915
14012
|
}) : undefined);
|
|
13916
|
-
const app = new
|
|
14013
|
+
const app = new Elysia26({
|
|
13917
14014
|
name: options.name ?? "absolutejs-voice-telnyx"
|
|
13918
14015
|
}).get(texmlPath, async ({ query, request }) => {
|
|
13919
14016
|
const streamUrl = await resolveTelnyxStreamUrl(options, {
|
|
@@ -14083,7 +14180,7 @@ var createVoicePhoneAgent = (options) => {
|
|
|
14083
14180
|
setupPath: resolveSetupPath(carrier),
|
|
14084
14181
|
smokePath: resolveSmokePath(carrier)
|
|
14085
14182
|
}));
|
|
14086
|
-
const app = new
|
|
14183
|
+
const app = new Elysia27({
|
|
14087
14184
|
name: options.name ?? "absolutejs-voice-phone-agent"
|
|
14088
14185
|
});
|
|
14089
14186
|
for (const carrier of options.carriers) {
|
|
@@ -16364,7 +16461,7 @@ var createVoiceMemoryStore = () => {
|
|
|
16364
16461
|
return { get, getOrCreate, list, remove, set };
|
|
16365
16462
|
};
|
|
16366
16463
|
// src/opsWebhook.ts
|
|
16367
|
-
import { Elysia as
|
|
16464
|
+
import { Elysia as Elysia28 } from "elysia";
|
|
16368
16465
|
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
16369
16466
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
16370
16467
|
const encoder = new TextEncoder;
|
|
@@ -16494,7 +16591,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
|
16494
16591
|
};
|
|
16495
16592
|
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
16496
16593
|
const path = options.path ?? "/api/voice-ops/webhook";
|
|
16497
|
-
return new
|
|
16594
|
+
return new Elysia28().post(path, async ({ body, request, set }) => {
|
|
16498
16595
|
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
16499
16596
|
if (options.signingSecret) {
|
|
16500
16597
|
const verification = await verifyVoiceOpsWebhookSignature({
|
|
@@ -18314,6 +18411,7 @@ export {
|
|
|
18314
18411
|
selectVoiceTraceEventsForPrune,
|
|
18315
18412
|
runVoiceToolContractSuite,
|
|
18316
18413
|
runVoiceToolContract,
|
|
18414
|
+
runVoiceSimulationSuite,
|
|
18317
18415
|
runVoiceSessionEvals,
|
|
18318
18416
|
runVoiceScenarioFixtureEvals,
|
|
18319
18417
|
runVoiceScenarioEvals,
|
|
@@ -18342,6 +18440,7 @@ export {
|
|
|
18342
18440
|
renderVoiceTraceHTML,
|
|
18343
18441
|
renderVoiceToolContractHTML,
|
|
18344
18442
|
renderVoiceTelephonyCarrierMatrixHTML,
|
|
18443
|
+
renderVoiceSimulationSuiteHTML,
|
|
18345
18444
|
renderVoiceSessionsHTML,
|
|
18346
18445
|
renderVoiceScenarioFixtureEvalHTML,
|
|
18347
18446
|
renderVoiceScenarioEvalHTML,
|
|
@@ -18430,6 +18529,7 @@ export {
|
|
|
18430
18529
|
createVoiceTaskSLABreachedEvent,
|
|
18431
18530
|
createVoiceTaskCreatedEvent,
|
|
18432
18531
|
createVoiceTTSProviderRouter,
|
|
18532
|
+
createVoiceSimulationSuiteRoutes,
|
|
18433
18533
|
createVoiceSessionsJSONHandler,
|
|
18434
18534
|
createVoiceSessionsHTMLHandler,
|
|
18435
18535
|
createVoiceSessionReplayRoutes,
|