@absolutejs/voice 0.0.22-beta.243 → 0.0.22-beta.245

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 CHANGED
@@ -37,6 +37,7 @@ These are the primitive-first paths a Vapi-style buyer usually needs. Each path
37
37
  | Phone voice assistant | `createVoicePhoneAgent(...)` | carrier matrix, setup instructions, phone smoke contract, production readiness, operations record | phone setup HTML/JSON, smoke HTML/JSON, framework status UI |
38
38
  | Multi-specialist support flow | `createVoiceAgentSquad(...)` | squad contract, handoff traces, context traces, operations record | Agent Squad status hooks/composables/services/widgets |
39
39
  | Business actions and tools | `createVoiceAgentTool(...)` plus agent tool runtime | tool contracts, audit events, integration events, outcome contracts | operations record, action center, contract routes |
40
+ | Guardrails and policy checks | `createVoiceGuardrailPolicy(...)` plus `createVoiceGuardrailRoutes(...)` | blocking/warning decisions, redacted content, `assistant.guardrail` trace events | guardrail JSON/Markdown routes and operations record traces |
40
41
  | Provider routing and fallback | provider routers, health checks, simulation controls | provider contract matrix, provider-stage traces, latency SLO reports | provider contract hooks/composables/services/widgets |
41
42
  | Production operations | ops status, ops recovery, production readiness, delivery runtime | readiness gates, recovery report, incident Markdown, delivery queues | ops action center, delivery runtime UI, operations record |
42
43
  | Outbound campaigns | `createVoiceCampaignRoutes(...)` | recipient validation, consent/dedupe, carrier dry-run, campaign readiness | campaign routes and operations-record-linked attempt proof |
@@ -68,7 +69,7 @@ Use this checklist when a buyer asks, "How do I know this replaces a hosted voic
68
69
  | Can I prove provider fallback and latency? | provider contract matrix, provider status UI, `/turn-latency`, `/live-latency` | Provider choice, fallback behavior, server turn timing, browser p50/p95 timing |
69
70
  | Can operators intervene safely? | live-ops routes, action center, ops action audit routes, operations record | Pause/resume/takeover, injected instructions, operator action audit trail |
70
71
  | Can I run outbound campaigns? | `/voice/campaigns`, `/voice/campaigns/observability`, `/api/voice/campaigns/readiness-proof` | Recipient import evidence, consent/dedupe checks, scheduling policy, worker-safe attempts |
71
- | Can I handle post-call workflow? | reviews, tasks, integration events, outcome contracts, operations record | Summary/review artifacts, task creation, webhook/sink delivery, matched session proof |
72
+ | Can I handle post-call workflow? | `createVoicePostCallAnalysisRoutes(...)`, reviews, tasks, integration events, outcome contracts, operations record | Extracted-field proof, task creation, webhook/sink delivery, matched session proof |
72
73
  | Can I keep data in my infrastructure? | `/data-control`, `/data-control/audit.md`, retention dry-run/apply routes | Customer-owned storage, redaction, audit export, guarded deletion, zero-retention planning |
73
74
  | Can I prove release readiness? | `/production-readiness`, `/ops-recovery`, delivery runtime, readiness profiles | Deploy gates for session health, audits, delivery queues, provider/campaign/phone proof |
74
75
 
@@ -82,6 +83,56 @@ For a demo, the fastest convincing path is:
82
83
 
83
84
  If those five surfaces are green and linked, the buyer can see the core difference from Vapi-style hosted orchestration: the operational proof lives inside the app, not in a vendor dashboard.
84
85
 
86
+ ## Post-Call Analysis Proof
87
+
88
+ Use `createVoicePostCallAnalysisRoutes(...)` when the hosted-platform feature you need is call analysis plus follow-up proof. It validates that required extracted fields exist, expected ops tasks were created, integration/webhook events delivered, and the report links back to `/voice-operations/:sessionId`.
89
+
90
+ ```ts
91
+ import { createVoicePostCallAnalysisRoutes } from '@absolutejs/voice';
92
+
93
+ app.use(
94
+ createVoicePostCallAnalysisRoutes({
95
+ path: '/api/voice/post-call-analysis',
96
+ operationRecordBasePath: '/voice-operations/:sessionId',
97
+ reviews: runtime.reviews,
98
+ tasks: runtime.tasks,
99
+ integrationEvents: runtime.events,
100
+ source: ({ reviewId, sessionId }) => ({
101
+ reviewId,
102
+ sessionId,
103
+ // Use your own extractor output here, for example fields persisted from an LLM/tool result.
104
+ extractedFields: loadExtractedPostCallFields(reviewId ?? sessionId)
105
+ }),
106
+ fields: [
107
+ { path: 'review.postCall.target', label: 'customer target' },
108
+ { path: 'customerId' },
109
+ { path: 'category' }
110
+ ],
111
+ requiredTaskKinds: ['support-triage'],
112
+ requireDeliveredIntegrationEvent: true
113
+ })
114
+ );
115
+ ```
116
+
117
+ ## Guardrails
118
+
119
+ Use `createVoiceGuardrailRoutes(...)` when you need code-owned policy checks for what an agent may say, what tool payloads may contain, or which transcript content should warn/redact before downstream workflow. The primitive does not force a moderation vendor or hosted dashboard; it returns JSON/Markdown proof and can emit `assistant.guardrail` trace events.
120
+
121
+ ```ts
122
+ import {
123
+ createVoiceGuardrailRoutes,
124
+ voiceGuardrailPolicyPresets
125
+ } from '@absolutejs/voice';
126
+
127
+ app.use(
128
+ createVoiceGuardrailRoutes({
129
+ path: '/api/voice/guardrails',
130
+ policies: [voiceGuardrailPolicyPresets.supportSafeDefaults],
131
+ trace: runtime.traces
132
+ })
133
+ );
134
+ ```
135
+
85
136
  ## Use-Case Recipe: Support Triage
86
137
 
87
138
  Use this path when you want a Vapi-style support assistant that can answer web or phone calls, look up customer context, route billing issues to a specialist, create follow-up work, and leave one debuggable call record. It is a recipe over primitives, not a support app kit.
@@ -0,0 +1,108 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { VoiceTraceEventStore } from './trace';
3
+ export type VoiceGuardrailStage = 'assistant-output' | 'handoff' | 'model-input' | 'tool-input' | 'tool-output' | 'transcript';
4
+ export type VoiceGuardrailSeverity = 'block' | 'warn';
5
+ export type VoiceGuardrailStatus = 'blocked' | 'pass' | 'warn';
6
+ export type VoiceGuardrailRule = {
7
+ action?: VoiceGuardrailSeverity;
8
+ description?: string;
9
+ id: string;
10
+ label?: string;
11
+ match: RegExp | string | ((input: VoiceGuardrailEvaluationInput) => boolean | Promise<boolean>);
12
+ redactWith?: string;
13
+ stages?: VoiceGuardrailStage[];
14
+ };
15
+ export type VoiceGuardrailEvaluationInput = {
16
+ content?: unknown;
17
+ metadata?: Record<string, unknown>;
18
+ sessionId?: string;
19
+ stage: VoiceGuardrailStage;
20
+ turnId?: string;
21
+ };
22
+ export type VoiceGuardrailFinding = {
23
+ action: VoiceGuardrailSeverity;
24
+ description?: string;
25
+ label: string;
26
+ ruleId: string;
27
+ stage: VoiceGuardrailStage;
28
+ };
29
+ export type VoiceGuardrailDecision = {
30
+ allowed: boolean;
31
+ checkedAt: number;
32
+ content?: unknown;
33
+ findings: VoiceGuardrailFinding[];
34
+ redactedContent?: unknown;
35
+ sessionId?: string;
36
+ stage: VoiceGuardrailStage;
37
+ status: VoiceGuardrailStatus;
38
+ turnId?: string;
39
+ };
40
+ export type VoiceGuardrailPolicy = {
41
+ defaultAction?: VoiceGuardrailSeverity;
42
+ id: string;
43
+ label?: string;
44
+ rules: VoiceGuardrailRule[];
45
+ };
46
+ export type VoiceGuardrailReport = {
47
+ checkedAt: number;
48
+ decisions: VoiceGuardrailDecision[];
49
+ failed: number;
50
+ policies: Array<{
51
+ id: string;
52
+ label?: string;
53
+ rules: number;
54
+ }>;
55
+ status: 'fail' | 'pass' | 'warn';
56
+ summary: {
57
+ blocked: number;
58
+ passed: number;
59
+ warned: number;
60
+ };
61
+ total: number;
62
+ };
63
+ export type VoiceGuardrailRoutesOptions = {
64
+ headers?: HeadersInit;
65
+ name?: string;
66
+ path?: string;
67
+ policies?: VoiceGuardrailPolicy[];
68
+ source?: ((input: VoiceGuardrailEvaluationInput) => Promise<VoiceGuardrailDecision | VoiceGuardrailReport> | VoiceGuardrailDecision | VoiceGuardrailReport) | VoiceGuardrailDecision | VoiceGuardrailReport;
69
+ trace?: VoiceTraceEventStore;
70
+ };
71
+ export declare const evaluateVoiceGuardrailPolicy: (policy: VoiceGuardrailPolicy, input: VoiceGuardrailEvaluationInput) => Promise<VoiceGuardrailDecision>;
72
+ export declare const buildVoiceGuardrailReport: (input?: {
73
+ decisions: VoiceGuardrailDecision[];
74
+ policies?: VoiceGuardrailPolicy[];
75
+ }) => VoiceGuardrailReport;
76
+ export declare const createVoiceGuardrailPolicy: (policy: VoiceGuardrailPolicy) => VoiceGuardrailPolicy;
77
+ export declare const voiceGuardrailPolicyPresets: {
78
+ supportSafeDefaults: VoiceGuardrailPolicy;
79
+ };
80
+ export declare const renderVoiceGuardrailMarkdown: (report: VoiceGuardrailReport) => string;
81
+ export declare const createVoiceGuardrailRoutes: (options?: VoiceGuardrailRoutesOptions) => Elysia<"", {
82
+ decorator: {};
83
+ store: {};
84
+ derive: {};
85
+ resolve: {};
86
+ }, {
87
+ typebox: {};
88
+ error: {};
89
+ }, {
90
+ schema: {};
91
+ standaloneSchema: {};
92
+ macro: {};
93
+ macroFn: {};
94
+ parser: {};
95
+ response: {};
96
+ }, {}, {
97
+ derive: {};
98
+ resolve: {};
99
+ schema: {};
100
+ standaloneSchema: {};
101
+ response: {};
102
+ }, {
103
+ derive: {};
104
+ resolve: {};
105
+ schema: {};
106
+ standaloneSchema: {};
107
+ response: {};
108
+ }>;
package/dist/index.d.ts CHANGED
@@ -83,6 +83,8 @@ export { createVoiceCallReviewFromSession, recordVoiceRuntimeOps } from './runti
83
83
  export { createVoiceOpsRuntime } from './opsRuntime';
84
84
  export { resolveVoiceOpsPreset } from './opsPresets';
85
85
  export { resolveVoiceOutcomeRecipe } from './outcomeRecipes';
86
+ export { buildVoicePostCallAnalysisReport, createVoicePostCallAnalysisRoutes, renderVoicePostCallAnalysisMarkdown } from './postCallAnalysis';
87
+ export { buildVoiceGuardrailReport, createVoiceGuardrailPolicy, createVoiceGuardrailRoutes, evaluateVoiceGuardrailPolicy, renderVoiceGuardrailMarkdown, voiceGuardrailPolicyPresets } from './guardrails';
86
88
  export { createId, createVoiceSessionRecord } from './store';
87
89
  export { createVoiceSTTRoutingCorrectionHandler, resolveVoiceSTTRoutingStrategy } from './routing';
88
90
  export { applyRiskTieredPhraseHintCorrections, applyPhraseHintCorrections, createDomainLexicon, createDomainPhraseHints, createPhraseHintCorrectionHandler, createRiskyTurnCorrectionHandler } from './correction';
@@ -135,6 +137,8 @@ export type { VoiceToolContractCase, VoiceToolContractCaseReport, VoiceToolContr
135
137
  export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
136
138
  export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPreset } from './opsPresets';
137
139
  export type { VoiceOutcomeRecipe, VoiceOutcomeRecipeName, VoiceOutcomeRecipeOptions } from './outcomeRecipes';
140
+ export type { VoicePostCallAnalysisFieldRequirement, VoicePostCallAnalysisFieldResult, VoicePostCallAnalysisIssue, VoicePostCallAnalysisIssueCode, VoicePostCallAnalysisOptions, VoicePostCallAnalysisReport, VoicePostCallAnalysisRoutesOptions, VoicePostCallAnalysisStatus } from './postCallAnalysis';
141
+ export type { VoiceGuardrailDecision, VoiceGuardrailEvaluationInput, VoiceGuardrailFinding, VoiceGuardrailPolicy, VoiceGuardrailReport, VoiceGuardrailRoutesOptions, VoiceGuardrailRule, VoiceGuardrailSeverity, VoiceGuardrailStage, VoiceGuardrailStatus } from './guardrails';
138
142
  export type { VoiceCRMActivitySinkOptions, VoiceHubSpotTaskSinkOptions, VoiceHubSpotTaskUpdateSinkOptions, VoiceHelpdeskTicketSinkOptions, VoiceIntegrationHTTPSinkOptions, VoiceIntegrationSink, VoiceIntegrationSinkDeliveryResult, VoiceLinearIssueSinkOptions, VoiceLinearIssueUpdateSinkOptions, VoiceZendeskTicketSinkOptions, VoiceZendeskTicketUpdateSinkOptions } from './opsSinks';
139
143
  export type { VoiceOpsWebhookEnvelope, VoiceOpsWebhookEntity, VoiceOpsWebhookLinkResolver, VoiceOpsWebhookReceiverRoutesOptions, VoiceOpsWebhookSinkOptions, VoiceOpsWebhookVerificationResult } from './opsWebhook';
140
144
  export type { VoiceHandoffDelivery, VoiceHandoffDeliveryRecord, VoiceHandoffDeliveryRecordInput, VoiceHandoffFanoutResult, VoiceQueuedHandoffDeliveryOptions, VoiceTwilioRedirectHandoffAdapterOptions, VoiceWebhookHandoffAdapterOptions } from './handoff';
package/dist/index.js CHANGED
@@ -28727,6 +28727,395 @@ var resolveVoiceOpsPreset = (name, overrides = {}) => {
28727
28727
  taskPolicies: mergePolicies(preset.taskPolicies, overrides.taskPolicies)
28728
28728
  };
28729
28729
  };
28730
+ // src/postCallAnalysis.ts
28731
+ import { Elysia as Elysia49 } from "elysia";
28732
+ var isStore = (value) => Boolean(value) && typeof value === "object" && value !== null && ("list" in value);
28733
+ var asArray = async (value) => Array.isArray(value) ? value : isStore(value) ? await value.list() : [];
28734
+ var getPathValue3 = (source, path) => {
28735
+ const parts = path.split(".").filter(Boolean);
28736
+ let current = source;
28737
+ for (const part of parts) {
28738
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
28739
+ return;
28740
+ }
28741
+ current = current[part];
28742
+ }
28743
+ return current;
28744
+ };
28745
+ var hasValue2 = (value) => {
28746
+ if (value === undefined || value === null) {
28747
+ return false;
28748
+ }
28749
+ if (typeof value === "string") {
28750
+ return value.trim().length > 0;
28751
+ }
28752
+ if (Array.isArray(value)) {
28753
+ return value.length > 0;
28754
+ }
28755
+ return true;
28756
+ };
28757
+ var matchesReview = (reviewId, id) => Boolean(reviewId && id && (id === reviewId || id.startsWith(`${reviewId}:`)));
28758
+ var matchesSession = (sessionId, event) => {
28759
+ const payloadSessionId = event.payload.sessionId;
28760
+ return Boolean(sessionId && (event.id === sessionId || event.id.startsWith(`${sessionId}:`) || payloadSessionId === sessionId));
28761
+ };
28762
+ var matchesIntegrationEvent = (input) => {
28763
+ const payloadReviewId = input.event.payload.reviewId;
28764
+ return matchesReview(input.reviewId, input.event.id) || payloadReviewId === input.reviewId || matchesSession(input.sessionId, input.event);
28765
+ };
28766
+ var normalizeOperationRecordHref = (basePath, sessionId) => {
28767
+ if (!basePath || !sessionId) {
28768
+ return;
28769
+ }
28770
+ return basePath.includes(":sessionId") ? basePath.replace(":sessionId", encodeURIComponent(sessionId)) : `${basePath.replace(/\/$/, "")}/${encodeURIComponent(sessionId)}`;
28771
+ };
28772
+ var isPostCallAnalysisReport = (value) => ("status" in value) && ("summary" in value) && Array.isArray(value.issues);
28773
+ var buildVoicePostCallAnalysisReport = async (options = {}) => {
28774
+ const reviews = await asArray(options.reviews);
28775
+ const review = options.review ?? reviews.find((candidate) => options.reviewId ? candidate.id === options.reviewId : options.sessionId ? candidate.id.startsWith(`${options.sessionId}:`) : false);
28776
+ const reviewId = options.reviewId ?? review?.id;
28777
+ const sessionId = options.sessionId ?? (reviewId?.endsWith(":review") ? reviewId.slice(0, -":review".length) : undefined);
28778
+ const allTasks = await asArray(options.tasks);
28779
+ const tasks = allTasks.filter((task) => reviewId ? task.reviewId === reviewId || task.intakeId === reviewId || matchesReview(reviewId, task.id) : false);
28780
+ const allIntegrationEvents = await asArray(options.integrationEvents);
28781
+ const integrationEvents = allIntegrationEvents.filter((event) => matchesIntegrationEvent({ event, reviewId, sessionId }));
28782
+ const fieldSource = {
28783
+ extractedFields: options.extractedFields ?? {},
28784
+ review
28785
+ };
28786
+ const fields = (options.fields ?? []).map((field) => {
28787
+ const value = getPathValue3(fieldSource.extractedFields, field.path) ?? getPathValue3(fieldSource, field.path);
28788
+ const required = field.required !== false;
28789
+ return {
28790
+ label: field.label ?? field.path,
28791
+ ok: !required || hasValue2(value),
28792
+ path: field.path,
28793
+ required,
28794
+ value
28795
+ };
28796
+ });
28797
+ const requiredTaskKinds = options.requiredTaskKinds ?? [];
28798
+ const missingTaskKinds = requiredTaskKinds.filter((kind) => !tasks.some((task) => task.kind === kind));
28799
+ const deliveredIntegrationEvents = integrationEvents.filter((event) => event.deliveryStatus === "delivered").length;
28800
+ const failedIntegrationEvents = integrationEvents.filter((event) => event.deliveryStatus === "failed").length;
28801
+ const issues = [];
28802
+ if (!review) {
28803
+ issues.push({
28804
+ code: "voice.post_call_analysis.review_missing",
28805
+ label: "Review missing",
28806
+ severity: "fail"
28807
+ });
28808
+ } else if (review.summary.pass === false) {
28809
+ issues.push({
28810
+ code: "voice.post_call_analysis.review_failed",
28811
+ detail: review.errors.join("; ") || review.summary.outcome,
28812
+ label: "Review failed",
28813
+ severity: "fail"
28814
+ });
28815
+ }
28816
+ for (const field of fields) {
28817
+ if (field.required && !field.ok) {
28818
+ issues.push({
28819
+ code: "voice.post_call_analysis.required_field_missing",
28820
+ detail: field.path,
28821
+ label: `Missing ${field.label}`,
28822
+ severity: "fail"
28823
+ });
28824
+ }
28825
+ }
28826
+ for (const kind of missingTaskKinds) {
28827
+ issues.push({
28828
+ code: "voice.post_call_analysis.required_task_missing",
28829
+ detail: kind,
28830
+ label: `Missing ${kind} task`,
28831
+ severity: "fail"
28832
+ });
28833
+ }
28834
+ if (options.requireDeliveredIntegrationEvent && deliveredIntegrationEvents === 0) {
28835
+ issues.push({
28836
+ code: "voice.post_call_analysis.integration_missing",
28837
+ label: "Delivered integration event missing",
28838
+ severity: "fail"
28839
+ });
28840
+ }
28841
+ if (failedIntegrationEvents > 0) {
28842
+ issues.push({
28843
+ code: "voice.post_call_analysis.integration_failed",
28844
+ detail: `${failedIntegrationEvents} failed integration event(s)`,
28845
+ label: "Integration delivery failed",
28846
+ severity: "warn"
28847
+ });
28848
+ }
28849
+ const status = issues.some((issue) => issue.severity === "fail") ? "fail" : issues.length > 0 ? "warn" : "pass";
28850
+ return {
28851
+ checkedAt: options.at ?? Date.now(),
28852
+ fields,
28853
+ integrationEvents,
28854
+ issues,
28855
+ operationRecordHref: normalizeOperationRecordHref(options.operationRecordBasePath, sessionId),
28856
+ review,
28857
+ reviewId,
28858
+ sessionId,
28859
+ status,
28860
+ summary: {
28861
+ deliveredIntegrationEvents,
28862
+ failedIntegrationEvents,
28863
+ fields: fields.length,
28864
+ missingRequiredFields: fields.filter((field) => field.required && !field.ok).length,
28865
+ missingRequiredTasks: missingTaskKinds.length,
28866
+ requiredFields: fields.filter((field) => field.required).length,
28867
+ requiredTaskKinds: requiredTaskKinds.length,
28868
+ tasks: tasks.length
28869
+ },
28870
+ tasks
28871
+ };
28872
+ };
28873
+ var renderVoicePostCallAnalysisMarkdown = (report) => {
28874
+ const lines = [
28875
+ "# Voice Post-Call Analysis",
28876
+ "",
28877
+ `Status: ${report.status}`,
28878
+ `Checked: ${new Date(report.checkedAt).toISOString()}`,
28879
+ report.reviewId ? `Review: ${report.reviewId}` : undefined,
28880
+ report.sessionId ? `Session: ${report.sessionId}` : undefined,
28881
+ report.operationRecordHref ? `Operations record: ${report.operationRecordHref}` : undefined,
28882
+ "",
28883
+ "## Summary",
28884
+ `- Fields: ${report.summary.fields}`,
28885
+ `- Missing required fields: ${report.summary.missingRequiredFields}`,
28886
+ `- Tasks: ${report.summary.tasks}`,
28887
+ `- Missing required tasks: ${report.summary.missingRequiredTasks}`,
28888
+ `- Delivered integration events: ${report.summary.deliveredIntegrationEvents}`,
28889
+ `- Failed integration events: ${report.summary.failedIntegrationEvents}`,
28890
+ "",
28891
+ "## Issues",
28892
+ ...report.issues.length ? report.issues.map((issue) => `- ${issue.severity}: ${issue.code} - ${issue.label}${issue.detail ? ` (${issue.detail})` : ""}`) : ["- none"]
28893
+ ].filter((line) => line !== undefined);
28894
+ return `${lines.join(`
28895
+ `)}
28896
+ `;
28897
+ };
28898
+ var resolvePostCallAnalysisReport = async (options, input) => {
28899
+ const source = options.source === undefined ? options : typeof options.source === "function" ? await options.source(input) : options.source;
28900
+ const merged = {
28901
+ ...options,
28902
+ ...source,
28903
+ reviewId: input.reviewId ?? source.reviewId ?? options.reviewId,
28904
+ sessionId: input.sessionId ?? source.sessionId ?? options.sessionId
28905
+ };
28906
+ return isPostCallAnalysisReport(merged) ? merged : buildVoicePostCallAnalysisReport(merged);
28907
+ };
28908
+ var createVoicePostCallAnalysisRoutes = (options = {}) => {
28909
+ const path = options.path ?? "/api/voice/post-call-analysis";
28910
+ const routes = new Elysia49({
28911
+ name: options.name ?? "absolutejs-voice-post-call-analysis"
28912
+ });
28913
+ routes.get(path, async ({ query }) => {
28914
+ const report = await resolvePostCallAnalysisReport(options, {
28915
+ reviewId: typeof query.reviewId === "string" ? query.reviewId : undefined,
28916
+ sessionId: typeof query.sessionId === "string" ? query.sessionId : undefined
28917
+ });
28918
+ return Response.json(report, { headers: options.headers });
28919
+ });
28920
+ routes.get(`${path}.md`, async ({ query }) => {
28921
+ const report = await resolvePostCallAnalysisReport(options, {
28922
+ reviewId: typeof query.reviewId === "string" ? query.reviewId : undefined,
28923
+ sessionId: typeof query.sessionId === "string" ? query.sessionId : undefined
28924
+ });
28925
+ return new Response(renderVoicePostCallAnalysisMarkdown(report), {
28926
+ headers: {
28927
+ "content-type": "text/markdown; charset=utf-8",
28928
+ ...options.headers
28929
+ }
28930
+ });
28931
+ });
28932
+ return routes;
28933
+ };
28934
+ // src/guardrails.ts
28935
+ import { Elysia as Elysia50 } from "elysia";
28936
+ var stringifyContent = (value) => typeof value === "string" ? value : JSON.stringify(value) ?? "";
28937
+ var appliesToStage = (rule, stage) => !rule.stages || rule.stages.length === 0 || rule.stages.includes(stage);
28938
+ var matchesRule = async (rule, input) => {
28939
+ if (!appliesToStage(rule, input.stage)) {
28940
+ return false;
28941
+ }
28942
+ if (typeof rule.match === "function") {
28943
+ return rule.match(input);
28944
+ }
28945
+ const content = stringifyContent(input.content);
28946
+ return typeof rule.match === "string" ? content.toLowerCase().includes(rule.match.toLowerCase()) : rule.match.test(content);
28947
+ };
28948
+ var applyRedactions = (content, rules, findings) => {
28949
+ if (typeof content !== "string") {
28950
+ return content;
28951
+ }
28952
+ return findings.reduce((value, finding) => {
28953
+ const rule = rules.find((candidate) => candidate.id === finding.ruleId);
28954
+ if (!rule || !rule.redactWith) {
28955
+ return value;
28956
+ }
28957
+ if (typeof rule.match === "string") {
28958
+ return value.replaceAll(rule.match, rule.redactWith);
28959
+ }
28960
+ if (rule.match instanceof RegExp) {
28961
+ return value.replace(rule.match, rule.redactWith);
28962
+ }
28963
+ return value;
28964
+ }, content);
28965
+ };
28966
+ var evaluateVoiceGuardrailPolicy = async (policy, input) => {
28967
+ const findings = [];
28968
+ for (const rule of policy.rules) {
28969
+ if (!await matchesRule(rule, input)) {
28970
+ continue;
28971
+ }
28972
+ findings.push({
28973
+ action: rule.action ?? policy.defaultAction ?? "block",
28974
+ description: rule.description,
28975
+ label: rule.label ?? rule.id,
28976
+ ruleId: rule.id,
28977
+ stage: input.stage
28978
+ });
28979
+ }
28980
+ const blocked = findings.some((finding) => finding.action === "block");
28981
+ const status = blocked ? "blocked" : findings.length > 0 ? "warn" : "pass";
28982
+ return {
28983
+ allowed: !blocked,
28984
+ checkedAt: Date.now(),
28985
+ content: input.content,
28986
+ findings,
28987
+ redactedContent: applyRedactions(input.content, policy.rules, findings),
28988
+ sessionId: input.sessionId,
28989
+ stage: input.stage,
28990
+ status,
28991
+ turnId: input.turnId
28992
+ };
28993
+ };
28994
+ var buildVoiceGuardrailReport = (input = { decisions: [] }) => {
28995
+ const blocked = input.decisions.filter((decision) => decision.status === "blocked").length;
28996
+ const warned = input.decisions.filter((decision) => decision.status === "warn").length;
28997
+ const passed = input.decisions.filter((decision) => decision.status === "pass").length;
28998
+ const status = blocked > 0 ? "fail" : warned > 0 ? "warn" : "pass";
28999
+ return {
29000
+ checkedAt: Date.now(),
29001
+ decisions: input.decisions,
29002
+ failed: blocked,
29003
+ policies: (input.policies ?? []).map((policy) => ({
29004
+ id: policy.id,
29005
+ label: policy.label,
29006
+ rules: policy.rules.length
29007
+ })),
29008
+ status,
29009
+ summary: {
29010
+ blocked,
29011
+ passed,
29012
+ warned
29013
+ },
29014
+ total: input.decisions.length
29015
+ };
29016
+ };
29017
+ var createVoiceGuardrailPolicy = (policy) => policy;
29018
+ var voiceGuardrailPolicyPresets = {
29019
+ supportSafeDefaults: createVoiceGuardrailPolicy({
29020
+ id: "support-safe-defaults",
29021
+ label: "Support safe defaults",
29022
+ rules: [
29023
+ {
29024
+ description: "Blocks final legal, medical, or financial advice claims that should route to a human or qualified professional.",
29025
+ id: "regulated-advice",
29026
+ label: "Regulated advice",
29027
+ match: /\b(legal advice|medical advice|financial advice|diagnose|prescribe|guaranteed refund|guaranteed approval)\b/i,
29028
+ stages: ["assistant-output"]
29029
+ },
29030
+ {
29031
+ description: "Warns when payment-card-like data appears in transcripts or tool payloads.",
29032
+ action: "warn",
29033
+ id: "payment-card-like-data",
29034
+ label: "Payment card-like data",
29035
+ match: /\b(?:\d[ -]*?){13,19}\b/,
29036
+ redactWith: "[redacted-card]",
29037
+ stages: ["transcript", "tool-input", "tool-output"]
29038
+ }
29039
+ ]
29040
+ })
29041
+ };
29042
+ var renderVoiceGuardrailMarkdown = (report) => {
29043
+ const lines = [
29044
+ "# Voice Guardrail Report",
29045
+ "",
29046
+ `Status: ${report.status}`,
29047
+ `Checked: ${new Date(report.checkedAt).toISOString()}`,
29048
+ `Decisions: ${report.total}`,
29049
+ `Blocked: ${report.summary.blocked}`,
29050
+ `Warned: ${report.summary.warned}`,
29051
+ "",
29052
+ "## Decisions",
29053
+ ...report.decisions.length > 0 ? report.decisions.map((decision) => `- ${decision.status}: ${decision.stage}${decision.sessionId ? ` session=${decision.sessionId}` : ""} findings=${decision.findings.length}`) : ["- none"]
29054
+ ];
29055
+ return `${lines.join(`
29056
+ `)}
29057
+ `;
29058
+ };
29059
+ var isGuardrailReport = (value) => ("decisions" in value) && ("summary" in value);
29060
+ var normalizeGuardrailRouteInput = async (request) => {
29061
+ if (request.method === "POST") {
29062
+ return await request.json().catch(() => ({}));
29063
+ }
29064
+ const url = new URL(request.url);
29065
+ return {
29066
+ content: url.searchParams.get("content") ?? "",
29067
+ sessionId: url.searchParams.get("sessionId") ?? undefined,
29068
+ stage: url.searchParams.get("stage") ?? "assistant-output",
29069
+ turnId: url.searchParams.get("turnId") ?? undefined
29070
+ };
29071
+ };
29072
+ var resolveGuardrailReport = async (options, input) => {
29073
+ if (options.source !== undefined) {
29074
+ const value = typeof options.source === "function" ? await options.source(input) : options.source;
29075
+ return isGuardrailReport(value) ? value : buildVoiceGuardrailReport({ decisions: [value] });
29076
+ }
29077
+ const decisions = await Promise.all((options.policies ?? []).map((policy) => evaluateVoiceGuardrailPolicy(policy, input)));
29078
+ return buildVoiceGuardrailReport({
29079
+ decisions,
29080
+ policies: options.policies
29081
+ });
29082
+ };
29083
+ var createVoiceGuardrailRoutes = (options = {}) => {
29084
+ const path = options.path ?? "/api/voice/guardrails";
29085
+ const routes = new Elysia50({
29086
+ name: options.name ?? "absolutejs-voice-guardrails"
29087
+ });
29088
+ routes.all(path, async ({ request }) => {
29089
+ const input = await normalizeGuardrailRouteInput(request);
29090
+ const report = await resolveGuardrailReport(options, input);
29091
+ if (options.trace) {
29092
+ await Promise.all(report.decisions.map((decision) => options.trace.append({
29093
+ at: decision.checkedAt,
29094
+ payload: {
29095
+ allowed: decision.allowed,
29096
+ findings: decision.findings,
29097
+ stage: decision.stage,
29098
+ status: decision.status
29099
+ },
29100
+ sessionId: decision.sessionId ?? "guardrail-check",
29101
+ turnId: decision.turnId,
29102
+ type: "assistant.guardrail"
29103
+ })));
29104
+ }
29105
+ return Response.json(report, { headers: options.headers });
29106
+ });
29107
+ routes.all(`${path}.md`, async ({ request }) => {
29108
+ const input = await normalizeGuardrailRouteInput(request);
29109
+ const report = await resolveGuardrailReport(options, input);
29110
+ return new Response(renderVoiceGuardrailMarkdown(report), {
29111
+ headers: {
29112
+ "content-type": "text/markdown; charset=utf-8",
29113
+ ...options.headers
29114
+ }
29115
+ });
29116
+ });
29117
+ return routes;
29118
+ };
28730
29119
  // src/correction.ts
28731
29120
  var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28732
29121
  var buildAliasMatcher = (alias) => new RegExp(`(?<![\\p{L}\\p{N}'])${escapeRegExp(alias)}(?![\\p{L}\\p{N}'])`, "giu");
@@ -29119,6 +29508,7 @@ export {
29119
29508
  voiceTelephonyOutcomeToRouteResult,
29120
29509
  voiceObservabilityExportSchemaVersion,
29121
29510
  voiceObservabilityExportSchemaId,
29511
+ voiceGuardrailPolicyPresets,
29122
29512
  voiceComplianceRedactionDefaults,
29123
29513
  voice,
29124
29514
  verifyVoiceTwilioWebhookSignature,
@@ -29219,6 +29609,7 @@ export {
29219
29609
  renderVoiceProviderContractMatrixHTML,
29220
29610
  renderVoiceProviderCapabilityHTML,
29221
29611
  renderVoiceProductionReadinessHTML,
29612
+ renderVoicePostCallAnalysisMarkdown,
29222
29613
  renderVoicePhoneAgentProductionSmokeHTML,
29223
29614
  renderVoiceOutcomeContractHTML,
29224
29615
  renderVoiceOpsStatusHTML,
@@ -29233,6 +29624,7 @@ export {
29233
29624
  renderVoiceLiveLatencyHTML,
29234
29625
  renderVoiceLatencySLOMarkdown,
29235
29626
  renderVoiceHandoffHealthHTML,
29627
+ renderVoiceGuardrailMarkdown,
29236
29628
  renderVoiceEvalHTML,
29237
29629
  renderVoiceEvalBaselineHTML,
29238
29630
  renderVoiceDemoReadyHTML,
@@ -29292,6 +29684,7 @@ export {
29292
29684
  evaluateVoiceTelephonyContract,
29293
29685
  evaluateVoiceQuality,
29294
29686
  evaluateVoiceProviderStackGaps,
29687
+ evaluateVoiceGuardrailPolicy,
29295
29688
  encodeTwilioMulawBase64,
29296
29689
  deliverVoiceTraceEventsToSinks,
29297
29690
  deliverVoiceObservabilityExport,
@@ -29414,6 +29807,7 @@ export {
29414
29807
  createVoicePostgresCampaignStore,
29415
29808
  createVoicePostgresAuditSinkDeliveryStore,
29416
29809
  createVoicePostgresAuditEventStore,
29810
+ createVoicePostCallAnalysisRoutes,
29417
29811
  createVoicePlivoCampaignDialer,
29418
29812
  createVoicePlatformCoverageRoutes,
29419
29813
  createVoicePhoneAgentProductionSmokeRoutes,
@@ -29470,6 +29864,8 @@ export {
29470
29864
  createVoiceHandoffDeliveryWorkerLoop,
29471
29865
  createVoiceHandoffDeliveryWorker,
29472
29866
  createVoiceHandoffDeliveryRecord,
29867
+ createVoiceGuardrailRoutes,
29868
+ createVoiceGuardrailPolicy,
29473
29869
  createVoiceFileTraceSinkDeliveryStore,
29474
29870
  createVoiceFileTraceEventStore,
29475
29871
  createVoiceFileTaskStore,
@@ -29570,6 +29966,7 @@ export {
29570
29966
  buildVoiceProofTrendReport,
29571
29967
  buildVoiceProductionReadinessReport,
29572
29968
  buildVoiceProductionReadinessGate,
29969
+ buildVoicePostCallAnalysisReport,
29573
29970
  buildVoicePlatformCoverageSummary,
29574
29971
  buildVoiceOpsTaskFromSLABreach,
29575
29972
  buildVoiceOpsTaskFromReview,
@@ -29585,6 +29982,7 @@ export {
29585
29982
  buildVoiceLiveOpsControlState,
29586
29983
  buildVoiceLatencySLOGate,
29587
29984
  buildVoiceIncidentBundle,
29985
+ buildVoiceGuardrailReport,
29588
29986
  buildVoiceDiagnosticsMarkdown,
29589
29987
  buildVoiceDemoReadyReport,
29590
29988
  buildVoiceDeliverySinkReport,
@@ -0,0 +1,98 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { StoredVoiceIntegrationEvent, StoredVoiceOpsTask, VoiceIntegrationEventStore, VoiceOpsTaskKind, VoiceOpsTaskStore } from './ops';
3
+ import type { StoredVoiceCallReviewArtifact, VoiceCallReviewStore } from './testing/review';
4
+ export type VoicePostCallAnalysisStatus = 'fail' | 'pass' | 'warn';
5
+ export type VoicePostCallAnalysisFieldRequirement = {
6
+ label?: string;
7
+ path: string;
8
+ required?: boolean;
9
+ };
10
+ export type VoicePostCallAnalysisFieldResult = {
11
+ label: string;
12
+ ok: boolean;
13
+ path: string;
14
+ required: boolean;
15
+ value?: unknown;
16
+ };
17
+ export type VoicePostCallAnalysisIssueCode = 'voice.post_call_analysis.integration_failed' | 'voice.post_call_analysis.integration_missing' | 'voice.post_call_analysis.required_field_missing' | 'voice.post_call_analysis.required_task_missing' | 'voice.post_call_analysis.review_failed' | 'voice.post_call_analysis.review_missing';
18
+ export type VoicePostCallAnalysisIssue = {
19
+ code: VoicePostCallAnalysisIssueCode;
20
+ detail?: string;
21
+ label: string;
22
+ severity: Exclude<VoicePostCallAnalysisStatus, 'pass'>;
23
+ };
24
+ export type VoicePostCallAnalysisReport = {
25
+ checkedAt: number;
26
+ fields: VoicePostCallAnalysisFieldResult[];
27
+ integrationEvents: StoredVoiceIntegrationEvent[];
28
+ issues: VoicePostCallAnalysisIssue[];
29
+ operationRecordHref?: string;
30
+ review?: StoredVoiceCallReviewArtifact;
31
+ reviewId?: string;
32
+ sessionId?: string;
33
+ status: VoicePostCallAnalysisStatus;
34
+ summary: {
35
+ deliveredIntegrationEvents: number;
36
+ failedIntegrationEvents: number;
37
+ fields: number;
38
+ missingRequiredFields: number;
39
+ missingRequiredTasks: number;
40
+ requiredFields: number;
41
+ requiredTaskKinds: number;
42
+ tasks: number;
43
+ };
44
+ tasks: StoredVoiceOpsTask[];
45
+ };
46
+ export type VoicePostCallAnalysisOptions = {
47
+ at?: number;
48
+ extractedFields?: Record<string, unknown>;
49
+ fields?: VoicePostCallAnalysisFieldRequirement[];
50
+ integrationEvents?: StoredVoiceIntegrationEvent[] | VoiceIntegrationEventStore;
51
+ operationRecordBasePath?: string;
52
+ requireDeliveredIntegrationEvent?: boolean;
53
+ requiredTaskKinds?: VoiceOpsTaskKind[];
54
+ review?: StoredVoiceCallReviewArtifact;
55
+ reviewId?: string;
56
+ reviews?: StoredVoiceCallReviewArtifact[] | VoiceCallReviewStore;
57
+ sessionId?: string;
58
+ tasks?: StoredVoiceOpsTask[] | VoiceOpsTaskStore;
59
+ };
60
+ export type VoicePostCallAnalysisRoutesOptions = VoicePostCallAnalysisOptions & {
61
+ headers?: HeadersInit;
62
+ name?: string;
63
+ path?: string;
64
+ source?: ((input: {
65
+ reviewId?: string;
66
+ sessionId?: string;
67
+ }) => Promise<VoicePostCallAnalysisOptions | VoicePostCallAnalysisReport> | VoicePostCallAnalysisOptions | VoicePostCallAnalysisReport) | VoicePostCallAnalysisOptions | VoicePostCallAnalysisReport;
68
+ };
69
+ export declare const buildVoicePostCallAnalysisReport: (options?: VoicePostCallAnalysisOptions) => Promise<VoicePostCallAnalysisReport>;
70
+ export declare const renderVoicePostCallAnalysisMarkdown: (report: VoicePostCallAnalysisReport) => string;
71
+ export declare const createVoicePostCallAnalysisRoutes: (options?: VoicePostCallAnalysisRoutesOptions) => Elysia<"", {
72
+ decorator: {};
73
+ store: {};
74
+ derive: {};
75
+ resolve: {};
76
+ }, {
77
+ typebox: {};
78
+ error: {};
79
+ }, {
80
+ schema: {};
81
+ standaloneSchema: {};
82
+ macro: {};
83
+ macroFn: {};
84
+ parser: {};
85
+ response: {};
86
+ }, {}, {
87
+ derive: {};
88
+ resolve: {};
89
+ schema: {};
90
+ standaloneSchema: {};
91
+ response: {};
92
+ }, {
93
+ derive: {};
94
+ resolve: {};
95
+ schema: {};
96
+ standaloneSchema: {};
97
+ response: {};
98
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.243",
3
+ "version": "0.0.22-beta.245",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",