@absolutejs/voice 0.0.22-beta.534 → 0.0.22-beta.535
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/angular/index.js +347 -347
- package/dist/angular/voice-cost-dashboard.service.d.ts +1 -1
- package/dist/angular/voice-replay-timeline.service.d.ts +1 -1
- package/dist/client/agentSquadStatusWidget.d.ts +3 -3
- package/dist/client/browserVoiceSupport.d.ts +1 -1
- package/dist/client/callDebugger.d.ts +2 -2
- package/dist/client/callDebuggerWidget.d.ts +3 -3
- package/dist/client/campaignDialerProof.d.ts +4 -4
- package/dist/client/deliveryRuntime.d.ts +4 -4
- package/dist/client/deliveryRuntimeWidget.d.ts +3 -3
- package/dist/client/htmxAttributes.d.ts +1 -5
- package/dist/client/htmxBootstrap.js +82 -82
- package/dist/client/htmxDashboardRenderers.d.ts +17 -17
- package/dist/client/index.js +2478 -2484
- package/dist/client/liveOps.d.ts +2 -2
- package/dist/client/liveOpsWidget.d.ts +3 -3
- package/dist/client/opsActionCenter.d.ts +3 -3
- package/dist/client/opsActionCenterWidget.d.ts +3 -3
- package/dist/client/opsActionHistory.d.ts +2 -2
- package/dist/client/opsActionHistoryWidget.d.ts +2 -2
- package/dist/client/opsStatus.d.ts +2 -2
- package/dist/client/opsStatusWidget.d.ts +4 -4
- package/dist/client/platformCoverage.d.ts +2 -2
- package/dist/client/platformCoverageWidget.d.ts +3 -3
- package/dist/client/profileComparison.d.ts +2 -2
- package/dist/client/profileComparisonWidget.d.ts +3 -3
- package/dist/client/profileSwitchRecommendation.d.ts +2 -2
- package/dist/client/profileSwitchRecommendationWidget.d.ts +3 -3
- package/dist/client/proofTrends.d.ts +2 -2
- package/dist/client/proofTrendsWidget.d.ts +3 -3
- package/dist/client/providerCapabilities.d.ts +2 -2
- package/dist/client/providerCapabilitiesWidget.d.ts +3 -3
- package/dist/client/providerContracts.d.ts +2 -2
- package/dist/client/providerContractsWidget.d.ts +3 -3
- package/dist/client/providerSimulationControls.d.ts +1 -1
- package/dist/client/providerSimulationControlsWidget.d.ts +4 -4
- package/dist/client/providerStatus.d.ts +2 -2
- package/dist/client/providerStatusWidget.d.ts +3 -3
- package/dist/client/readinessFailures.d.ts +2 -2
- package/dist/client/readinessFailuresWidget.d.ts +3 -3
- package/dist/client/reconnectProfileEvidence.d.ts +2 -2
- package/dist/client/reconnectProfileEvidenceWidget.d.ts +3 -3
- package/dist/client/routingStatus.d.ts +2 -2
- package/dist/client/routingStatusWidget.d.ts +3 -3
- package/dist/client/sessionObservability.d.ts +2 -2
- package/dist/client/sessionObservabilityWidget.d.ts +3 -3
- package/dist/client/sessionSnapshot.d.ts +2 -2
- package/dist/client/sessionSnapshotWidget.d.ts +2 -2
- package/dist/client/traceTimeline.d.ts +2 -2
- package/dist/client/traceTimelineWidget.d.ts +3 -3
- package/dist/client/turnLatency.d.ts +4 -4
- package/dist/client/turnLatencyWidget.d.ts +3 -3
- package/dist/client/turnQuality.d.ts +2 -2
- package/dist/client/turnQualityWidget.d.ts +3 -3
- package/dist/client/workflowStatus.d.ts +2 -2
- package/dist/core/agent.d.ts +1 -1
- package/dist/core/agentSquadContract.d.ts +2 -2
- package/dist/core/agentState.d.ts +1 -1
- package/dist/core/aiScorecard.d.ts +1 -1
- package/dist/core/assistant.d.ts +1 -1
- package/dist/core/assistantHealth.d.ts +3 -3
- package/dist/core/assistantMemory.d.ts +7 -7
- package/dist/core/audioConditioning.d.ts +1 -1
- package/dist/core/audit.d.ts +6 -6
- package/dist/core/auditDeliveryRoutes.d.ts +7 -7
- package/dist/core/auditExport.d.ts +10 -10
- package/dist/core/auditRoutes.d.ts +5 -5
- package/dist/core/auditSinks.d.ts +7 -7
- package/dist/core/bargeInRoutes.d.ts +6 -6
- package/dist/core/bookingFlow.d.ts +1 -1
- package/dist/core/browserCallProfiles.d.ts +3 -3
- package/dist/core/browserMediaRoutes.d.ts +5 -5
- package/dist/core/callDebugger.d.ts +1 -1
- package/dist/core/callDisposition.d.ts +1 -1
- package/dist/core/callScorecard.d.ts +1 -1
- package/dist/core/campaign.d.ts +5 -5
- package/dist/core/campaignControls.d.ts +1 -1
- package/dist/core/campaignDialers.d.ts +4 -4
- package/dist/core/campaignTemplate.d.ts +1 -1
- package/dist/core/competitiveCoverage.d.ts +2 -2
- package/dist/core/conversationSimulator.d.ts +1 -1
- package/dist/core/correction.d.ts +1 -1
- package/dist/core/dataControl.d.ts +5 -5
- package/dist/core/deliveryRuntime.d.ts +7 -7
- package/dist/core/deliverySinkRoutes.d.ts +7 -7
- package/dist/core/demoReadyRoutes.d.ts +1 -1
- package/dist/core/dncRegistry.d.ts +1 -1
- package/dist/core/dtmfCollector.d.ts +1 -1
- package/dist/core/evalRoutes.d.ts +16 -16
- package/dist/core/fileStore.d.ts +18 -18
- package/dist/core/guardrails.d.ts +2 -2
- package/dist/core/handoff.d.ts +4 -4
- package/dist/core/handoffHealth.d.ts +4 -4
- package/dist/core/htmx.d.ts +1 -1
- package/dist/core/incidentBundle.d.ts +1 -1
- package/dist/core/incidentTimeline.d.ts +11 -11
- package/dist/core/latencySlo.d.ts +1 -1
- package/dist/core/liveCoach.d.ts +1 -1
- package/dist/core/liveLatency.d.ts +3 -3
- package/dist/core/liveOps.d.ts +6 -6
- package/dist/core/mediaPipelineRoutes.d.ts +4 -4
- package/dist/core/monitor.d.ts +1 -1
- package/dist/core/multilingualProof.d.ts +1 -1
- package/dist/core/observabilityExport.d.ts +15 -15
- package/dist/core/operationalStatus.d.ts +3 -3
- package/dist/core/operationsRecord.d.ts +8 -8
- package/dist/core/ops.d.ts +58 -58
- package/dist/core/opsActionAuditRoutes.d.ts +10 -10
- package/dist/core/opsConsoleRoutes.d.ts +3 -3
- package/dist/core/opsRecovery.d.ts +4 -4
- package/dist/core/opsSinks.d.ts +6 -6
- package/dist/core/opsStatusRoutes.d.ts +3 -3
- package/dist/core/opsWebhook.d.ts +9 -9
- package/dist/core/otelExporter.d.ts +1 -1
- package/dist/core/outcomeContract.d.ts +6 -6
- package/dist/core/pathway.d.ts +2 -2
- package/dist/core/pathwayRuntime.d.ts +2 -2
- package/dist/core/phoneAgent.d.ts +2 -2
- package/dist/core/phoneAgentProductionSmoke.d.ts +7 -7
- package/dist/core/platformCoverage.d.ts +1 -1
- package/dist/core/postCallSurvey.d.ts +1 -1
- package/dist/core/postgresStore.d.ts +8 -8
- package/dist/core/productionReadiness.d.ts +9 -9
- package/dist/core/profileSwitchRecommendation.d.ts +9 -9
- package/dist/core/proofAssertions.d.ts +1 -1
- package/dist/core/proofPack.d.ts +12 -12
- package/dist/core/proofRunner.d.ts +2 -2
- package/dist/core/proofTrends.d.ts +26 -26
- package/dist/core/providerCapabilities.d.ts +5 -5
- package/dist/core/providerDecisionTraces.d.ts +4 -4
- package/dist/core/providerHealth.d.ts +3 -3
- package/dist/core/providerOrchestration.d.ts +4 -4
- package/dist/core/providerRouterTraces.d.ts +2 -2
- package/dist/core/providerRoutingContract.d.ts +2 -2
- package/dist/core/providerSlo.d.ts +5 -5
- package/dist/core/providerStackRecommendations.d.ts +8 -8
- package/dist/core/qualityRoutes.d.ts +3 -3
- package/dist/core/queue.d.ts +26 -26
- package/dist/core/realtimeChannel.d.ts +5 -5
- package/dist/core/realtimeProviderContracts.d.ts +3 -3
- package/dist/core/reconnectContract.d.ts +16 -16
- package/dist/core/recordingStore.d.ts +2 -2
- package/dist/core/reminderScheduler.d.ts +1 -1
- package/dist/core/resilienceRoutes.d.ts +1 -1
- package/dist/core/routing.d.ts +1 -1
- package/dist/core/sessionObservability.d.ts +2 -2
- package/dist/core/sessionReplay.d.ts +12 -12
- package/dist/core/sessionSnapshot.d.ts +1 -1
- package/dist/core/simulationSuite.d.ts +4 -4
- package/dist/core/sloCalibration.d.ts +12 -12
- package/dist/core/sqliteStore.d.ts +8 -8
- package/dist/core/telephonyMediaRoutes.d.ts +4 -4
- package/dist/core/telephonyOutcome.d.ts +2 -2
- package/dist/core/toolContract.d.ts +10 -10
- package/dist/core/toolRuntime.d.ts +1 -1
- package/dist/core/trace.d.ts +18 -18
- package/dist/core/traceDeliveryRoutes.d.ts +7 -7
- package/dist/core/traceTimeline.d.ts +3 -3
- package/dist/core/turnLatency.d.ts +4 -4
- package/dist/core/turnQuality.d.ts +5 -5
- package/dist/core/types.d.ts +7 -2
- package/dist/core/voiceMonitoring.d.ts +11 -11
- package/dist/core/webhookVerification.d.ts +4 -4
- package/dist/core/whisperChannel.d.ts +4 -4
- package/dist/core/workflowContract.d.ts +13 -13
- package/dist/core/zeroDataRetention.d.ts +3 -13
- package/dist/drizzle/assistantMemory.d.ts +95 -0
- package/dist/drizzle/eval.d.ts +61 -0
- package/dist/drizzle/handoff.d.ts +62 -0
- package/dist/drizzle/index.d.ts +1029 -0
- package/dist/drizzle/index.js +3028 -0
- package/dist/drizzle/observabilityExport.d.ts +61 -0
- package/dist/drizzle/proofTrends.d.ts +126 -0
- package/dist/drizzle/runtimeStorage.d.ts +1311 -0
- package/dist/drizzle/shared.d.ts +75 -0
- package/dist/embed/index.js +72 -72
- package/dist/embed/voice-widget.js +2 -2
- package/dist/index.js +7034 -7101
- package/dist/react/index.js +2148 -2150
- package/dist/svelte/createVoiceAgentSquadStatus.d.ts +2 -2
- package/dist/svelte/createVoiceCallDebugger.d.ts +1 -1
- package/dist/svelte/createVoiceCallPlayer.d.ts +8 -8
- package/dist/svelte/createVoiceCampaignDialerProof.d.ts +2 -2
- package/dist/svelte/createVoiceCostDashboard.d.ts +4 -4
- package/dist/svelte/createVoiceDeliveryRuntime.d.ts +3 -3
- package/dist/svelte/createVoiceLiveAgentConsole.d.ts +4 -4
- package/dist/svelte/createVoiceLiveOps.d.ts +4 -4
- package/dist/svelte/createVoiceOpsActionCenter.d.ts +2 -2
- package/dist/svelte/createVoiceOpsStatus.d.ts +2 -2
- package/dist/svelte/createVoiceProviderCapabilities.d.ts +1 -1
- package/dist/svelte/createVoiceProviderContracts.d.ts +1 -1
- package/dist/svelte/createVoiceProviderSimulationControls.d.ts +1 -1
- package/dist/svelte/createVoiceProviderStatus.d.ts +1 -1
- package/dist/svelte/createVoiceReplayTimeline.d.ts +1 -1
- package/dist/svelte/createVoiceRoutingStatus.d.ts +1 -1
- package/dist/svelte/createVoiceSessionObservability.d.ts +1 -1
- package/dist/svelte/createVoiceSessionSnapshot.d.ts +1 -1
- package/dist/svelte/createVoiceTraceTimeline.d.ts +1 -1
- package/dist/svelte/createVoiceTurnLatency.d.ts +2 -2
- package/dist/svelte/createVoiceTurnQuality.d.ts +1 -1
- package/dist/svelte/createVoiceWidget.d.ts +2 -2
- package/dist/svelte/createVoiceWorkflowStatus.d.ts +1 -1
- package/dist/svelte/index.js +1518 -1522
- package/dist/telephony/matrix.d.ts +3 -3
- package/dist/telephony/plivo.d.ts +2 -2
- package/dist/telephony/security.d.ts +3 -3
- package/dist/telephony/telnyx.d.ts +1 -1
- package/dist/telephony/twilio.d.ts +2 -2
- package/dist/testing/benchmark.d.ts +6 -6
- package/dist/testing/corrected.d.ts +4 -4
- package/dist/testing/duplex.d.ts +2 -2
- package/dist/testing/fixtures.d.ts +1 -1
- package/dist/testing/index.js +1382 -1388
- package/dist/testing/review.d.ts +4 -4
- package/dist/testing/sessionBenchmark.d.ts +10 -10
- package/dist/testing/telephony.d.ts +3 -3
- package/dist/testing/tts.d.ts +1 -1
- package/dist/vue/VoiceCostDashboard.d.ts +2 -2
- package/dist/vue/index.js +2110 -2112
- package/dist/vue/useVoiceController.d.ts +5 -5
- package/dist/vue/useVoiceDeliveryRuntime.d.ts +1 -1
- package/dist/vue/useVoiceStream.d.ts +5 -5
- package/package.json +28 -6
package/dist/testing/index.js
CHANGED
|
@@ -512,8 +512,8 @@ var scoreSpeakerTurns = (fixture, result) => {
|
|
|
512
512
|
const available = scoredTurns.every((turn) => turn.speaker !== undefined);
|
|
513
513
|
if (!available) {
|
|
514
514
|
return {
|
|
515
|
-
available: false,
|
|
516
515
|
actualTurnCount: scoredTurns.length,
|
|
516
|
+
available: false,
|
|
517
517
|
expectedTurnCount: expectedTurns.length,
|
|
518
518
|
passes: false,
|
|
519
519
|
patternMatchRate: 0,
|
|
@@ -531,8 +531,8 @@ var scoreSpeakerTurns = (fixture, result) => {
|
|
|
531
531
|
}
|
|
532
532
|
const patternMatchRate = roundMetric(matches / maxLength) ?? 0;
|
|
533
533
|
return {
|
|
534
|
-
available: true,
|
|
535
534
|
actualTurnCount: scoredTurns.length,
|
|
535
|
+
available: true,
|
|
536
536
|
expectedTurnCount: expectedTurns.length,
|
|
537
537
|
passes: scoredTurns.length === expectedTurns.length && patternMatchRate === 1,
|
|
538
538
|
patternMatchRate,
|
|
@@ -622,37 +622,33 @@ var toFixtureBenchmarkResult = (fixture, result, elapsedMs) => {
|
|
|
622
622
|
title: fixture.title
|
|
623
623
|
};
|
|
624
624
|
};
|
|
625
|
-
var
|
|
626
|
-
const
|
|
627
|
-
|
|
625
|
+
var compareSTTBenchmarks = (reports) => {
|
|
626
|
+
const entries = reports.map((report) => ({
|
|
627
|
+
adapterId: report.adapterId,
|
|
628
|
+
summary: report.summary
|
|
629
|
+
}));
|
|
630
|
+
const bestByMetric = (selectMetric, direction) => entries.reduce((best, entry) => {
|
|
631
|
+
if (!best) {
|
|
632
|
+
return entry;
|
|
633
|
+
}
|
|
634
|
+
const next = selectMetric(entry);
|
|
635
|
+
const current = selectMetric(best);
|
|
636
|
+
if (direction === "max" ? next > current : next < current) {
|
|
637
|
+
return entry;
|
|
638
|
+
}
|
|
639
|
+
return best;
|
|
640
|
+
}, undefined);
|
|
628
641
|
return {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
averageFinalCount: roundMetric(average(fixtures.map((fixture) => fixture.finalCount)), 2) ?? 0,
|
|
634
|
-
averageSpeakerTurnMatchRate: roundMetric(average(fixtures.map((fixture) => fixture.speakerTurns?.patternMatchRate))),
|
|
635
|
-
averageTermRecall: roundMetric(average(fixtures.map((fixture) => fixture.expectedTerms.recall))) ?? 0,
|
|
636
|
-
averagePostSpeechTimeToEndOfTurnMs: roundMetric(average(fixtures.map((fixture) => fixture.postSpeechTimeToEndOfTurnMs)), 2),
|
|
637
|
-
averagePostSpeechTimeToFirstFinalMs: roundMetric(average(fixtures.map((fixture) => fixture.postSpeechTimeToFirstFinalMs)), 2),
|
|
638
|
-
averageTimeToEndOfTurnMs: roundMetric(average(fixtures.map((fixture) => fixture.timeToEndOfTurnMs)), 2),
|
|
639
|
-
averageTimeToFirstFinalMs: roundMetric(average(fixtures.map((fixture) => fixture.timeToFirstFinalMs)), 2),
|
|
640
|
-
averageTimeToFirstPartialMs: roundMetric(average(fixtures.map((fixture) => fixture.timeToFirstPartialMs)), 2),
|
|
641
|
-
averageWordErrorRate: roundMetric(average(fixtures.map((fixture) => fixture.accuracy.wordErrorRate))) ?? 0,
|
|
642
|
-
fixtureCount,
|
|
643
|
-
fixturesWithErrors: fixtures.filter((fixture) => fixture.errorCount > 0).length,
|
|
644
|
-
fixturesWithFragmentation: fixtures.filter((fixture) => fixture.fragmentationCount > 0).length,
|
|
645
|
-
groupSummaries: calculateGroupSummary(fixtures),
|
|
646
|
-
passCount,
|
|
647
|
-
passRate: fixtureCount > 0 ? roundMetric(passCount / fixtureCount) ?? 0 : 0,
|
|
648
|
-
totalErrorCount: fixtures.reduce((sum, fixture) => sum + fixture.errorCount, 0),
|
|
649
|
-
wordAccuracyRate: fixtureCount > 0 ? roundMetric(1 - (average(fixtures.map((fixture) => fixture.accuracy.wordErrorRate)) ?? 0)) ?? 0 : 0
|
|
642
|
+
bestByPassRate: bestByMetric((entry) => entry.summary.passRate, "max"),
|
|
643
|
+
bestByTermRecall: bestByMetric((entry) => entry.summary.averageTermRecall, "max"),
|
|
644
|
+
bestByWordErrorRate: bestByMetric((entry) => entry.summary.averageWordErrorRate, "min"),
|
|
645
|
+
entries
|
|
650
646
|
};
|
|
651
647
|
};
|
|
652
648
|
var evaluateSTTBenchmarkAcceptance = (report, thresholds = {}) => {
|
|
653
649
|
const failures = [];
|
|
654
650
|
const details = thresholds;
|
|
655
|
-
const overallPassRate = details
|
|
651
|
+
const { overallPassRate } = details;
|
|
656
652
|
if (overallPassRate !== undefined && report.summary.passRate < overallPassRate) {
|
|
657
653
|
failures.push(`overall passRate ${(report.summary.passRate * 100).toFixed(2)}% below ${(overallPassRate * 100).toFixed(2)}%`);
|
|
658
654
|
}
|
|
@@ -687,29 +683,6 @@ var evaluateSTTBenchmarkAcceptance = (report, thresholds = {}) => {
|
|
|
687
683
|
score
|
|
688
684
|
};
|
|
689
685
|
};
|
|
690
|
-
var compareSTTBenchmarks = (reports) => {
|
|
691
|
-
const entries = reports.map((report) => ({
|
|
692
|
-
adapterId: report.adapterId,
|
|
693
|
-
summary: report.summary
|
|
694
|
-
}));
|
|
695
|
-
const bestByMetric = (selectMetric, direction) => entries.reduce((best, entry) => {
|
|
696
|
-
if (!best) {
|
|
697
|
-
return entry;
|
|
698
|
-
}
|
|
699
|
-
const next = selectMetric(entry);
|
|
700
|
-
const current = selectMetric(best);
|
|
701
|
-
if (direction === "max" ? next > current : next < current) {
|
|
702
|
-
return entry;
|
|
703
|
-
}
|
|
704
|
-
return best;
|
|
705
|
-
}, undefined);
|
|
706
|
-
return {
|
|
707
|
-
bestByPassRate: bestByMetric((entry) => entry.summary.passRate, "max"),
|
|
708
|
-
bestByTermRecall: bestByMetric((entry) => entry.summary.averageTermRecall, "max"),
|
|
709
|
-
bestByWordErrorRate: bestByMetric((entry) => entry.summary.averageWordErrorRate, "min"),
|
|
710
|
-
entries
|
|
711
|
-
};
|
|
712
|
-
};
|
|
713
686
|
var runSTTAdapterBenchmark = async ({
|
|
714
687
|
adapter,
|
|
715
688
|
adapterId,
|
|
@@ -732,6 +705,55 @@ var runSTTAdapterBenchmark = async ({
|
|
|
732
705
|
summary: summarizeSTTBenchmark(adapterId, results)
|
|
733
706
|
};
|
|
734
707
|
};
|
|
708
|
+
var runSTTAdapterBenchmarkSeries = async ({
|
|
709
|
+
adapter,
|
|
710
|
+
adapterId,
|
|
711
|
+
fixtures,
|
|
712
|
+
options = {},
|
|
713
|
+
runs
|
|
714
|
+
}) => {
|
|
715
|
+
const reports = [];
|
|
716
|
+
const runCount = Math.max(1, Math.floor(runs));
|
|
717
|
+
for (let runIndex = 0;runIndex < runCount; runIndex += 1) {
|
|
718
|
+
reports.push(await runSTTAdapterBenchmark({
|
|
719
|
+
adapter,
|
|
720
|
+
adapterId,
|
|
721
|
+
fixtures,
|
|
722
|
+
options
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
725
|
+
return summarizeSTTBenchmarkSeries({
|
|
726
|
+
adapterId,
|
|
727
|
+
reports
|
|
728
|
+
});
|
|
729
|
+
};
|
|
730
|
+
var summarizeSTTBenchmark = (adapterId, fixtures) => {
|
|
731
|
+
const fixtureCount = fixtures.length;
|
|
732
|
+
const passCount = fixtures.filter((fixture) => fixture.passes).length;
|
|
733
|
+
return {
|
|
734
|
+
adapterId,
|
|
735
|
+
averageCharErrorRate: roundMetric(average(fixtures.map((fixture) => fixture.accuracy.charErrorRate))) ?? 0,
|
|
736
|
+
averageElapsedMs: roundMetric(average(fixtures.map((fixture) => fixture.elapsedMs)), 2) ?? 0,
|
|
737
|
+
averageEndOfTurnCount: roundMetric(average(fixtures.map((fixture) => fixture.endOfTurnCount)), 2) ?? 0,
|
|
738
|
+
averageFinalCount: roundMetric(average(fixtures.map((fixture) => fixture.finalCount)), 2) ?? 0,
|
|
739
|
+
averageSpeakerTurnMatchRate: roundMetric(average(fixtures.map((fixture) => fixture.speakerTurns?.patternMatchRate))),
|
|
740
|
+
averageTermRecall: roundMetric(average(fixtures.map((fixture) => fixture.expectedTerms.recall))) ?? 0,
|
|
741
|
+
averagePostSpeechTimeToEndOfTurnMs: roundMetric(average(fixtures.map((fixture) => fixture.postSpeechTimeToEndOfTurnMs)), 2),
|
|
742
|
+
averagePostSpeechTimeToFirstFinalMs: roundMetric(average(fixtures.map((fixture) => fixture.postSpeechTimeToFirstFinalMs)), 2),
|
|
743
|
+
averageTimeToEndOfTurnMs: roundMetric(average(fixtures.map((fixture) => fixture.timeToEndOfTurnMs)), 2),
|
|
744
|
+
averageTimeToFirstFinalMs: roundMetric(average(fixtures.map((fixture) => fixture.timeToFirstFinalMs)), 2),
|
|
745
|
+
averageTimeToFirstPartialMs: roundMetric(average(fixtures.map((fixture) => fixture.timeToFirstPartialMs)), 2),
|
|
746
|
+
averageWordErrorRate: roundMetric(average(fixtures.map((fixture) => fixture.accuracy.wordErrorRate))) ?? 0,
|
|
747
|
+
fixtureCount,
|
|
748
|
+
fixturesWithErrors: fixtures.filter((fixture) => fixture.errorCount > 0).length,
|
|
749
|
+
fixturesWithFragmentation: fixtures.filter((fixture) => fixture.fragmentationCount > 0).length,
|
|
750
|
+
groupSummaries: calculateGroupSummary(fixtures),
|
|
751
|
+
passCount,
|
|
752
|
+
passRate: fixtureCount > 0 ? roundMetric(passCount / fixtureCount) ?? 0 : 0,
|
|
753
|
+
totalErrorCount: fixtures.reduce((sum, fixture) => sum + fixture.errorCount, 0),
|
|
754
|
+
wordAccuracyRate: fixtureCount > 0 ? roundMetric(1 - (average(fixtures.map((fixture) => fixture.accuracy.wordErrorRate)) ?? 0)) ?? 0 : 0
|
|
755
|
+
};
|
|
756
|
+
};
|
|
735
757
|
var summarizeSTTBenchmarkSeries = (input) => {
|
|
736
758
|
const fixtureMap = new Map;
|
|
737
759
|
for (const report of input.reports) {
|
|
@@ -780,28 +802,6 @@ var summarizeSTTBenchmarkSeries = (input) => {
|
|
|
780
802
|
}
|
|
781
803
|
};
|
|
782
804
|
};
|
|
783
|
-
var runSTTAdapterBenchmarkSeries = async ({
|
|
784
|
-
adapter,
|
|
785
|
-
adapterId,
|
|
786
|
-
fixtures,
|
|
787
|
-
options = {},
|
|
788
|
-
runs
|
|
789
|
-
}) => {
|
|
790
|
-
const reports = [];
|
|
791
|
-
const runCount = Math.max(1, Math.floor(runs));
|
|
792
|
-
for (let runIndex = 0;runIndex < runCount; runIndex += 1) {
|
|
793
|
-
reports.push(await runSTTAdapterBenchmark({
|
|
794
|
-
adapter,
|
|
795
|
-
adapterId,
|
|
796
|
-
fixtures,
|
|
797
|
-
options
|
|
798
|
-
}));
|
|
799
|
-
}
|
|
800
|
-
return summarizeSTTBenchmarkSeries({
|
|
801
|
-
adapterId,
|
|
802
|
-
reports
|
|
803
|
-
});
|
|
804
|
-
};
|
|
805
805
|
// src/core/correction.ts
|
|
806
806
|
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
807
807
|
var buildAliasMatcher = (alias) => new RegExp(`(?<![\\p{L}\\p{N}'])${escapeRegExp(alias)}(?![\\p{L}\\p{N}'])`, "giu");
|
|
@@ -911,11 +911,9 @@ var findFuzzyAliasMatch = (text, alias, riskTier) => {
|
|
|
911
911
|
return bestMatch;
|
|
912
912
|
};
|
|
913
913
|
var normalizeHintAliases = (hint) => (hint.aliases ?? []).map((alias) => alias.trim()).filter((alias) => alias.length > 0).sort((left, right) => right.length - left.length);
|
|
914
|
-
var applyPhraseHintCorrections = (text, phraseHints) => {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
});
|
|
918
|
-
};
|
|
914
|
+
var applyPhraseHintCorrections = (text, phraseHints) => applyRiskTieredPhraseHintCorrections(text, phraseHints, {
|
|
915
|
+
riskTier: "risky"
|
|
916
|
+
});
|
|
919
917
|
var applyRiskTieredPhraseHintCorrections = (text, phraseHints, options = {}) => {
|
|
920
918
|
const riskTier = options.riskTier ?? "safe";
|
|
921
919
|
let corrected = text;
|
|
@@ -973,6 +971,25 @@ var isSafeAlias = (alias) => {
|
|
|
973
971
|
const tokens = normalized.split(" ").filter((token) => token.length > 0);
|
|
974
972
|
return tokens.length >= 2 || normalized.length >= 7;
|
|
975
973
|
};
|
|
974
|
+
var createDomainLexicon = (terms) => {
|
|
975
|
+
const entries = [];
|
|
976
|
+
const seen = new Set;
|
|
977
|
+
for (const term of terms) {
|
|
978
|
+
const normalizedText = normalizeDomainTerm(term.text);
|
|
979
|
+
if (!normalizedText || seen.has(normalizedText)) {
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
entries.push({
|
|
983
|
+
aliases: dedupeAliases(term.aliases ?? []),
|
|
984
|
+
language: term.language,
|
|
985
|
+
metadata: term.metadata,
|
|
986
|
+
pronunciation: term.pronunciation,
|
|
987
|
+
text: term.text
|
|
988
|
+
});
|
|
989
|
+
seen.add(normalizedText);
|
|
990
|
+
}
|
|
991
|
+
return entries;
|
|
992
|
+
};
|
|
976
993
|
var createDomainPhraseHints = (terms, options = {}) => {
|
|
977
994
|
const riskTier = options.riskTier ?? "safe";
|
|
978
995
|
const hints = [];
|
|
@@ -1002,25 +1019,6 @@ var createDomainPhraseHints = (terms, options = {}) => {
|
|
|
1002
1019
|
}
|
|
1003
1020
|
return hints;
|
|
1004
1021
|
};
|
|
1005
|
-
var createDomainLexicon = (terms) => {
|
|
1006
|
-
const entries = [];
|
|
1007
|
-
const seen = new Set;
|
|
1008
|
-
for (const term of terms) {
|
|
1009
|
-
const normalizedText = normalizeDomainTerm(term.text);
|
|
1010
|
-
if (!normalizedText || seen.has(normalizedText)) {
|
|
1011
|
-
continue;
|
|
1012
|
-
}
|
|
1013
|
-
entries.push({
|
|
1014
|
-
aliases: dedupeAliases(term.aliases ?? []),
|
|
1015
|
-
language: term.language,
|
|
1016
|
-
metadata: term.metadata,
|
|
1017
|
-
pronunciation: term.pronunciation,
|
|
1018
|
-
text: term.text
|
|
1019
|
-
});
|
|
1020
|
-
seen.add(normalizedText);
|
|
1021
|
-
}
|
|
1022
|
-
return entries;
|
|
1023
|
-
};
|
|
1024
1022
|
var averageTranscriptConfidence = (transcripts) => {
|
|
1025
1023
|
const confidences = transcripts.map((transcript) => transcript.confidence).filter((value) => typeof value === "number");
|
|
1026
1024
|
return confidences.length > 0 ? confidences.reduce((sum, value) => sum + value, 0) / confidences.length : undefined;
|
|
@@ -1155,7 +1153,7 @@ var countDistinctScripts = (value) => {
|
|
|
1155
1153
|
return Number(hasLatin) + Number(hasDevanagari);
|
|
1156
1154
|
};
|
|
1157
1155
|
var scoreCodeSwitchLexiconEntry = (entry) => {
|
|
1158
|
-
const text = entry
|
|
1156
|
+
const { text } = entry;
|
|
1159
1157
|
const tokens = tokenizeCodeSwitchText(text);
|
|
1160
1158
|
const normalized = normalizeBenchmarkText2(text);
|
|
1161
1159
|
const hasAlias = (entry.aliases?.length ?? 0) > 0;
|
|
@@ -1192,7 +1190,7 @@ var pushUniqueLexiconEntryWithAliases = (target, seen, text, aliases, language)
|
|
|
1192
1190
|
var hasFixtureTag = (fixture, targetTag) => ((fixturesTags) => fixturesTags.some((tag) => tag === targetTag))((fixture.tags ?? []).map((tag) => tag.trim().toLowerCase()));
|
|
1193
1191
|
var appendParlamentParlaCodeSwitchLexicon = (target, seen, fixture) => {
|
|
1194
1192
|
const expectedText = normalizeBenchmarkText2(fixture.expectedText ?? "");
|
|
1195
|
-
const language = fixture
|
|
1193
|
+
const { language } = fixture;
|
|
1196
1194
|
if (expectedText.includes("espanya es paro y muerte")) {
|
|
1197
1195
|
pushUniqueLexiconEntryWithAliases(target, seen, "espanya es paro y muerte", [
|
|
1198
1196
|
"espanya espanya i muertes",
|
|
@@ -1315,66 +1313,6 @@ var buildFixtureDomainTerms = (fixture) => {
|
|
|
1315
1313
|
}
|
|
1316
1314
|
return terms;
|
|
1317
1315
|
};
|
|
1318
|
-
var buildCodeSwitchBenchmarkLexicon = (fixture) => {
|
|
1319
|
-
const expectedTermEntries = [];
|
|
1320
|
-
const candidateEntries = [];
|
|
1321
|
-
const seen = new Set;
|
|
1322
|
-
const language = fixture.language;
|
|
1323
|
-
const tokens = tokenizeCodeSwitchText(fixture.expectedText ?? "");
|
|
1324
|
-
for (const expectedTerm of fixture.expectedTerms ?? []) {
|
|
1325
|
-
pushUniqueLexiconEntry(expectedTermEntries, seen, expectedTerm, language);
|
|
1326
|
-
}
|
|
1327
|
-
for (const token of tokens) {
|
|
1328
|
-
if (!token.marked || token.text.length < 4) {
|
|
1329
|
-
continue;
|
|
1330
|
-
}
|
|
1331
|
-
pushUniqueLexiconEntry(candidateEntries, seen, token.text, language);
|
|
1332
|
-
}
|
|
1333
|
-
for (let startIndex = 0;startIndex < tokens.length; startIndex += 1) {
|
|
1334
|
-
for (let windowSize = 2;windowSize <= 3; windowSize += 1) {
|
|
1335
|
-
const window2 = tokens.slice(startIndex, startIndex + windowSize);
|
|
1336
|
-
if (window2.length !== windowSize || !isUsefulCodeSwitchWindow(window2)) {
|
|
1337
|
-
continue;
|
|
1338
|
-
}
|
|
1339
|
-
pushUniqueLexiconEntry(candidateEntries, seen, window2.map((token) => token.text).join(" "), language);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
if (hasFixtureTag(fixture, "parlament_parla")) {
|
|
1343
|
-
appendParlamentParlaCodeSwitchLexicon(candidateEntries, seen, fixture);
|
|
1344
|
-
}
|
|
1345
|
-
return [
|
|
1346
|
-
...expectedTermEntries,
|
|
1347
|
-
...candidateEntries.sort((left, right) => scoreCodeSwitchLexiconEntry(right) - scoreCodeSwitchLexiconEntry(left))
|
|
1348
|
-
].slice(0, MAX_CODE_SWITCH_LEXICON_ENTRIES);
|
|
1349
|
-
};
|
|
1350
|
-
var buildCodeSwitchBenchmarkPhraseHints = (fixture) => buildCodeSwitchBenchmarkLexicon(fixture).map((entry) => ({
|
|
1351
|
-
aliases: entry.aliases,
|
|
1352
|
-
metadata: entry.metadata,
|
|
1353
|
-
text: entry.text
|
|
1354
|
-
}));
|
|
1355
|
-
var createCodeSwitchBenchmarkCorrectionHandler = () => createLexiconCorrectionHandler({
|
|
1356
|
-
provider: "codeswitch-lexicon-corrector",
|
|
1357
|
-
reason: "codeswitch-lexicon-correction"
|
|
1358
|
-
});
|
|
1359
|
-
var createBenchmarkCorrectionHandler = (profile) => createPhraseHintCorrectionHandler({
|
|
1360
|
-
provider: profile === "generic" ? "generic-hint-corrector" : "benchmark-seeded-corrector",
|
|
1361
|
-
reason: profile === "generic" ? "generic-domain-correction" : "benchmark-seeded-correction"
|
|
1362
|
-
});
|
|
1363
|
-
var scoreCorrectedExpectedTerms = (actualText, expectedTerms) => {
|
|
1364
|
-
const normalizedActual = normalizeBenchmarkText2(actualText);
|
|
1365
|
-
const normalizedExpectedTerms = (expectedTerms ?? []).map((entry) => normalizeBenchmarkText2(entry));
|
|
1366
|
-
const matchedTerms = normalizedExpectedTerms.filter((term) => term.length > 0 && normalizedActual.includes(term));
|
|
1367
|
-
const missingTerms = normalizedExpectedTerms.filter((term) => term.length > 0 && !matchedTerms.includes(term));
|
|
1368
|
-
const denominator = normalizedExpectedTerms.length;
|
|
1369
|
-
const recall = denominator > 0 ? matchedTerms.length / denominator : 1;
|
|
1370
|
-
return {
|
|
1371
|
-
allMatched: missingTerms.length === 0,
|
|
1372
|
-
expectedTerms: normalizedExpectedTerms,
|
|
1373
|
-
matchedTerms,
|
|
1374
|
-
missingTerms,
|
|
1375
|
-
recall
|
|
1376
|
-
};
|
|
1377
|
-
};
|
|
1378
1316
|
var applyCorrectedBenchmarkReport = (report, fixtures, profile = "benchmark-seeded") => {
|
|
1379
1317
|
const fixtureMap = new Map(fixtures.map((fixture) => [fixture.id, fixture]));
|
|
1380
1318
|
const correctedFixtures = report.fixtures.map((result) => {
|
|
@@ -1473,20 +1411,80 @@ var applyLexiconCorrectedBenchmarkReport = (report, fixtures, buildLexicon) => {
|
|
|
1473
1411
|
summary: summarizeSTTBenchmark(report.adapterId, correctedFixtures)
|
|
1474
1412
|
};
|
|
1475
1413
|
};
|
|
1476
|
-
var
|
|
1477
|
-
const
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1414
|
+
var buildCodeSwitchBenchmarkLexicon = (fixture) => {
|
|
1415
|
+
const expectedTermEntries = [];
|
|
1416
|
+
const candidateEntries = [];
|
|
1417
|
+
const seen = new Set;
|
|
1418
|
+
const { language } = fixture;
|
|
1419
|
+
const tokens = tokenizeCodeSwitchText(fixture.expectedText ?? "");
|
|
1420
|
+
for (const expectedTerm of fixture.expectedTerms ?? []) {
|
|
1421
|
+
pushUniqueLexiconEntry(expectedTermEntries, seen, expectedTerm, language);
|
|
1422
|
+
}
|
|
1423
|
+
for (const token of tokens) {
|
|
1424
|
+
if (!token.marked || token.text.length < 4) {
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
pushUniqueLexiconEntry(candidateEntries, seen, token.text, language);
|
|
1428
|
+
}
|
|
1429
|
+
for (let startIndex = 0;startIndex < tokens.length; startIndex += 1) {
|
|
1430
|
+
for (let windowSize = 2;windowSize <= 3; windowSize += 1) {
|
|
1431
|
+
const window2 = tokens.slice(startIndex, startIndex + windowSize);
|
|
1432
|
+
if (window2.length !== windowSize || !isUsefulCodeSwitchWindow(window2)) {
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
pushUniqueLexiconEntry(candidateEntries, seen, window2.map((token) => token.text).join(" "), language);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (hasFixtureTag(fixture, "parlament_parla")) {
|
|
1439
|
+
appendParlamentParlaCodeSwitchLexicon(candidateEntries, seen, fixture);
|
|
1440
|
+
}
|
|
1441
|
+
return [
|
|
1442
|
+
...expectedTermEntries,
|
|
1443
|
+
...candidateEntries.sort((left, right) => scoreCodeSwitchLexiconEntry(right) - scoreCodeSwitchLexiconEntry(left))
|
|
1444
|
+
].slice(0, MAX_CODE_SWITCH_LEXICON_ENTRIES);
|
|
1445
|
+
};
|
|
1446
|
+
var buildCodeSwitchBenchmarkPhraseHints = (fixture) => buildCodeSwitchBenchmarkLexicon(fixture).map((entry) => ({
|
|
1447
|
+
aliases: entry.aliases,
|
|
1448
|
+
metadata: entry.metadata,
|
|
1449
|
+
text: entry.text
|
|
1450
|
+
}));
|
|
1451
|
+
var createBenchmarkCorrectionHandler = (profile) => createPhraseHintCorrectionHandler({
|
|
1452
|
+
provider: profile === "generic" ? "generic-hint-corrector" : "benchmark-seeded-corrector",
|
|
1453
|
+
reason: profile === "generic" ? "generic-domain-correction" : "benchmark-seeded-correction"
|
|
1454
|
+
});
|
|
1455
|
+
var createCodeSwitchBenchmarkCorrectionHandler = () => createLexiconCorrectionHandler({
|
|
1456
|
+
provider: "codeswitch-lexicon-corrector",
|
|
1457
|
+
reason: "codeswitch-lexicon-correction"
|
|
1458
|
+
});
|
|
1459
|
+
var scoreCorrectedExpectedTerms = (actualText, expectedTerms) => {
|
|
1460
|
+
const normalizedActual = normalizeBenchmarkText2(actualText);
|
|
1461
|
+
const normalizedExpectedTerms = (expectedTerms ?? []).map((entry) => normalizeBenchmarkText2(entry));
|
|
1462
|
+
const matchedTerms = normalizedExpectedTerms.filter((term) => term.length > 0 && normalizedActual.includes(term));
|
|
1463
|
+
const missingTerms = normalizedExpectedTerms.filter((term) => term.length > 0 && !matchedTerms.includes(term));
|
|
1464
|
+
const denominator = normalizedExpectedTerms.length;
|
|
1465
|
+
const recall = denominator > 0 ? matchedTerms.length / denominator : 1;
|
|
1466
|
+
return {
|
|
1467
|
+
allMatched: missingTerms.length === 0,
|
|
1468
|
+
expectedTerms: normalizedExpectedTerms,
|
|
1469
|
+
matchedTerms,
|
|
1470
|
+
missingTerms,
|
|
1471
|
+
recall
|
|
1472
|
+
};
|
|
1473
|
+
};
|
|
1474
|
+
var sliceSTTHoldoutReport = (report, fixtureIds) => {
|
|
1475
|
+
const fixtureIdSet = new Set(fixtureIds);
|
|
1476
|
+
const fixtures = report.fixtures.filter((fixture) => fixtureIdSet.has(fixture.fixtureId));
|
|
1477
|
+
return {
|
|
1478
|
+
adapterId: report.adapterId,
|
|
1479
|
+
fixtures,
|
|
1480
|
+
generatedAt: report.generatedAt,
|
|
1481
|
+
summary: summarizeSTTBenchmark(report.adapterId, fixtures)
|
|
1482
|
+
};
|
|
1483
|
+
};
|
|
1484
|
+
var buildCorrectionBenchmarkAudit = (rawReport, fixtures) => {
|
|
1485
|
+
const generic = applyCorrectedBenchmarkReport(rawReport, fixtures, "generic");
|
|
1486
|
+
const experimental = applyExperimentalBenchmarkReport(rawReport, fixtures);
|
|
1487
|
+
const benchmarkSeeded = applyCorrectedBenchmarkReport(rawReport, fixtures, "benchmark-seeded");
|
|
1490
1488
|
const holdoutFixtureIds = rawReport.fixtures.map((fixture) => fixture.fixtureId).filter(isCorrectionHoldoutFixtureId);
|
|
1491
1489
|
const lexicalHoldoutFixtureIds = fixtures.filter((fixture) => !usesBenchmarkSeedVocabulary(fixture)).map((fixture) => fixture.id).filter((fixtureId) => rawReport.fixtures.some((result) => result.fixtureId === fixtureId));
|
|
1492
1490
|
return {
|
|
@@ -1598,7 +1596,7 @@ var getAudioContextCtor = () => {
|
|
|
1598
1596
|
return window.AudioContext ?? window.webkitAudioContext;
|
|
1599
1597
|
};
|
|
1600
1598
|
var decodePCM16LEChunk = (audioContext, chunk) => {
|
|
1601
|
-
const format = chunk
|
|
1599
|
+
const { format } = chunk;
|
|
1602
1600
|
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
1603
1601
|
throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
|
|
1604
1602
|
}
|
|
@@ -1799,6 +1797,9 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1799
1797
|
}
|
|
1800
1798
|
});
|
|
1801
1799
|
const player = {
|
|
1800
|
+
get activeSourceCount() {
|
|
1801
|
+
return state.activeSourceCount;
|
|
1802
|
+
},
|
|
1802
1803
|
close: async () => {
|
|
1803
1804
|
unsubscribeSource();
|
|
1804
1805
|
stopQueuedPlayback({ forceClear: true });
|
|
@@ -1820,19 +1821,10 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1820
1821
|
isPlaying: false
|
|
1821
1822
|
});
|
|
1822
1823
|
},
|
|
1823
|
-
get activeSourceCount() {
|
|
1824
|
-
return state.activeSourceCount;
|
|
1825
|
-
},
|
|
1826
1824
|
get error() {
|
|
1827
1825
|
return state.error;
|
|
1828
1826
|
},
|
|
1829
1827
|
getSnapshot: () => state,
|
|
1830
|
-
get isActive() {
|
|
1831
|
-
return state.isActive;
|
|
1832
|
-
},
|
|
1833
|
-
get isPlaying() {
|
|
1834
|
-
return state.isPlaying;
|
|
1835
|
-
},
|
|
1836
1828
|
interrupt: async () => {
|
|
1837
1829
|
const startedAt = Date.now();
|
|
1838
1830
|
const context = await ensureAudioContext();
|
|
@@ -1864,6 +1856,12 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1864
1856
|
stopQueuedPlayback();
|
|
1865
1857
|
await interruptPromise;
|
|
1866
1858
|
},
|
|
1859
|
+
get isActive() {
|
|
1860
|
+
return state.isActive;
|
|
1861
|
+
},
|
|
1862
|
+
get isPlaying() {
|
|
1863
|
+
return state.isPlaying;
|
|
1864
|
+
},
|
|
1867
1865
|
get lastInterruptLatencyMs() {
|
|
1868
1866
|
return state.lastInterruptLatencyMs;
|
|
1869
1867
|
},
|
|
@@ -2239,14 +2237,14 @@ var createVoiceBrowserMediaReporter = (options) => {
|
|
|
2239
2237
|
return {
|
|
2240
2238
|
close: stop,
|
|
2241
2239
|
reportOnce,
|
|
2240
|
+
stop,
|
|
2242
2241
|
start: () => {
|
|
2243
2242
|
if (interval) {
|
|
2244
2243
|
return;
|
|
2245
2244
|
}
|
|
2246
2245
|
run();
|
|
2247
2246
|
interval = setInterval(run, options.intervalMs ?? DEFAULT_BROWSER_MEDIA_INTERVAL_MS);
|
|
2248
|
-
}
|
|
2249
|
-
stop
|
|
2247
|
+
}
|
|
2250
2248
|
};
|
|
2251
2249
|
};
|
|
2252
2250
|
|
|
@@ -2264,14 +2262,14 @@ var NOOP_CONNECTION = {
|
|
|
2264
2262
|
callControl: noop,
|
|
2265
2263
|
close: noop,
|
|
2266
2264
|
endTurn: noop,
|
|
2267
|
-
getReadyState: () => WS_CLOSED,
|
|
2268
|
-
getScenarioId: () => "",
|
|
2269
|
-
getSessionId: () => "",
|
|
2270
2265
|
send: noop,
|
|
2271
2266
|
sendAudio: noop,
|
|
2272
2267
|
simulateDisconnect: noop,
|
|
2273
|
-
|
|
2274
|
-
|
|
2268
|
+
subscribe: noopUnsubscribe,
|
|
2269
|
+
getReadyState: () => WS_CLOSED,
|
|
2270
|
+
getScenarioId: () => "",
|
|
2271
|
+
getSessionId: () => "",
|
|
2272
|
+
start: () => {}
|
|
2275
2273
|
};
|
|
2276
2274
|
var createSessionId = () => crypto.randomUUID();
|
|
2277
2275
|
var buildWsUrl = (path, sessionId, scenarioId) => {
|
|
@@ -2468,9 +2466,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
2468
2466
|
state.scenarioId = input.scenarioId;
|
|
2469
2467
|
}
|
|
2470
2468
|
send({
|
|
2471
|
-
|
|
2469
|
+
scenarioId: state.scenarioId ?? undefined,
|
|
2472
2470
|
sessionId: state.sessionId,
|
|
2473
|
-
|
|
2471
|
+
type: "start"
|
|
2474
2472
|
});
|
|
2475
2473
|
};
|
|
2476
2474
|
const sendAudio = (audio) => {
|
|
@@ -2510,14 +2508,14 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
2510
2508
|
callControl,
|
|
2511
2509
|
close,
|
|
2512
2510
|
endTurn,
|
|
2513
|
-
getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
|
|
2514
|
-
getScenarioId: () => state.scenarioId ?? "",
|
|
2515
|
-
getSessionId: () => state.sessionId,
|
|
2516
2511
|
send,
|
|
2517
2512
|
sendAudio,
|
|
2518
2513
|
simulateDisconnect,
|
|
2519
2514
|
start,
|
|
2520
|
-
subscribe
|
|
2515
|
+
subscribe,
|
|
2516
|
+
getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
|
|
2517
|
+
getScenarioId: () => state.scenarioId ?? "",
|
|
2518
|
+
getSessionId: () => state.sessionId
|
|
2521
2519
|
};
|
|
2522
2520
|
};
|
|
2523
2521
|
|
|
@@ -2533,11 +2531,11 @@ var createInitialState2 = () => ({
|
|
|
2533
2531
|
call: null,
|
|
2534
2532
|
error: null,
|
|
2535
2533
|
isConnected: false,
|
|
2536
|
-
sessionMetadata: null,
|
|
2537
|
-
scenarioId: null,
|
|
2538
2534
|
partial: "",
|
|
2539
2535
|
reconnect: createInitialReconnectState(),
|
|
2536
|
+
scenarioId: null,
|
|
2540
2537
|
sessionId: null,
|
|
2538
|
+
sessionMetadata: null,
|
|
2541
2539
|
status: "idle",
|
|
2542
2540
|
turns: []
|
|
2543
2541
|
});
|
|
@@ -2740,6 +2738,16 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
2740
2738
|
}
|
|
2741
2739
|
});
|
|
2742
2740
|
return {
|
|
2741
|
+
start,
|
|
2742
|
+
get assistantAudio() {
|
|
2743
|
+
return store.getSnapshot().assistantAudio;
|
|
2744
|
+
},
|
|
2745
|
+
get assistantTexts() {
|
|
2746
|
+
return store.getSnapshot().assistantTexts;
|
|
2747
|
+
},
|
|
2748
|
+
get call() {
|
|
2749
|
+
return store.getSnapshot().call;
|
|
2750
|
+
},
|
|
2743
2751
|
callControl(message) {
|
|
2744
2752
|
connection.callControl(message);
|
|
2745
2753
|
},
|
|
@@ -2765,48 +2773,38 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
2765
2773
|
get isConnected() {
|
|
2766
2774
|
return store.getSnapshot().isConnected;
|
|
2767
2775
|
},
|
|
2768
|
-
get scenarioId() {
|
|
2769
|
-
return store.getSnapshot().scenarioId;
|
|
2770
|
-
},
|
|
2771
|
-
get sessionMetadata() {
|
|
2772
|
-
return store.getSnapshot().sessionMetadata;
|
|
2773
|
-
},
|
|
2774
|
-
start,
|
|
2775
2776
|
get partial() {
|
|
2776
2777
|
return store.getSnapshot().partial;
|
|
2777
2778
|
},
|
|
2778
2779
|
get reconnect() {
|
|
2779
2780
|
return store.getSnapshot().reconnect;
|
|
2780
2781
|
},
|
|
2781
|
-
get
|
|
2782
|
-
return
|
|
2783
|
-
},
|
|
2784
|
-
get status() {
|
|
2785
|
-
return store.getSnapshot().status;
|
|
2786
|
-
},
|
|
2787
|
-
get turns() {
|
|
2788
|
-
return store.getSnapshot().turns;
|
|
2789
|
-
},
|
|
2790
|
-
get assistantTexts() {
|
|
2791
|
-
return store.getSnapshot().assistantTexts;
|
|
2792
|
-
},
|
|
2793
|
-
get assistantAudio() {
|
|
2794
|
-
return store.getSnapshot().assistantAudio;
|
|
2795
|
-
},
|
|
2796
|
-
get call() {
|
|
2797
|
-
return store.getSnapshot().call;
|
|
2782
|
+
get scenarioId() {
|
|
2783
|
+
return store.getSnapshot().scenarioId;
|
|
2798
2784
|
},
|
|
2799
2785
|
sendAudio(audio) {
|
|
2800
2786
|
connection.sendAudio(audio);
|
|
2801
2787
|
},
|
|
2788
|
+
get sessionId() {
|
|
2789
|
+
return connection.getSessionId();
|
|
2790
|
+
},
|
|
2791
|
+
get sessionMetadata() {
|
|
2792
|
+
return store.getSnapshot().sessionMetadata;
|
|
2793
|
+
},
|
|
2802
2794
|
simulateDisconnect() {
|
|
2803
2795
|
connection.simulateDisconnect();
|
|
2804
2796
|
},
|
|
2797
|
+
get status() {
|
|
2798
|
+
return store.getSnapshot().status;
|
|
2799
|
+
},
|
|
2805
2800
|
subscribe(subscriber) {
|
|
2806
2801
|
subscribers.add(subscriber);
|
|
2807
2802
|
return () => {
|
|
2808
2803
|
subscribers.delete(subscriber);
|
|
2809
2804
|
};
|
|
2805
|
+
},
|
|
2806
|
+
get turns() {
|
|
2807
|
+
return store.getSnapshot().turns;
|
|
2810
2808
|
}
|
|
2811
2809
|
};
|
|
2812
2810
|
};
|
|
@@ -2833,18 +2831,6 @@ var computeRms = (samples) => {
|
|
|
2833
2831
|
}
|
|
2834
2832
|
return Math.sqrt(sumSquares / samples.length);
|
|
2835
2833
|
};
|
|
2836
|
-
var resolveAudioConditioningConfig = (config) => {
|
|
2837
|
-
if (!config || config.enabled === false) {
|
|
2838
|
-
return;
|
|
2839
|
-
}
|
|
2840
|
-
return {
|
|
2841
|
-
enabled: true,
|
|
2842
|
-
maxGain: config.maxGain ?? DEFAULT_MAX_GAIN,
|
|
2843
|
-
noiseGateAttenuation: config.noiseGateAttenuation ?? DEFAULT_NOISE_GATE_ATTENUATION,
|
|
2844
|
-
noiseGateThreshold: config.noiseGateThreshold ?? DEFAULT_NOISE_GATE_THRESHOLD,
|
|
2845
|
-
targetLevel: config.targetLevel ?? DEFAULT_TARGET_LEVEL
|
|
2846
|
-
};
|
|
2847
|
-
};
|
|
2848
2834
|
var conditionAudioChunk = (audio, config) => {
|
|
2849
2835
|
if (!config) {
|
|
2850
2836
|
return audio;
|
|
@@ -2865,6 +2851,18 @@ var conditionAudioChunk = (audio, config) => {
|
|
|
2865
2851
|
}
|
|
2866
2852
|
return new Uint8Array(output.buffer);
|
|
2867
2853
|
};
|
|
2854
|
+
var resolveAudioConditioningConfig = (config) => {
|
|
2855
|
+
if (!config || config.enabled === false) {
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
return {
|
|
2859
|
+
enabled: true,
|
|
2860
|
+
maxGain: config.maxGain ?? DEFAULT_MAX_GAIN,
|
|
2861
|
+
noiseGateAttenuation: config.noiseGateAttenuation ?? DEFAULT_NOISE_GATE_ATTENUATION,
|
|
2862
|
+
noiseGateThreshold: config.noiseGateThreshold ?? DEFAULT_NOISE_GATE_THRESHOLD,
|
|
2863
|
+
targetLevel: config.targetLevel ?? DEFAULT_TARGET_LEVEL
|
|
2864
|
+
};
|
|
2865
|
+
};
|
|
2868
2866
|
|
|
2869
2867
|
// src/core/turnProfiles.ts
|
|
2870
2868
|
var TURN_PROFILE_DEFAULTS = {
|
|
@@ -2888,12 +2886,12 @@ var TURN_PROFILE_DEFAULTS = {
|
|
|
2888
2886
|
}
|
|
2889
2887
|
};
|
|
2890
2888
|
var QUALITY_PROFILE_DEFAULTS = {
|
|
2891
|
-
general: {},
|
|
2892
2889
|
"accent-heavy": {
|
|
2893
2890
|
silenceMs: 1200,
|
|
2894
2891
|
speechThreshold: 0.01,
|
|
2895
2892
|
transcriptStabilityMs: 1200
|
|
2896
2893
|
},
|
|
2894
|
+
general: {},
|
|
2897
2895
|
"noisy-room": {
|
|
2898
2896
|
silenceMs: 2000,
|
|
2899
2897
|
speechThreshold: 0.02,
|
|
@@ -2942,8 +2940,8 @@ var PRESET_INPUTS = {
|
|
|
2942
2940
|
},
|
|
2943
2941
|
sttLifecycle: "continuous",
|
|
2944
2942
|
turnDetection: {
|
|
2945
|
-
|
|
2946
|
-
|
|
2943
|
+
profile: "balanced",
|
|
2944
|
+
qualityProfile: "short-command"
|
|
2947
2945
|
}
|
|
2948
2946
|
},
|
|
2949
2947
|
default: {
|
|
@@ -2958,8 +2956,8 @@ var PRESET_INPUTS = {
|
|
|
2958
2956
|
},
|
|
2959
2957
|
sttLifecycle: "continuous",
|
|
2960
2958
|
turnDetection: {
|
|
2961
|
-
|
|
2962
|
-
|
|
2959
|
+
profile: "fast",
|
|
2960
|
+
qualityProfile: "general"
|
|
2963
2961
|
}
|
|
2964
2962
|
},
|
|
2965
2963
|
dictation: {
|
|
@@ -2981,8 +2979,8 @@ var PRESET_INPUTS = {
|
|
|
2981
2979
|
},
|
|
2982
2980
|
sttLifecycle: "continuous",
|
|
2983
2981
|
turnDetection: {
|
|
2984
|
-
|
|
2985
|
-
|
|
2982
|
+
profile: "long-form",
|
|
2983
|
+
qualityProfile: "accent-heavy"
|
|
2986
2984
|
}
|
|
2987
2985
|
},
|
|
2988
2986
|
"guided-intake": {
|
|
@@ -3004,8 +3002,8 @@ var PRESET_INPUTS = {
|
|
|
3004
3002
|
},
|
|
3005
3003
|
sttLifecycle: "turn-scoped",
|
|
3006
3004
|
turnDetection: {
|
|
3007
|
-
|
|
3008
|
-
|
|
3005
|
+
profile: "long-form",
|
|
3006
|
+
qualityProfile: "accent-heavy"
|
|
3009
3007
|
}
|
|
3010
3008
|
},
|
|
3011
3009
|
"noisy-room": {
|
|
@@ -3027,8 +3025,8 @@ var PRESET_INPUTS = {
|
|
|
3027
3025
|
},
|
|
3028
3026
|
sttLifecycle: "continuous",
|
|
3029
3027
|
turnDetection: {
|
|
3030
|
-
qualityProfile: "noisy-room",
|
|
3031
3028
|
profile: "long-form",
|
|
3029
|
+
qualityProfile: "noisy-room",
|
|
3032
3030
|
silenceMs: 2100,
|
|
3033
3031
|
speechThreshold: 0.02,
|
|
3034
3032
|
transcriptStabilityMs: 1650
|
|
@@ -3053,8 +3051,8 @@ var PRESET_INPUTS = {
|
|
|
3053
3051
|
},
|
|
3054
3052
|
sttLifecycle: "continuous",
|
|
3055
3053
|
turnDetection: {
|
|
3056
|
-
qualityProfile: "noisy-room",
|
|
3057
3054
|
profile: "long-form",
|
|
3055
|
+
qualityProfile: "noisy-room",
|
|
3058
3056
|
silenceMs: 660,
|
|
3059
3057
|
speechThreshold: 0.012,
|
|
3060
3058
|
transcriptStabilityMs: 300
|
|
@@ -3079,8 +3077,8 @@ var PRESET_INPUTS = {
|
|
|
3079
3077
|
},
|
|
3080
3078
|
sttLifecycle: "continuous",
|
|
3081
3079
|
turnDetection: {
|
|
3082
|
-
qualityProfile: "noisy-room",
|
|
3083
3080
|
profile: "long-form",
|
|
3081
|
+
qualityProfile: "noisy-room",
|
|
3084
3082
|
silenceMs: 620,
|
|
3085
3083
|
speechThreshold: 0.012,
|
|
3086
3084
|
transcriptStabilityMs: 280
|
|
@@ -3105,8 +3103,8 @@ var PRESET_INPUTS = {
|
|
|
3105
3103
|
},
|
|
3106
3104
|
sttLifecycle: "continuous",
|
|
3107
3105
|
turnDetection: {
|
|
3108
|
-
|
|
3109
|
-
|
|
3106
|
+
profile: "long-form",
|
|
3107
|
+
qualityProfile: "noisy-room"
|
|
3110
3108
|
}
|
|
3111
3109
|
}
|
|
3112
3110
|
};
|
|
@@ -3246,11 +3244,22 @@ var createVoiceController = (path, options = {}) => {
|
|
|
3246
3244
|
stream.close();
|
|
3247
3245
|
};
|
|
3248
3246
|
return {
|
|
3247
|
+
close,
|
|
3248
|
+
startRecording,
|
|
3249
|
+
stopRecording,
|
|
3250
|
+
get assistantAudio() {
|
|
3251
|
+
return state.assistantAudio;
|
|
3252
|
+
},
|
|
3253
|
+
get assistantTexts() {
|
|
3254
|
+
return state.assistantTexts;
|
|
3255
|
+
},
|
|
3249
3256
|
bindHTMX(bindingOptions) {
|
|
3250
3257
|
return bindVoiceHTMX(stream, bindingOptions);
|
|
3251
3258
|
},
|
|
3259
|
+
get call() {
|
|
3260
|
+
return state.call;
|
|
3261
|
+
},
|
|
3252
3262
|
callControl: (message) => stream.callControl(message),
|
|
3253
|
-
close,
|
|
3254
3263
|
endTurn: () => stream.endTurn(),
|
|
3255
3264
|
get error() {
|
|
3256
3265
|
return state.error;
|
|
@@ -3266,28 +3275,26 @@ var createVoiceController = (path, options = {}) => {
|
|
|
3266
3275
|
get partial() {
|
|
3267
3276
|
return state.partial;
|
|
3268
3277
|
},
|
|
3278
|
+
get reconnect() {
|
|
3279
|
+
return state.reconnect;
|
|
3280
|
+
},
|
|
3269
3281
|
get recordingError() {
|
|
3270
3282
|
return state.recordingError;
|
|
3271
3283
|
},
|
|
3272
|
-
get
|
|
3273
|
-
return state.
|
|
3284
|
+
get scenarioId() {
|
|
3285
|
+
return state.scenarioId;
|
|
3274
3286
|
},
|
|
3275
3287
|
sendAudio: (audio) => stream.sendAudio(audio),
|
|
3276
|
-
simulateDisconnect: () => stream.simulateDisconnect(),
|
|
3277
3288
|
get sessionId() {
|
|
3278
3289
|
return state.sessionId;
|
|
3279
3290
|
},
|
|
3280
3291
|
get sessionMetadata() {
|
|
3281
3292
|
return state.sessionMetadata;
|
|
3282
3293
|
},
|
|
3283
|
-
|
|
3284
|
-
return state.scenarioId;
|
|
3285
|
-
},
|
|
3286
|
-
startRecording,
|
|
3294
|
+
simulateDisconnect: () => stream.simulateDisconnect(),
|
|
3287
3295
|
get status() {
|
|
3288
3296
|
return state.status;
|
|
3289
3297
|
},
|
|
3290
|
-
stopRecording,
|
|
3291
3298
|
subscribe: (subscriber) => {
|
|
3292
3299
|
subscribers.add(subscriber);
|
|
3293
3300
|
return () => {
|
|
@@ -3303,15 +3310,6 @@ var createVoiceController = (path, options = {}) => {
|
|
|
3303
3310
|
},
|
|
3304
3311
|
get turns() {
|
|
3305
3312
|
return state.turns;
|
|
3306
|
-
},
|
|
3307
|
-
get assistantTexts() {
|
|
3308
|
-
return state.assistantTexts;
|
|
3309
|
-
},
|
|
3310
|
-
get assistantAudio() {
|
|
3311
|
-
return state.assistantAudio;
|
|
3312
|
-
},
|
|
3313
|
-
get call() {
|
|
3314
|
-
return state.call;
|
|
3315
3313
|
}
|
|
3316
3314
|
};
|
|
3317
3315
|
};
|
|
@@ -3438,9 +3436,6 @@ var createFakeController = (scenario) => {
|
|
|
3438
3436
|
const subscribers = new Set;
|
|
3439
3437
|
const sentAudio = [];
|
|
3440
3438
|
return {
|
|
3441
|
-
get partial() {
|
|
3442
|
-
return partial;
|
|
3443
|
-
},
|
|
3444
3439
|
emitPartial(next) {
|
|
3445
3440
|
partial = next;
|
|
3446
3441
|
for (const subscriber of subscribers) {
|
|
@@ -3450,6 +3445,9 @@ var createFakeController = (scenario) => {
|
|
|
3450
3445
|
getSentAudio() {
|
|
3451
3446
|
return sentAudio;
|
|
3452
3447
|
},
|
|
3448
|
+
get partial() {
|
|
3449
|
+
return partial;
|
|
3450
|
+
},
|
|
3453
3451
|
sendAudio(audio) {
|
|
3454
3452
|
sentAudio.push(audio);
|
|
3455
3453
|
},
|
|
@@ -3466,6 +3464,13 @@ var createFakePlayer = (delayMs) => {
|
|
|
3466
3464
|
let isPlaying = true;
|
|
3467
3465
|
let lastInterruptLatencyMs;
|
|
3468
3466
|
return {
|
|
3467
|
+
async interrupt() {
|
|
3468
|
+
const startedAt = Date.now();
|
|
3469
|
+
await Bun.sleep(delayMs);
|
|
3470
|
+
interruptCount += 1;
|
|
3471
|
+
isPlaying = false;
|
|
3472
|
+
lastInterruptLatencyMs = Date.now() - startedAt;
|
|
3473
|
+
},
|
|
3469
3474
|
get interruptCount() {
|
|
3470
3475
|
return interruptCount;
|
|
3471
3476
|
},
|
|
@@ -3474,26 +3479,30 @@ var createFakePlayer = (delayMs) => {
|
|
|
3474
3479
|
},
|
|
3475
3480
|
get lastInterruptLatencyMs() {
|
|
3476
3481
|
return lastInterruptLatencyMs;
|
|
3477
|
-
},
|
|
3478
|
-
async interrupt() {
|
|
3479
|
-
const startedAt = Date.now();
|
|
3480
|
-
await Bun.sleep(delayMs);
|
|
3481
|
-
interruptCount += 1;
|
|
3482
|
-
isPlaying = false;
|
|
3483
|
-
lastInterruptLatencyMs = Date.now() - startedAt;
|
|
3484
3482
|
}
|
|
3485
3483
|
};
|
|
3486
3484
|
};
|
|
3487
3485
|
var getDefaultVoiceDuplexBenchmarkScenarios = () => DEFAULT_SCENARIOS.map((scenario) => ({ ...scenario }));
|
|
3488
|
-
var
|
|
3489
|
-
const
|
|
3490
|
-
const
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3486
|
+
var runVoiceDuplexBenchmark = async (scenarios = getDefaultVoiceDuplexBenchmarkScenarios(), options = {}) => {
|
|
3487
|
+
const fixtures = [];
|
|
3488
|
+
for (const scenario of scenarios) {
|
|
3489
|
+
fixtures.push(await runVoiceDuplexBenchmarkScenario(scenario, options));
|
|
3490
|
+
}
|
|
3491
|
+
return {
|
|
3492
|
+
fixtures,
|
|
3493
|
+
generatedAt: Date.now(),
|
|
3494
|
+
summary: summarizeVoiceDuplexBenchmark(fixtures)
|
|
3495
|
+
};
|
|
3496
|
+
};
|
|
3497
|
+
var runVoiceDuplexBenchmarkScenario = async (scenario, options = {}) => {
|
|
3498
|
+
const controller = createFakeController(scenario);
|
|
3499
|
+
const player = createFakePlayer(scenario.interruptDelayMs ?? options.defaultInterruptDelayMs ?? 1);
|
|
3500
|
+
const binding = bindVoiceBargeIn(controller, player, options.bargeIn);
|
|
3501
|
+
const startedAt = Date.now();
|
|
3502
|
+
try {
|
|
3503
|
+
switch (scenario.mode) {
|
|
3504
|
+
case "audio-send":
|
|
3505
|
+
binding.sendAudio(new Uint8Array([1, 2, 3]));
|
|
3497
3506
|
break;
|
|
3498
3507
|
case "input-level":
|
|
3499
3508
|
binding.handleLevel(scenario.level ?? 0.2);
|
|
@@ -3529,17 +3538,6 @@ var summarizeVoiceDuplexBenchmark = (fixtures) => {
|
|
|
3529
3538
|
scenarioCount
|
|
3530
3539
|
};
|
|
3531
3540
|
};
|
|
3532
|
-
var runVoiceDuplexBenchmark = async (scenarios = getDefaultVoiceDuplexBenchmarkScenarios(), options = {}) => {
|
|
3533
|
-
const fixtures = [];
|
|
3534
|
-
for (const scenario of scenarios) {
|
|
3535
|
-
fixtures.push(await runVoiceDuplexBenchmarkScenario(scenario, options));
|
|
3536
|
-
}
|
|
3537
|
-
return {
|
|
3538
|
-
fixtures,
|
|
3539
|
-
generatedAt: Date.now(),
|
|
3540
|
-
summary: summarizeVoiceDuplexBenchmark(fixtures)
|
|
3541
|
-
};
|
|
3542
|
-
};
|
|
3543
3541
|
// src/testing/fixtures.ts
|
|
3544
3542
|
import { resolve } from "path";
|
|
3545
3543
|
var JARGON_FIXTURE_IDS = [
|
|
@@ -3711,6 +3709,12 @@ var requireFixture = (fixtures, id) => {
|
|
|
3711
3709
|
}
|
|
3712
3710
|
return fixture;
|
|
3713
3711
|
};
|
|
3712
|
+
var createJargonVoiceTestFixtures = (fixtures) => JARGON_FIXTURE_IDS.map((id) => requireFixture(fixtures, id)).filter((fixture) => (fixture.expectedTerms?.length ?? 0) > 0).map((fixture) => ({
|
|
3713
|
+
...fixture,
|
|
3714
|
+
id: `${fixture.id}-jargon`,
|
|
3715
|
+
tags: Array.from(new Set([...fixture.tags ?? [], "domain-heavy", "jargon"])),
|
|
3716
|
+
title: `${fixture.title} (jargon)`
|
|
3717
|
+
}));
|
|
3714
3718
|
var createMultiSpeakerVoiceTestFixtures = (fixtures, options = {}) => {
|
|
3715
3719
|
const silenceMs = options.silenceMs ?? DEFAULT_MULTI_SPEAKER_SILENCE_MS;
|
|
3716
3720
|
const speakerA = requireFixture(fixtures, "quietly-alone-clean");
|
|
@@ -3776,12 +3780,6 @@ var createMultiSpeakerVoiceTestFixtures = (fixtures, options = {}) => {
|
|
|
3776
3780
|
}
|
|
3777
3781
|
];
|
|
3778
3782
|
};
|
|
3779
|
-
var createJargonVoiceTestFixtures = (fixtures) => JARGON_FIXTURE_IDS.map((id) => requireFixture(fixtures, id)).filter((fixture) => (fixture.expectedTerms?.length ?? 0) > 0).map((fixture) => ({
|
|
3780
|
-
...fixture,
|
|
3781
|
-
id: `${fixture.id}-jargon`,
|
|
3782
|
-
tags: Array.from(new Set([...fixture.tags ?? [], "domain-heavy", "jargon"])),
|
|
3783
|
-
title: `${fixture.title} (jargon)`
|
|
3784
|
-
}));
|
|
3785
3783
|
var loadVoiceTestFixtures = async (fixtureDirectory) => {
|
|
3786
3784
|
const fixtureDirectories = await resolveVoiceFixtureDirectories(fixtureDirectory);
|
|
3787
3785
|
const fixtures = [];
|
|
@@ -4019,6 +4017,7 @@ var createVoiceProviderOrchestrationProfile = (options) => {
|
|
|
4019
4017
|
return {
|
|
4020
4018
|
defaultSurface,
|
|
4021
4019
|
id: options.id,
|
|
4020
|
+
surfaces: options.surfaces,
|
|
4022
4021
|
resolve: (surface = defaultSurface) => {
|
|
4023
4022
|
const config = options.surfaces[surface];
|
|
4024
4023
|
if (!config) {
|
|
@@ -4034,8 +4033,7 @@ var createVoiceProviderOrchestrationProfile = (options) => {
|
|
|
4034
4033
|
providerProfiles: config.providerProfiles,
|
|
4035
4034
|
timeoutMs: config.timeoutMs
|
|
4036
4035
|
};
|
|
4037
|
-
}
|
|
4038
|
-
surfaces: options.surfaces
|
|
4036
|
+
}
|
|
4039
4037
|
};
|
|
4040
4038
|
};
|
|
4041
4039
|
var OUTPUT_SCHEMA = {
|
|
@@ -4249,7 +4247,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
4249
4247
|
if (!healthOptions) {
|
|
4250
4248
|
return;
|
|
4251
4249
|
}
|
|
4252
|
-
const suppressedUntil = getHealth(provider)
|
|
4250
|
+
const { suppressedUntil } = getHealth(provider);
|
|
4253
4251
|
return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
|
|
4254
4252
|
};
|
|
4255
4253
|
const isSuppressed = (provider) => {
|
|
@@ -4775,11 +4773,11 @@ var extractGeminiCandidateParts = (response) => {
|
|
|
4775
4773
|
if (!first || typeof first !== "object") {
|
|
4776
4774
|
return [];
|
|
4777
4775
|
}
|
|
4778
|
-
const content = first
|
|
4776
|
+
const { content } = first;
|
|
4779
4777
|
if (!content || typeof content !== "object") {
|
|
4780
4778
|
return [];
|
|
4781
4779
|
}
|
|
4782
|
-
const parts = content
|
|
4780
|
+
const { parts } = content;
|
|
4783
4781
|
return Array.isArray(parts) ? parts : [];
|
|
4784
4782
|
};
|
|
4785
4783
|
var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
@@ -4790,7 +4788,7 @@ var extractGeminiToolCalls = (response) => {
|
|
|
4790
4788
|
if (!part || typeof part !== "object") {
|
|
4791
4789
|
continue;
|
|
4792
4790
|
}
|
|
4793
|
-
const functionCall = part
|
|
4791
|
+
const { functionCall } = part;
|
|
4794
4792
|
if (!functionCall || typeof functionCall !== "object") {
|
|
4795
4793
|
continue;
|
|
4796
4794
|
}
|
|
@@ -4926,7 +4924,7 @@ var toVoiceSessionSummary = (session) => ({
|
|
|
4926
4924
|
var getContextQuery = (context) => context.query;
|
|
4927
4925
|
var titleCaseProvider = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ");
|
|
4928
4926
|
var resolveRequestedProvider = (context, providers) => {
|
|
4929
|
-
const provider = getContextQuery(context)
|
|
4927
|
+
const { provider } = getContextQuery(context);
|
|
4930
4928
|
return providers.includes(provider) ? provider : providers[0];
|
|
4931
4929
|
};
|
|
4932
4930
|
var createVoiceProviderFailureSimulator = (options) => {
|
|
@@ -4956,8 +4954,19 @@ var createVoiceProviderFailureSimulator = (options) => {
|
|
|
4956
4954
|
}
|
|
4957
4955
|
]));
|
|
4958
4956
|
const router = createVoiceProviderRouter({
|
|
4957
|
+
fallbackMode: "provider-error",
|
|
4958
|
+
isProviderError: options.isProviderError,
|
|
4959
|
+
isRateLimitError: options.isRateLimitError,
|
|
4960
|
+
onProviderEvent: options.onProviderEvent,
|
|
4961
|
+
policy: "prefer-selected",
|
|
4962
|
+
providerHealth: options.providerHealth ?? {
|
|
4963
|
+
cooldownMs: 30000,
|
|
4964
|
+
failureThreshold: 1,
|
|
4965
|
+
rateLimitCooldownMs: 120000
|
|
4966
|
+
},
|
|
4967
|
+
providers: providerModels,
|
|
4959
4968
|
allowProviders: async (input) => {
|
|
4960
|
-
const recoverProvider = getContextQuery(input.context)
|
|
4969
|
+
const { recoverProvider } = getContextQuery(input.context);
|
|
4961
4970
|
if (recoverProvider) {
|
|
4962
4971
|
return [recoverProvider];
|
|
4963
4972
|
}
|
|
@@ -4973,17 +4982,6 @@ var createVoiceProviderFailureSimulator = (options) => {
|
|
|
4973
4982
|
}
|
|
4974
4983
|
return options.fallback ?? options.providers.filter((provider) => provider !== selectedProvider);
|
|
4975
4984
|
},
|
|
4976
|
-
fallbackMode: "provider-error",
|
|
4977
|
-
isProviderError: options.isProviderError,
|
|
4978
|
-
isRateLimitError: options.isRateLimitError,
|
|
4979
|
-
onProviderEvent: options.onProviderEvent,
|
|
4980
|
-
policy: "prefer-selected",
|
|
4981
|
-
providerHealth: options.providerHealth ?? {
|
|
4982
|
-
cooldownMs: 30000,
|
|
4983
|
-
failureThreshold: 1,
|
|
4984
|
-
rateLimitCooldownMs: 120000
|
|
4985
|
-
},
|
|
4986
|
-
providers: providerModels,
|
|
4987
4985
|
selectProvider: ({ context }) => resolveRequestedProvider(context, options.providers)
|
|
4988
4986
|
});
|
|
4989
4987
|
const run = async (provider, mode) => {
|
|
@@ -5107,41 +5105,15 @@ var defaultWebhookBody = (input) => ({
|
|
|
5107
5105
|
source: "absolutejs-voice",
|
|
5108
5106
|
target: input.target
|
|
5109
5107
|
});
|
|
5110
|
-
var
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
}
|
|
5120
|
-
try {
|
|
5121
|
-
const result = await adapter.handoff(input.handoff);
|
|
5122
|
-
deliveries[adapter.id] = {
|
|
5123
|
-
...result,
|
|
5124
|
-
adapterId: adapter.id,
|
|
5125
|
-
adapterKind: adapter.kind
|
|
5126
|
-
};
|
|
5127
|
-
} catch (error) {
|
|
5128
|
-
deliveries[adapter.id] = {
|
|
5129
|
-
adapterId: adapter.id,
|
|
5130
|
-
adapterKind: adapter.kind,
|
|
5131
|
-
error: toErrorMessage(error),
|
|
5132
|
-
status: "failed"
|
|
5133
|
-
};
|
|
5134
|
-
if (input.config.failMode === "throw") {
|
|
5135
|
-
throw error;
|
|
5136
|
-
}
|
|
5137
|
-
}
|
|
5138
|
-
}
|
|
5139
|
-
return {
|
|
5140
|
-
action: input.handoff.action,
|
|
5141
|
-
deliveries,
|
|
5142
|
-
status: aggregateHandoffStatus(deliveries)
|
|
5143
|
-
};
|
|
5144
|
-
};
|
|
5108
|
+
var applyVoiceHandoffDeliveryResult = (delivery, result) => ({
|
|
5109
|
+
...delivery,
|
|
5110
|
+
deliveredAt: result.status === "delivered" || result.status === "skipped" ? Date.now() : delivery.deliveredAt,
|
|
5111
|
+
deliveries: result.deliveries,
|
|
5112
|
+
deliveryAttempts: (delivery.deliveryAttempts ?? 0) + 1,
|
|
5113
|
+
deliveryError: result.status === "failed" ? resolveHandoffDeliveryError(result.deliveries) : undefined,
|
|
5114
|
+
deliveryStatus: result.status,
|
|
5115
|
+
updatedAt: Date.now()
|
|
5116
|
+
});
|
|
5145
5117
|
var createVoiceHandoffDeliveryRecord = (input) => {
|
|
5146
5118
|
const now = Date.now();
|
|
5147
5119
|
return {
|
|
@@ -5163,39 +5135,6 @@ var createVoiceHandoffDeliveryRecord = (input) => {
|
|
|
5163
5135
|
updatedAt: now
|
|
5164
5136
|
};
|
|
5165
5137
|
};
|
|
5166
|
-
var applyVoiceHandoffDeliveryResult = (delivery, result) => ({
|
|
5167
|
-
...delivery,
|
|
5168
|
-
deliveredAt: result.status === "delivered" || result.status === "skipped" ? Date.now() : delivery.deliveredAt,
|
|
5169
|
-
deliveries: result.deliveries,
|
|
5170
|
-
deliveryAttempts: (delivery.deliveryAttempts ?? 0) + 1,
|
|
5171
|
-
deliveryError: result.status === "failed" ? resolveHandoffDeliveryError(result.deliveries) : undefined,
|
|
5172
|
-
deliveryStatus: result.status,
|
|
5173
|
-
updatedAt: Date.now()
|
|
5174
|
-
});
|
|
5175
|
-
var deliverVoiceHandoffDelivery = async (options) => {
|
|
5176
|
-
const result = await deliverVoiceHandoff({
|
|
5177
|
-
config: {
|
|
5178
|
-
adapters: options.adapters,
|
|
5179
|
-
failMode: options.failMode
|
|
5180
|
-
},
|
|
5181
|
-
handoff: {
|
|
5182
|
-
action: options.delivery.action,
|
|
5183
|
-
api: options.api,
|
|
5184
|
-
context: options.delivery.context,
|
|
5185
|
-
metadata: options.delivery.metadata,
|
|
5186
|
-
reason: options.delivery.reason,
|
|
5187
|
-
result: options.delivery.result,
|
|
5188
|
-
session: options.delivery.session,
|
|
5189
|
-
target: options.delivery.target
|
|
5190
|
-
}
|
|
5191
|
-
});
|
|
5192
|
-
return result ? applyVoiceHandoffDeliveryResult(options.delivery, result) : {
|
|
5193
|
-
...options.delivery,
|
|
5194
|
-
deliveryAttempts: (options.delivery.deliveryAttempts ?? 0) + 1,
|
|
5195
|
-
deliveryStatus: "skipped",
|
|
5196
|
-
updatedAt: Date.now()
|
|
5197
|
-
};
|
|
5198
|
-
};
|
|
5199
5138
|
var createVoiceMemoryHandoffDeliveryStore = () => {
|
|
5200
5139
|
const deliveries = new Map;
|
|
5201
5140
|
return {
|
|
@@ -5211,6 +5150,8 @@ var createVoiceMemoryHandoffDeliveryStore = () => {
|
|
|
5211
5150
|
};
|
|
5212
5151
|
var createVoiceWebhookHandoffAdapter = (options) => ({
|
|
5213
5152
|
actions: options.actions,
|
|
5153
|
+
id: options.id,
|
|
5154
|
+
kind: options.kind ?? "webhook",
|
|
5214
5155
|
handoff: async (input) => {
|
|
5215
5156
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
5216
5157
|
if (typeof fetchImpl !== "function") {
|
|
@@ -5260,10 +5201,67 @@ var createVoiceWebhookHandoffAdapter = (options) => ({
|
|
|
5260
5201
|
clearTimeout(timeout);
|
|
5261
5202
|
}
|
|
5262
5203
|
}
|
|
5263
|
-
}
|
|
5264
|
-
id: options.id,
|
|
5265
|
-
kind: options.kind ?? "webhook"
|
|
5204
|
+
}
|
|
5266
5205
|
});
|
|
5206
|
+
var deliverVoiceHandoff = async (input) => {
|
|
5207
|
+
if (!input.config || input.config.adapters.length === 0) {
|
|
5208
|
+
return;
|
|
5209
|
+
}
|
|
5210
|
+
const deliveries = {};
|
|
5211
|
+
for (const adapter of input.config.adapters) {
|
|
5212
|
+
if (adapter.actions && !adapter.actions.includes(input.handoff.action)) {
|
|
5213
|
+
deliveries[adapter.id] = createSkippedDelivery(adapter);
|
|
5214
|
+
continue;
|
|
5215
|
+
}
|
|
5216
|
+
try {
|
|
5217
|
+
const result = await adapter.handoff(input.handoff);
|
|
5218
|
+
deliveries[adapter.id] = {
|
|
5219
|
+
...result,
|
|
5220
|
+
adapterId: adapter.id,
|
|
5221
|
+
adapterKind: adapter.kind
|
|
5222
|
+
};
|
|
5223
|
+
} catch (error) {
|
|
5224
|
+
deliveries[adapter.id] = {
|
|
5225
|
+
adapterId: adapter.id,
|
|
5226
|
+
adapterKind: adapter.kind,
|
|
5227
|
+
error: toErrorMessage(error),
|
|
5228
|
+
status: "failed"
|
|
5229
|
+
};
|
|
5230
|
+
if (input.config.failMode === "throw") {
|
|
5231
|
+
throw error;
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
return {
|
|
5236
|
+
action: input.handoff.action,
|
|
5237
|
+
deliveries,
|
|
5238
|
+
status: aggregateHandoffStatus(deliveries)
|
|
5239
|
+
};
|
|
5240
|
+
};
|
|
5241
|
+
var deliverVoiceHandoffDelivery = async (options) => {
|
|
5242
|
+
const result = await deliverVoiceHandoff({
|
|
5243
|
+
config: {
|
|
5244
|
+
adapters: options.adapters,
|
|
5245
|
+
failMode: options.failMode
|
|
5246
|
+
},
|
|
5247
|
+
handoff: {
|
|
5248
|
+
action: options.delivery.action,
|
|
5249
|
+
api: options.api,
|
|
5250
|
+
context: options.delivery.context,
|
|
5251
|
+
metadata: options.delivery.metadata,
|
|
5252
|
+
reason: options.delivery.reason,
|
|
5253
|
+
result: options.delivery.result,
|
|
5254
|
+
session: options.delivery.session,
|
|
5255
|
+
target: options.delivery.target
|
|
5256
|
+
}
|
|
5257
|
+
});
|
|
5258
|
+
return result ? applyVoiceHandoffDeliveryResult(options.delivery, result) : {
|
|
5259
|
+
...options.delivery,
|
|
5260
|
+
deliveryAttempts: (options.delivery.deliveryAttempts ?? 0) + 1,
|
|
5261
|
+
deliveryStatus: "skipped",
|
|
5262
|
+
updatedAt: Date.now()
|
|
5263
|
+
};
|
|
5264
|
+
};
|
|
5267
5265
|
var escapeXml = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
5268
5266
|
var defaultTwilioTransferTwiML = (input) => {
|
|
5269
5267
|
if (!input.target) {
|
|
@@ -5285,6 +5283,8 @@ var resolveTwilioCallSid = async (resolver, input) => {
|
|
|
5285
5283
|
};
|
|
5286
5284
|
var createVoiceTwilioRedirectHandoffAdapter = (options) => ({
|
|
5287
5285
|
actions: options.actions ?? ["transfer"],
|
|
5286
|
+
id: options.id ?? "twilio-redirect",
|
|
5287
|
+
kind: "twilio-redirect",
|
|
5288
5288
|
handoff: async (input) => {
|
|
5289
5289
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
5290
5290
|
const callSid = await resolveTwilioCallSid(options.callSid, input);
|
|
@@ -5337,9 +5337,7 @@ var createVoiceTwilioRedirectHandoffAdapter = (options) => ({
|
|
|
5337
5337
|
clearTimeout(timeout);
|
|
5338
5338
|
}
|
|
5339
5339
|
}
|
|
5340
|
-
}
|
|
5341
|
-
id: options.id ?? "twilio-redirect",
|
|
5342
|
-
kind: "twilio-redirect"
|
|
5340
|
+
}
|
|
5343
5341
|
});
|
|
5344
5342
|
|
|
5345
5343
|
// src/core/logger.ts
|
|
@@ -5374,7 +5372,7 @@ var encodePcmAsWav = (pcm, format) => {
|
|
|
5374
5372
|
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
5375
5373
|
throw new Error(`encodePcmAsWav only supports raw pcm_s16le input (got container=${format.container}, encoding=${format.encoding})`);
|
|
5376
5374
|
}
|
|
5377
|
-
const channels = format
|
|
5375
|
+
const { channels } = format;
|
|
5378
5376
|
const sampleRate = format.sampleRateHz;
|
|
5379
5377
|
const bitsPerSample = 16;
|
|
5380
5378
|
const byteRate = sampleRate * channels * bitsPerSample / 8;
|
|
@@ -5410,28 +5408,6 @@ var interleaveStereoPcm = (input) => {
|
|
|
5410
5408
|
}
|
|
5411
5409
|
return new Uint8Array(output.buffer);
|
|
5412
5410
|
};
|
|
5413
|
-
var encodeStereoWav = ({
|
|
5414
|
-
format,
|
|
5415
|
-
left,
|
|
5416
|
-
right
|
|
5417
|
-
}) => {
|
|
5418
|
-
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
5419
|
-
throw new Error("encodeStereoWav requires raw pcm_s16le format on each channel");
|
|
5420
|
-
}
|
|
5421
|
-
if (format.channels !== 1) {
|
|
5422
|
-
throw new Error("encodeStereoWav expects mono input channels");
|
|
5423
|
-
}
|
|
5424
|
-
const interleaved = interleaveStereoPcm({ left, right });
|
|
5425
|
-
return encodePcmAsWav(interleaved, { ...format, channels: 2 });
|
|
5426
|
-
};
|
|
5427
|
-
var createVoiceWavRecordingEncoder = () => ({
|
|
5428
|
-
encode: ({ format, pcm }) => ({
|
|
5429
|
-
bytes: encodePcmAsWav(pcm, format),
|
|
5430
|
-
contentType: "audio/wav",
|
|
5431
|
-
extension: "wav"
|
|
5432
|
-
}),
|
|
5433
|
-
kind: "wav"
|
|
5434
|
-
});
|
|
5435
5411
|
var computePcmDurationMs = (pcmByteLength, format) => {
|
|
5436
5412
|
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
5437
5413
|
return 0;
|
|
@@ -5458,6 +5434,28 @@ var createVoiceMemoryRecordingStore = () => {
|
|
|
5458
5434
|
}
|
|
5459
5435
|
};
|
|
5460
5436
|
};
|
|
5437
|
+
var createVoiceWavRecordingEncoder = () => ({
|
|
5438
|
+
kind: "wav",
|
|
5439
|
+
encode: ({ format, pcm }) => ({
|
|
5440
|
+
bytes: encodePcmAsWav(pcm, format),
|
|
5441
|
+
contentType: "audio/wav",
|
|
5442
|
+
extension: "wav"
|
|
5443
|
+
})
|
|
5444
|
+
});
|
|
5445
|
+
var encodeStereoWav = ({
|
|
5446
|
+
format,
|
|
5447
|
+
left,
|
|
5448
|
+
right
|
|
5449
|
+
}) => {
|
|
5450
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
5451
|
+
throw new Error("encodeStereoWav requires raw pcm_s16le format on each channel");
|
|
5452
|
+
}
|
|
5453
|
+
if (format.channels !== 1) {
|
|
5454
|
+
throw new Error("encodeStereoWav expects mono input channels");
|
|
5455
|
+
}
|
|
5456
|
+
const interleaved = interleaveStereoPcm({ left, right });
|
|
5457
|
+
return encodePcmAsWav(interleaved, { ...format, channels: 2 });
|
|
5458
|
+
};
|
|
5461
5459
|
|
|
5462
5460
|
// src/core/assistantMode.ts
|
|
5463
5461
|
var resolveVoiceAssistantMode = (options) => {
|
|
@@ -5710,7 +5708,7 @@ var createVoiceSession = (options) => {
|
|
|
5710
5708
|
});
|
|
5711
5709
|
const phraseHints = options.phraseHints ?? [];
|
|
5712
5710
|
const lexicon = options.lexicon ?? [];
|
|
5713
|
-
let socket = options
|
|
5711
|
+
let { socket } = options;
|
|
5714
5712
|
let sttSession = null;
|
|
5715
5713
|
let ttsSession = null;
|
|
5716
5714
|
let ttsSessionPromise = null;
|
|
@@ -7589,13 +7587,13 @@ var createVoiceSession = (options) => {
|
|
|
7589
7587
|
disconnect: async (event) => runSerial("api.disconnect", async () => {
|
|
7590
7588
|
await disconnectInternal(event);
|
|
7591
7589
|
}),
|
|
7592
|
-
fail: async (error) => runSerial("api.fail", async () => {
|
|
7593
|
-
await failInternal(error);
|
|
7594
|
-
}),
|
|
7595
7590
|
escalate: async (input) => runSerial("api.escalate", async () => {
|
|
7596
7591
|
await escalateInternal(input);
|
|
7597
7592
|
}),
|
|
7598
|
-
|
|
7593
|
+
fail: async (error) => runSerial("api.fail", async () => {
|
|
7594
|
+
await failInternal(error);
|
|
7595
|
+
}),
|
|
7596
|
+
markNoAnswer: async (input) => runSerial("api.markNoAnswer", async () => {
|
|
7599
7597
|
await markNoAnswerInternal(input);
|
|
7600
7598
|
}),
|
|
7601
7599
|
markVoicemail: async (input) => runSerial("api.markVoicemail", async () => {
|
|
@@ -7604,10 +7602,10 @@ var createVoiceSession = (options) => {
|
|
|
7604
7602
|
receiveAudio: async (audio) => runSerial("api.receiveAudio", async () => {
|
|
7605
7603
|
await receiveAudioInternal(audio);
|
|
7606
7604
|
}),
|
|
7605
|
+
snapshot: async () => runSerial("api.snapshot", async () => readSession()),
|
|
7607
7606
|
transfer: async (input) => runSerial("api.transfer", async () => {
|
|
7608
7607
|
await transferInternal(input);
|
|
7609
|
-
})
|
|
7610
|
-
snapshot: async () => runSerial("api.snapshot", async () => readSession())
|
|
7608
|
+
})
|
|
7611
7609
|
};
|
|
7612
7610
|
return api;
|
|
7613
7611
|
};
|
|
@@ -7689,6 +7687,7 @@ var runScenario = async (id, title, run) => {
|
|
|
7689
7687
|
try {
|
|
7690
7688
|
await run({
|
|
7691
7689
|
adapter,
|
|
7690
|
+
turns,
|
|
7692
7691
|
commit: async (text, transcriptId = `${id}-${turns.length}`) => {
|
|
7693
7692
|
await adapter.session.emit("final", {
|
|
7694
7693
|
receivedAt: Date.now(),
|
|
@@ -7729,8 +7728,7 @@ var runScenario = async (id, title, run) => {
|
|
|
7729
7728
|
},
|
|
7730
7729
|
type: "final"
|
|
7731
7730
|
});
|
|
7732
|
-
}
|
|
7733
|
-
turns
|
|
7731
|
+
}
|
|
7734
7732
|
});
|
|
7735
7733
|
} finally {
|
|
7736
7734
|
await voice.close("resilience-complete");
|
|
@@ -7962,10 +7960,6 @@ var compactTimeline = (timeline) => {
|
|
|
7962
7960
|
}
|
|
7963
7961
|
return rows;
|
|
7964
7962
|
};
|
|
7965
|
-
var withVoiceCallReviewId = (id, artifact) => ({
|
|
7966
|
-
...artifact,
|
|
7967
|
-
id
|
|
7968
|
-
});
|
|
7969
7963
|
var createVoiceCallReviewFromLiveTelephonyReport = (report, options = {}) => {
|
|
7970
7964
|
const fixture = report.fixtures?.[0];
|
|
7971
7965
|
if (!fixture) {
|
|
@@ -8044,6 +8038,10 @@ var createVoiceCallReviewFromLiveTelephonyReport = (report, options = {}) => {
|
|
|
8044
8038
|
}
|
|
8045
8039
|
};
|
|
8046
8040
|
};
|
|
8041
|
+
var withVoiceCallReviewId = (id, artifact) => ({
|
|
8042
|
+
...artifact,
|
|
8043
|
+
id
|
|
8044
|
+
});
|
|
8047
8045
|
var toErrorMessage2 = (error) => {
|
|
8048
8046
|
if (typeof error === "string" && error.trim().length > 0) {
|
|
8049
8047
|
return error;
|
|
@@ -8206,7 +8204,6 @@ var createVoiceCallReviewRecorder = (options = {}) => {
|
|
|
8206
8204
|
return;
|
|
8207
8205
|
case "pong":
|
|
8208
8206
|
push("benchmark", "pong");
|
|
8209
|
-
return;
|
|
8210
8207
|
}
|
|
8211
8208
|
}
|
|
8212
8209
|
};
|
|
@@ -8267,54 +8264,6 @@ var renderLatencyBreakdown = (breakdown) => {
|
|
|
8267
8264
|
].join(`
|
|
8268
8265
|
`);
|
|
8269
8266
|
};
|
|
8270
|
-
var renderVoiceCallReviewMarkdown = (artifact) => {
|
|
8271
|
-
const summaryLines = [
|
|
8272
|
-
`- pass: ${artifact.summary.pass ? "yes" : "no"}`,
|
|
8273
|
-
formatMetric("first turn", artifact.summary.firstTurnLatencyMs),
|
|
8274
|
-
formatMetric("first outbound media", artifact.summary.firstOutboundMediaLatencyMs),
|
|
8275
|
-
formatMetric("mark", artifact.summary.markLatencyMs),
|
|
8276
|
-
formatMetric("clear", artifact.summary.clearLatencyMs),
|
|
8277
|
-
formatMetric("elapsed", artifact.summary.elapsedMs),
|
|
8278
|
-
typeof artifact.summary.wordErrorRate === "number" ? `- word error rate: ${artifact.summary.wordErrorRate}` : undefined,
|
|
8279
|
-
typeof artifact.summary.termRecall === "number" ? `- term recall: ${artifact.summary.termRecall}` : undefined,
|
|
8280
|
-
typeof artifact.summary.turnCount === "number" ? `- turn count: ${artifact.summary.turnCount}` : undefined,
|
|
8281
|
-
typeof artifact.summary.outboundMediaCount === "number" ? `- outbound media count: ${artifact.summary.outboundMediaCount}` : undefined
|
|
8282
|
-
].filter((value) => typeof value === "string");
|
|
8283
|
-
const notes = artifact.notes.length ? ["## Notes", "", ...artifact.notes.map((note) => `- ${note}`)].join(`
|
|
8284
|
-
`) : "";
|
|
8285
|
-
const errors = artifact.errors.length ? ["## Errors", "", ...artifact.errors.map((error) => `- ${error}`)].join(`
|
|
8286
|
-
`) : "";
|
|
8287
|
-
const latency = renderLatencyBreakdown(artifact.latencyBreakdown);
|
|
8288
|
-
const transportSummary = renderTransportSummary(artifact.timeline);
|
|
8289
|
-
return [
|
|
8290
|
-
`# ${artifact.title}`,
|
|
8291
|
-
"",
|
|
8292
|
-
artifact.path ? `Source: \`${artifact.path}\`` : undefined,
|
|
8293
|
-
artifact.fixtureId ? `Fixture: \`${artifact.fixtureId}\`` : undefined,
|
|
8294
|
-
"",
|
|
8295
|
-
"## Summary",
|
|
8296
|
-
"",
|
|
8297
|
-
...summaryLines,
|
|
8298
|
-
"",
|
|
8299
|
-
"## Transcript",
|
|
8300
|
-
"",
|
|
8301
|
-
`- expected: ${artifact.transcript.expected ?? "_n/a_"}`,
|
|
8302
|
-
`- actual: ${artifact.transcript.actual}`,
|
|
8303
|
-
"",
|
|
8304
|
-
notes,
|
|
8305
|
-
notes ? "" : undefined,
|
|
8306
|
-
latency,
|
|
8307
|
-
latency ? "" : undefined,
|
|
8308
|
-
transportSummary,
|
|
8309
|
-
transportSummary ? "" : undefined,
|
|
8310
|
-
errors,
|
|
8311
|
-
errors ? "" : undefined,
|
|
8312
|
-
renderConfigSection(artifact.config),
|
|
8313
|
-
renderConfigSection(artifact.config) ? "" : undefined,
|
|
8314
|
-
renderTimeline(artifact.timeline)
|
|
8315
|
-
].filter((value) => typeof value === "string").join(`
|
|
8316
|
-
`);
|
|
8317
|
-
};
|
|
8318
8267
|
var renderVoiceCallReviewHTML = (artifact) => {
|
|
8319
8268
|
const notes = artifact.notes.map((note) => `<li>${escapeHtml(note)}</li>`).join("");
|
|
8320
8269
|
const latency = artifact.latencyBreakdown.map((entry) => `<li><strong>${escapeHtml(entry.label)}:</strong> ${roundMetric4(entry.valueMs)}ms</li>`).join("");
|
|
@@ -8392,6 +8341,54 @@ var renderVoiceCallReviewHTML = (artifact) => {
|
|
|
8392
8341
|
</body>
|
|
8393
8342
|
</html>`;
|
|
8394
8343
|
};
|
|
8344
|
+
var renderVoiceCallReviewMarkdown = (artifact) => {
|
|
8345
|
+
const summaryLines = [
|
|
8346
|
+
`- pass: ${artifact.summary.pass ? "yes" : "no"}`,
|
|
8347
|
+
formatMetric("first turn", artifact.summary.firstTurnLatencyMs),
|
|
8348
|
+
formatMetric("first outbound media", artifact.summary.firstOutboundMediaLatencyMs),
|
|
8349
|
+
formatMetric("mark", artifact.summary.markLatencyMs),
|
|
8350
|
+
formatMetric("clear", artifact.summary.clearLatencyMs),
|
|
8351
|
+
formatMetric("elapsed", artifact.summary.elapsedMs),
|
|
8352
|
+
typeof artifact.summary.wordErrorRate === "number" ? `- word error rate: ${artifact.summary.wordErrorRate}` : undefined,
|
|
8353
|
+
typeof artifact.summary.termRecall === "number" ? `- term recall: ${artifact.summary.termRecall}` : undefined,
|
|
8354
|
+
typeof artifact.summary.turnCount === "number" ? `- turn count: ${artifact.summary.turnCount}` : undefined,
|
|
8355
|
+
typeof artifact.summary.outboundMediaCount === "number" ? `- outbound media count: ${artifact.summary.outboundMediaCount}` : undefined
|
|
8356
|
+
].filter((value) => typeof value === "string");
|
|
8357
|
+
const notes = artifact.notes.length ? ["## Notes", "", ...artifact.notes.map((note) => `- ${note}`)].join(`
|
|
8358
|
+
`) : "";
|
|
8359
|
+
const errors = artifact.errors.length ? ["## Errors", "", ...artifact.errors.map((error) => `- ${error}`)].join(`
|
|
8360
|
+
`) : "";
|
|
8361
|
+
const latency = renderLatencyBreakdown(artifact.latencyBreakdown);
|
|
8362
|
+
const transportSummary = renderTransportSummary(artifact.timeline);
|
|
8363
|
+
return [
|
|
8364
|
+
`# ${artifact.title}`,
|
|
8365
|
+
"",
|
|
8366
|
+
artifact.path ? `Source: \`${artifact.path}\`` : undefined,
|
|
8367
|
+
artifact.fixtureId ? `Fixture: \`${artifact.fixtureId}\`` : undefined,
|
|
8368
|
+
"",
|
|
8369
|
+
"## Summary",
|
|
8370
|
+
"",
|
|
8371
|
+
...summaryLines,
|
|
8372
|
+
"",
|
|
8373
|
+
"## Transcript",
|
|
8374
|
+
"",
|
|
8375
|
+
`- expected: ${artifact.transcript.expected ?? "_n/a_"}`,
|
|
8376
|
+
`- actual: ${artifact.transcript.actual}`,
|
|
8377
|
+
"",
|
|
8378
|
+
notes,
|
|
8379
|
+
notes ? "" : undefined,
|
|
8380
|
+
latency,
|
|
8381
|
+
latency ? "" : undefined,
|
|
8382
|
+
transportSummary,
|
|
8383
|
+
transportSummary ? "" : undefined,
|
|
8384
|
+
errors,
|
|
8385
|
+
errors ? "" : undefined,
|
|
8386
|
+
renderConfigSection(artifact.config),
|
|
8387
|
+
renderConfigSection(artifact.config) ? "" : undefined,
|
|
8388
|
+
renderTimeline(artifact.timeline)
|
|
8389
|
+
].filter((value) => typeof value === "string").join(`
|
|
8390
|
+
`);
|
|
8391
|
+
};
|
|
8395
8392
|
// src/testing/sessionBenchmark.ts
|
|
8396
8393
|
var average3 = (values) => values.length > 0 ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
8397
8394
|
var normalizeTurnText = (value) => value.toLowerCase().replace(/[^\p{L}\p{N}\s']/gu, " ").replace(/\s+/g, " ").trim();
|
|
@@ -8498,6 +8495,22 @@ var waitForSessionIdle = async (session, settleMs, idleTimeoutMs) => {
|
|
|
8498
8495
|
await Bun.sleep(Math.min(100, settleMs));
|
|
8499
8496
|
}
|
|
8500
8497
|
};
|
|
8498
|
+
var runVoiceSessionBenchmark = async (input) => {
|
|
8499
|
+
const scenarioResults = [];
|
|
8500
|
+
for (const scenario of input.scenarios) {
|
|
8501
|
+
scenarioResults.push(await runVoiceSessionBenchmarkScenario(input.adapter, scenario, {
|
|
8502
|
+
correctTurn: input.correctTurn,
|
|
8503
|
+
sttFallback: input.sttFallback,
|
|
8504
|
+
trace: input.trace
|
|
8505
|
+
}));
|
|
8506
|
+
}
|
|
8507
|
+
return {
|
|
8508
|
+
adapterId: input.adapterId,
|
|
8509
|
+
generatedAt: Date.now(),
|
|
8510
|
+
scenarios: scenarioResults,
|
|
8511
|
+
summary: summarizeVoiceSessionBenchmark(input.adapterId, scenarioResults)
|
|
8512
|
+
};
|
|
8513
|
+
};
|
|
8501
8514
|
var runVoiceSessionBenchmarkScenario = async (adapter, fixture, options = {}) => {
|
|
8502
8515
|
const store = createVoiceMemoryStore();
|
|
8503
8516
|
const committedTurns = [];
|
|
@@ -8712,6 +8725,24 @@ var runVoiceSessionBenchmarkScenario = async (adapter, fixture, options = {}) =>
|
|
|
8712
8725
|
trace: options.trace ? trace : undefined
|
|
8713
8726
|
};
|
|
8714
8727
|
};
|
|
8728
|
+
var runVoiceSessionBenchmarkSeries = async (input) => {
|
|
8729
|
+
const reports = [];
|
|
8730
|
+
const runCount = Math.max(1, Math.floor(input.runs));
|
|
8731
|
+
for (let runIndex = 0;runIndex < runCount; runIndex += 1) {
|
|
8732
|
+
reports.push(await runVoiceSessionBenchmark({
|
|
8733
|
+
adapter: input.adapter,
|
|
8734
|
+
adapterId: input.adapterId,
|
|
8735
|
+
correctTurn: input.correctTurn,
|
|
8736
|
+
scenarios: input.scenarios,
|
|
8737
|
+
sttFallback: input.sttFallback,
|
|
8738
|
+
trace: input.trace
|
|
8739
|
+
}));
|
|
8740
|
+
}
|
|
8741
|
+
return summarizeVoiceSessionBenchmarkSeries({
|
|
8742
|
+
adapterId: input.adapterId,
|
|
8743
|
+
reports
|
|
8744
|
+
});
|
|
8745
|
+
};
|
|
8715
8746
|
var summarizeVoiceSessionBenchmark = (adapterId, scenarios) => {
|
|
8716
8747
|
const passCount = scenarios.filter((scenario) => scenario.passes).length;
|
|
8717
8748
|
const reconnectScenarios = scenarios.filter((scenario) => scenario.expectedReconnectCount > 0);
|
|
@@ -8801,40 +8832,6 @@ var summarizeVoiceSessionBenchmarkSeries = (input) => {
|
|
|
8801
8832
|
}
|
|
8802
8833
|
};
|
|
8803
8834
|
};
|
|
8804
|
-
var runVoiceSessionBenchmark = async (input) => {
|
|
8805
|
-
const scenarioResults = [];
|
|
8806
|
-
for (const scenario of input.scenarios) {
|
|
8807
|
-
scenarioResults.push(await runVoiceSessionBenchmarkScenario(input.adapter, scenario, {
|
|
8808
|
-
correctTurn: input.correctTurn,
|
|
8809
|
-
sttFallback: input.sttFallback,
|
|
8810
|
-
trace: input.trace
|
|
8811
|
-
}));
|
|
8812
|
-
}
|
|
8813
|
-
return {
|
|
8814
|
-
adapterId: input.adapterId,
|
|
8815
|
-
generatedAt: Date.now(),
|
|
8816
|
-
scenarios: scenarioResults,
|
|
8817
|
-
summary: summarizeVoiceSessionBenchmark(input.adapterId, scenarioResults)
|
|
8818
|
-
};
|
|
8819
|
-
};
|
|
8820
|
-
var runVoiceSessionBenchmarkSeries = async (input) => {
|
|
8821
|
-
const reports = [];
|
|
8822
|
-
const runCount = Math.max(1, Math.floor(input.runs));
|
|
8823
|
-
for (let runIndex = 0;runIndex < runCount; runIndex += 1) {
|
|
8824
|
-
reports.push(await runVoiceSessionBenchmark({
|
|
8825
|
-
adapter: input.adapter,
|
|
8826
|
-
adapterId: input.adapterId,
|
|
8827
|
-
correctTurn: input.correctTurn,
|
|
8828
|
-
scenarios: input.scenarios,
|
|
8829
|
-
sttFallback: input.sttFallback,
|
|
8830
|
-
trace: input.trace
|
|
8831
|
-
}));
|
|
8832
|
-
}
|
|
8833
|
-
return summarizeVoiceSessionBenchmarkSeries({
|
|
8834
|
-
adapterId: input.adapterId,
|
|
8835
|
-
reports
|
|
8836
|
-
});
|
|
8837
|
-
};
|
|
8838
8835
|
// src/core/operationsRecord.ts
|
|
8839
8836
|
import { Elysia as Elysia4 } from "elysia";
|
|
8840
8837
|
import {
|
|
@@ -8858,6 +8855,63 @@ var createVoiceAuditEvent = (event) => ({
|
|
|
8858
8855
|
at: event.at ?? Date.now(),
|
|
8859
8856
|
id: event.id ?? crypto.randomUUID()
|
|
8860
8857
|
});
|
|
8858
|
+
var createVoiceAuditLogger = (store) => ({
|
|
8859
|
+
handoff: (input) => recordVoiceHandoffAuditEvent({ ...input, store }),
|
|
8860
|
+
operatorAction: (input) => recordVoiceOperatorAuditEvent({ ...input, store }),
|
|
8861
|
+
providerCall: (input) => recordVoiceProviderAuditEvent({ ...input, store }),
|
|
8862
|
+
record: (event) => recordVoiceAuditEvent(store, event),
|
|
8863
|
+
retention: (input) => recordVoiceRetentionAuditEvent({ ...input, store }),
|
|
8864
|
+
toolCall: (input) => recordVoiceToolAuditEvent({ ...input, store })
|
|
8865
|
+
});
|
|
8866
|
+
var createVoiceMemoryAuditEventStore = () => {
|
|
8867
|
+
const events = new Map;
|
|
8868
|
+
return {
|
|
8869
|
+
append: (event) => {
|
|
8870
|
+
const stored = createVoiceAuditEvent(event);
|
|
8871
|
+
events.set(stored.id, stored);
|
|
8872
|
+
return stored;
|
|
8873
|
+
},
|
|
8874
|
+
get: (id) => events.get(id),
|
|
8875
|
+
list: (filter) => filterVoiceAuditEvents([...events.values()], filter)
|
|
8876
|
+
};
|
|
8877
|
+
};
|
|
8878
|
+
var createVoiceScopedAuditEventStore = (store, scope) => {
|
|
8879
|
+
const upstreamFilter = (filter = {}) => {
|
|
8880
|
+
const next = { ...filter };
|
|
8881
|
+
delete next.limit;
|
|
8882
|
+
if (scope.actorId !== undefined) {
|
|
8883
|
+
delete next.actorId;
|
|
8884
|
+
}
|
|
8885
|
+
if (scope.outcome !== undefined) {
|
|
8886
|
+
delete next.outcome;
|
|
8887
|
+
}
|
|
8888
|
+
if (scope.resourceId !== undefined) {
|
|
8889
|
+
delete next.resourceId;
|
|
8890
|
+
}
|
|
8891
|
+
if (scope.resourceType !== undefined) {
|
|
8892
|
+
delete next.resourceType;
|
|
8893
|
+
}
|
|
8894
|
+
if (scope.sessionId !== undefined) {
|
|
8895
|
+
delete next.sessionId;
|
|
8896
|
+
}
|
|
8897
|
+
if (scope.traceId !== undefined) {
|
|
8898
|
+
delete next.traceId;
|
|
8899
|
+
}
|
|
8900
|
+
if (scope.type !== undefined) {
|
|
8901
|
+
delete next.type;
|
|
8902
|
+
}
|
|
8903
|
+
return next;
|
|
8904
|
+
};
|
|
8905
|
+
const scopedFilter = (filter = {}) => ({
|
|
8906
|
+
...filter,
|
|
8907
|
+
...scope
|
|
8908
|
+
});
|
|
8909
|
+
return {
|
|
8910
|
+
append: (event) => store.append(event),
|
|
8911
|
+
get: (id) => store.get(id),
|
|
8912
|
+
list: async (filter) => filterVoiceAuditEvents(await store.list(upstreamFilter(filter)), scopedFilter(filter))
|
|
8913
|
+
};
|
|
8914
|
+
};
|
|
8861
8915
|
var filterVoiceAuditEvents = (events, filter = {}) => {
|
|
8862
8916
|
const sorted = events.filter((event) => {
|
|
8863
8917
|
if (!includes(filter.type, event.type)) {
|
|
@@ -8897,114 +8951,57 @@ var filterVoiceAuditEvents = (events, filter = {}) => {
|
|
|
8897
8951
|
}).sort((left, right) => left.at - right.at || left.id.localeCompare(right.id));
|
|
8898
8952
|
return typeof filter.limit === "number" && filter.limit >= 0 ? sorted.slice(0, filter.limit) : sorted;
|
|
8899
8953
|
};
|
|
8900
|
-
var createVoiceScopedAuditEventStore = (store, scope) => {
|
|
8901
|
-
const upstreamFilter = (filter = {}) => {
|
|
8902
|
-
const next = { ...filter };
|
|
8903
|
-
delete next.limit;
|
|
8904
|
-
if (scope.actorId !== undefined) {
|
|
8905
|
-
delete next.actorId;
|
|
8906
|
-
}
|
|
8907
|
-
if (scope.outcome !== undefined) {
|
|
8908
|
-
delete next.outcome;
|
|
8909
|
-
}
|
|
8910
|
-
if (scope.resourceId !== undefined) {
|
|
8911
|
-
delete next.resourceId;
|
|
8912
|
-
}
|
|
8913
|
-
if (scope.resourceType !== undefined) {
|
|
8914
|
-
delete next.resourceType;
|
|
8915
|
-
}
|
|
8916
|
-
if (scope.sessionId !== undefined) {
|
|
8917
|
-
delete next.sessionId;
|
|
8918
|
-
}
|
|
8919
|
-
if (scope.traceId !== undefined) {
|
|
8920
|
-
delete next.traceId;
|
|
8921
|
-
}
|
|
8922
|
-
if (scope.type !== undefined) {
|
|
8923
|
-
delete next.type;
|
|
8924
|
-
}
|
|
8925
|
-
return next;
|
|
8926
|
-
};
|
|
8927
|
-
const scopedFilter = (filter = {}) => ({
|
|
8928
|
-
...filter,
|
|
8929
|
-
...scope
|
|
8930
|
-
});
|
|
8931
|
-
return {
|
|
8932
|
-
append: (event) => store.append(event),
|
|
8933
|
-
get: (id) => store.get(id),
|
|
8934
|
-
list: async (filter) => filterVoiceAuditEvents(await store.list(upstreamFilter(filter)), scopedFilter(filter))
|
|
8935
|
-
};
|
|
8936
|
-
};
|
|
8937
|
-
var createVoiceMemoryAuditEventStore = () => {
|
|
8938
|
-
const events = new Map;
|
|
8939
|
-
return {
|
|
8940
|
-
append: (event) => {
|
|
8941
|
-
const stored = createVoiceAuditEvent(event);
|
|
8942
|
-
events.set(stored.id, stored);
|
|
8943
|
-
return stored;
|
|
8944
|
-
},
|
|
8945
|
-
get: (id) => events.get(id),
|
|
8946
|
-
list: (filter) => filterVoiceAuditEvents([...events.values()], filter)
|
|
8947
|
-
};
|
|
8948
|
-
};
|
|
8949
8954
|
var recordVoiceAuditEvent = (store, event) => store.append(createVoiceAuditEvent(event));
|
|
8950
|
-
var
|
|
8951
|
-
action:
|
|
8955
|
+
var recordVoiceHandoffAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
8956
|
+
action: "handoff",
|
|
8952
8957
|
actor: input.actor,
|
|
8953
8958
|
metadata: input.metadata,
|
|
8954
8959
|
outcome: input.outcome,
|
|
8955
8960
|
payload: {
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
model: input.model,
|
|
8961
|
-
provider: input.provider
|
|
8961
|
+
fromAgentId: input.fromAgentId,
|
|
8962
|
+
reason: input.reason,
|
|
8963
|
+
target: input.target,
|
|
8964
|
+
toAgentId: input.toAgentId
|
|
8962
8965
|
},
|
|
8963
8966
|
resource: {
|
|
8964
|
-
id: input.
|
|
8965
|
-
type: "
|
|
8967
|
+
id: input.toAgentId ?? input.target,
|
|
8968
|
+
type: "handoff"
|
|
8966
8969
|
},
|
|
8967
8970
|
sessionId: input.sessionId,
|
|
8968
8971
|
traceId: input.traceId,
|
|
8969
|
-
type: "
|
|
8972
|
+
type: "handoff"
|
|
8970
8973
|
});
|
|
8971
|
-
var
|
|
8972
|
-
action:
|
|
8974
|
+
var recordVoiceOperatorAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
8975
|
+
action: input.action,
|
|
8973
8976
|
actor: input.actor,
|
|
8974
8977
|
metadata: input.metadata,
|
|
8975
|
-
outcome: input.outcome,
|
|
8976
|
-
payload:
|
|
8977
|
-
|
|
8978
|
-
error: input.error,
|
|
8979
|
-
toolCallId: input.toolCallId,
|
|
8980
|
-
toolName: input.toolName
|
|
8981
|
-
},
|
|
8982
|
-
resource: {
|
|
8983
|
-
id: input.toolName,
|
|
8984
|
-
type: "tool"
|
|
8985
|
-
},
|
|
8978
|
+
outcome: input.outcome ?? "success",
|
|
8979
|
+
payload: input.payload,
|
|
8980
|
+
resource: input.resource,
|
|
8986
8981
|
sessionId: input.sessionId,
|
|
8987
8982
|
traceId: input.traceId,
|
|
8988
|
-
type: "
|
|
8983
|
+
type: "operator.action"
|
|
8989
8984
|
});
|
|
8990
|
-
var
|
|
8991
|
-
action:
|
|
8985
|
+
var recordVoiceProviderAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
8986
|
+
action: `${input.kind}.provider.call`,
|
|
8992
8987
|
actor: input.actor,
|
|
8993
8988
|
metadata: input.metadata,
|
|
8994
8989
|
outcome: input.outcome,
|
|
8995
8990
|
payload: {
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
8991
|
+
cost: input.cost,
|
|
8992
|
+
elapsedMs: input.elapsedMs,
|
|
8993
|
+
error: input.error,
|
|
8994
|
+
kind: input.kind,
|
|
8995
|
+
model: input.model,
|
|
8996
|
+
provider: input.provider
|
|
9000
8997
|
},
|
|
9001
8998
|
resource: {
|
|
9002
|
-
id: input.
|
|
9003
|
-
type: "
|
|
8999
|
+
id: input.provider,
|
|
9000
|
+
type: "provider"
|
|
9004
9001
|
},
|
|
9005
9002
|
sessionId: input.sessionId,
|
|
9006
9003
|
traceId: input.traceId,
|
|
9007
|
-
type: "
|
|
9004
|
+
type: "provider.call"
|
|
9008
9005
|
});
|
|
9009
9006
|
var recordVoiceRetentionAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
9010
9007
|
action: input.dryRun ? "retention.plan" : "retention.apply",
|
|
@@ -9024,34 +9021,27 @@ var recordVoiceRetentionAuditEvent = (input) => recordVoiceAuditEvent(input.stor
|
|
|
9024
9021
|
},
|
|
9025
9022
|
type: "retention.policy"
|
|
9026
9023
|
});
|
|
9027
|
-
var
|
|
9028
|
-
action:
|
|
9024
|
+
var recordVoiceToolAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
9025
|
+
action: "tool.call",
|
|
9029
9026
|
actor: input.actor,
|
|
9030
9027
|
metadata: input.metadata,
|
|
9031
|
-
outcome: input.outcome
|
|
9032
|
-
payload:
|
|
9033
|
-
|
|
9028
|
+
outcome: input.outcome,
|
|
9029
|
+
payload: {
|
|
9030
|
+
elapsedMs: input.elapsedMs,
|
|
9031
|
+
error: input.error,
|
|
9032
|
+
toolCallId: input.toolCallId,
|
|
9033
|
+
toolName: input.toolName
|
|
9034
|
+
},
|
|
9035
|
+
resource: {
|
|
9036
|
+
id: input.toolName,
|
|
9037
|
+
type: "tool"
|
|
9038
|
+
},
|
|
9034
9039
|
sessionId: input.sessionId,
|
|
9035
9040
|
traceId: input.traceId,
|
|
9036
|
-
type: "
|
|
9037
|
-
});
|
|
9038
|
-
var createVoiceAuditLogger = (store) => ({
|
|
9039
|
-
handoff: (input) => recordVoiceHandoffAuditEvent({ ...input, store }),
|
|
9040
|
-
operatorAction: (input) => recordVoiceOperatorAuditEvent({ ...input, store }),
|
|
9041
|
-
providerCall: (input) => recordVoiceProviderAuditEvent({ ...input, store }),
|
|
9042
|
-
record: (event) => recordVoiceAuditEvent(store, event),
|
|
9043
|
-
retention: (input) => recordVoiceRetentionAuditEvent({ ...input, store }),
|
|
9044
|
-
toolCall: (input) => recordVoiceToolAuditEvent({ ...input, store })
|
|
9041
|
+
type: "tool.call"
|
|
9045
9042
|
});
|
|
9046
9043
|
|
|
9047
9044
|
// src/core/trace.ts
|
|
9048
|
-
var createVoiceTraceEventId = (event) => [
|
|
9049
|
-
event.sessionId,
|
|
9050
|
-
event.turnId ?? "session",
|
|
9051
|
-
event.type,
|
|
9052
|
-
String(event.at ?? Date.now()),
|
|
9053
|
-
crypto.randomUUID()
|
|
9054
|
-
].map(encodeURIComponent).join(":");
|
|
9055
9045
|
var createVoiceTraceEvent = (event) => ({
|
|
9056
9046
|
...event,
|
|
9057
9047
|
at: event.at,
|
|
@@ -9062,6 +9052,13 @@ var createVoiceTraceEvent = (event) => ({
|
|
|
9062
9052
|
type: event.type
|
|
9063
9053
|
})
|
|
9064
9054
|
});
|
|
9055
|
+
var createVoiceTraceEventId = (event) => [
|
|
9056
|
+
event.sessionId,
|
|
9057
|
+
event.turnId ?? "session",
|
|
9058
|
+
event.type,
|
|
9059
|
+
String(event.at ?? Date.now()),
|
|
9060
|
+
crypto.randomUUID()
|
|
9061
|
+
].map(encodeURIComponent).join(":");
|
|
9065
9062
|
var createVoiceTraceSinkDeliveryId = (events) => {
|
|
9066
9063
|
const firstEvent = events[0];
|
|
9067
9064
|
return [
|
|
@@ -9106,9 +9103,24 @@ var matchesTraceFilter = (event, filter) => {
|
|
|
9106
9103
|
}
|
|
9107
9104
|
return true;
|
|
9108
9105
|
};
|
|
9109
|
-
var
|
|
9110
|
-
const
|
|
9111
|
-
|
|
9106
|
+
var createVoiceProofTraceStore = (options = {}) => {
|
|
9107
|
+
const proofStore = options.proofStore ?? createVoiceMemoryTraceEventStore();
|
|
9108
|
+
const scopedProofStore = options.scope ? createVoiceScopedTraceEventStore(proofStore, options.scope) : proofStore;
|
|
9109
|
+
return {
|
|
9110
|
+
append: async (event) => {
|
|
9111
|
+
const stored = await proofStore.append(event);
|
|
9112
|
+
await options.mirrorStore?.append(stored);
|
|
9113
|
+
return stored;
|
|
9114
|
+
},
|
|
9115
|
+
get: async (id) => await proofStore.get(id) ?? await options.mirrorStore?.get(id),
|
|
9116
|
+
list: (filter) => scopedProofStore.list(filter),
|
|
9117
|
+
remove: async (id) => {
|
|
9118
|
+
await Promise.all([
|
|
9119
|
+
proofStore.remove(id),
|
|
9120
|
+
options.mirrorStore?.remove(id) ?? Promise.resolve()
|
|
9121
|
+
]);
|
|
9122
|
+
}
|
|
9123
|
+
};
|
|
9112
9124
|
};
|
|
9113
9125
|
var createVoiceScopedTraceEventStore = (store, scope) => {
|
|
9114
9126
|
const upstreamFilter = (filter = {}) => {
|
|
@@ -9142,24 +9154,9 @@ var createVoiceScopedTraceEventStore = (store, scope) => {
|
|
|
9142
9154
|
remove: (id) => store.remove(id)
|
|
9143
9155
|
};
|
|
9144
9156
|
};
|
|
9145
|
-
var
|
|
9146
|
-
const
|
|
9147
|
-
|
|
9148
|
-
return {
|
|
9149
|
-
append: async (event) => {
|
|
9150
|
-
const stored = await proofStore.append(event);
|
|
9151
|
-
await options.mirrorStore?.append(stored);
|
|
9152
|
-
return stored;
|
|
9153
|
-
},
|
|
9154
|
-
get: async (id) => await proofStore.get(id) ?? await options.mirrorStore?.get(id),
|
|
9155
|
-
list: (filter) => scopedProofStore.list(filter),
|
|
9156
|
-
remove: async (id) => {
|
|
9157
|
-
await Promise.all([
|
|
9158
|
-
proofStore.remove(id),
|
|
9159
|
-
options.mirrorStore?.remove(id) ?? Promise.resolve()
|
|
9160
|
-
]);
|
|
9161
|
-
}
|
|
9162
|
-
};
|
|
9157
|
+
var filterVoiceTraceEvents = (events, filter = {}) => {
|
|
9158
|
+
const sorted = events.filter((event) => matchesTraceFilter(event, filter)).sort((left, right) => left.at - right.at || left.id.localeCompare(right.id));
|
|
9159
|
+
return typeof filter.limit === "number" && filter.limit >= 0 ? sorted.slice(0, filter.limit) : sorted;
|
|
9163
9160
|
};
|
|
9164
9161
|
var isPruneTimeMatch = (event, options) => {
|
|
9165
9162
|
if (typeof options.before === "number" && event.at >= options.before) {
|
|
@@ -9170,14 +9167,6 @@ var isPruneTimeMatch = (event, options) => {
|
|
|
9170
9167
|
}
|
|
9171
9168
|
return true;
|
|
9172
9169
|
};
|
|
9173
|
-
var selectVoiceTraceEventsForPrune = (events, options = {}) => {
|
|
9174
|
-
let candidates = filterVoiceTraceEvents(events, options.filter).filter((event) => isPruneTimeMatch(event, options));
|
|
9175
|
-
if (typeof options.keepNewest === "number" && options.keepNewest >= 0) {
|
|
9176
|
-
const newestIds = new Set([...candidates].sort((left, right) => right.at - left.at || right.id.localeCompare(left.id)).slice(0, options.keepNewest).map((event) => event.id));
|
|
9177
|
-
candidates = candidates.filter((event) => !newestIds.has(event.id));
|
|
9178
|
-
}
|
|
9179
|
-
return typeof options.limit === "number" && options.limit >= 0 ? candidates.slice(0, options.limit) : candidates;
|
|
9180
|
-
};
|
|
9181
9170
|
var pruneVoiceTraceEvents = async (options) => {
|
|
9182
9171
|
const events = await options.store.list(options.filter);
|
|
9183
9172
|
const deleted = selectVoiceTraceEventsForPrune(events, options);
|
|
@@ -9191,6 +9180,14 @@ var pruneVoiceTraceEvents = async (options) => {
|
|
|
9191
9180
|
scannedCount: events.length
|
|
9192
9181
|
};
|
|
9193
9182
|
};
|
|
9183
|
+
var selectVoiceTraceEventsForPrune = (events, options = {}) => {
|
|
9184
|
+
let candidates = filterVoiceTraceEvents(events, options.filter).filter((event) => isPruneTimeMatch(event, options));
|
|
9185
|
+
if (typeof options.keepNewest === "number" && options.keepNewest >= 0) {
|
|
9186
|
+
const newestIds = new Set([...candidates].sort((left, right) => right.at - left.at || right.id.localeCompare(left.id)).slice(0, options.keepNewest).map((event) => event.id));
|
|
9187
|
+
candidates = candidates.filter((event) => !newestIds.has(event.id));
|
|
9188
|
+
}
|
|
9189
|
+
return typeof options.limit === "number" && options.limit >= 0 ? candidates.slice(0, options.limit) : candidates;
|
|
9190
|
+
};
|
|
9194
9191
|
var sleep2 = async (delayMs) => {
|
|
9195
9192
|
if (delayMs <= 0) {
|
|
9196
9193
|
return;
|
|
@@ -9226,7 +9223,7 @@ var createVoiceTraceS3ObjectKey = (prefix, events) => {
|
|
|
9226
9223
|
return `${prefix}/${safeSessionId}/${Date.now()}-${safeEventId}.json`;
|
|
9227
9224
|
};
|
|
9228
9225
|
var resolveVoiceS3DeliveredTo = (options, key) => {
|
|
9229
|
-
const bucket = options
|
|
9226
|
+
const { bucket } = options;
|
|
9230
9227
|
return bucket ? `s3://${bucket}/${key}` : `s3://${key}`;
|
|
9231
9228
|
};
|
|
9232
9229
|
var aggregateVoiceTraceSinkDeliveryStatus = (deliveries) => {
|
|
@@ -9240,6 +9237,9 @@ var aggregateVoiceTraceSinkDeliveryStatus = (deliveries) => {
|
|
|
9240
9237
|
return "delivered";
|
|
9241
9238
|
};
|
|
9242
9239
|
var createVoiceTraceHTTPSink = (options) => ({
|
|
9240
|
+
eventTypes: options.eventTypes,
|
|
9241
|
+
id: options.id,
|
|
9242
|
+
kind: options.kind ?? "http",
|
|
9243
9243
|
deliver: async ({ events }) => {
|
|
9244
9244
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
9245
9245
|
if (typeof fetchImpl !== "function") {
|
|
@@ -9329,15 +9329,15 @@ var createVoiceTraceHTTPSink = (options) => ({
|
|
|
9329
9329
|
eventCount: events.length,
|
|
9330
9330
|
status: "failed"
|
|
9331
9331
|
};
|
|
9332
|
-
}
|
|
9333
|
-
eventTypes: options.eventTypes,
|
|
9334
|
-
id: options.id,
|
|
9335
|
-
kind: options.kind ?? "http"
|
|
9332
|
+
}
|
|
9336
9333
|
});
|
|
9337
9334
|
var createVoiceTraceS3Sink = (options) => {
|
|
9338
9335
|
const client = options.client ?? new Bun.S3Client(options);
|
|
9339
9336
|
const keyPrefix = normalizeVoiceTraceS3KeyPrefix(options.keyPrefix);
|
|
9340
9337
|
return {
|
|
9338
|
+
eventTypes: options.eventTypes,
|
|
9339
|
+
id: options.id,
|
|
9340
|
+
kind: options.kind ?? "s3",
|
|
9341
9341
|
deliver: async ({ events }) => {
|
|
9342
9342
|
const key = createVoiceTraceS3ObjectKey(keyPrefix, events);
|
|
9343
9343
|
const payload = options.body ? await options.body({ events, key }) : {
|
|
@@ -9368,43 +9368,7 @@ var createVoiceTraceS3Sink = (options) => {
|
|
|
9368
9368
|
status: "failed"
|
|
9369
9369
|
};
|
|
9370
9370
|
}
|
|
9371
|
-
},
|
|
9372
|
-
eventTypes: options.eventTypes,
|
|
9373
|
-
id: options.id,
|
|
9374
|
-
kind: options.kind ?? "s3"
|
|
9375
|
-
};
|
|
9376
|
-
};
|
|
9377
|
-
var deliverVoiceTraceEventsToSinks = async (input) => {
|
|
9378
|
-
const events = input.redact ? redactVoiceTraceEvents(input.events, input.redact) : input.events;
|
|
9379
|
-
const sinkDeliveries = {};
|
|
9380
|
-
for (const sink of input.sinks) {
|
|
9381
|
-
const sinkEvents = sink.eventTypes?.length ? events.filter((event) => sink.eventTypes?.includes(event.type)) : events;
|
|
9382
|
-
if (sinkEvents.length === 0) {
|
|
9383
|
-
sinkDeliveries[sink.id] = {
|
|
9384
|
-
attempts: 0,
|
|
9385
|
-
eventCount: 0,
|
|
9386
|
-
status: "skipped"
|
|
9387
|
-
};
|
|
9388
|
-
continue;
|
|
9389
|
-
}
|
|
9390
|
-
try {
|
|
9391
|
-
sinkDeliveries[sink.id] = await sink.deliver({
|
|
9392
|
-
events: sinkEvents
|
|
9393
|
-
});
|
|
9394
|
-
} catch (error) {
|
|
9395
|
-
sinkDeliveries[sink.id] = {
|
|
9396
|
-
attempts: 1,
|
|
9397
|
-
error: error instanceof Error ? error.message : String(error),
|
|
9398
|
-
eventCount: sinkEvents.length,
|
|
9399
|
-
status: "failed"
|
|
9400
|
-
};
|
|
9401
9371
|
}
|
|
9402
|
-
}
|
|
9403
|
-
return {
|
|
9404
|
-
deliveredAt: Date.now(),
|
|
9405
|
-
eventCount: events.length,
|
|
9406
|
-
sinkDeliveries,
|
|
9407
|
-
status: aggregateVoiceTraceSinkDeliveryStatus(sinkDeliveries)
|
|
9408
9372
|
};
|
|
9409
9373
|
};
|
|
9410
9374
|
var createVoiceTraceSinkStore = (options) => {
|
|
@@ -9441,45 +9405,53 @@ var createVoiceTraceSinkStore = (options) => {
|
|
|
9441
9405
|
remove: (id) => options.store.remove(id)
|
|
9442
9406
|
};
|
|
9443
9407
|
};
|
|
9444
|
-
var
|
|
9445
|
-
|
|
9446
|
-
const
|
|
9447
|
-
const
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
append: async (event) => {
|
|
9455
|
-
const profile = await resolveProfile(event);
|
|
9456
|
-
if (!profile) {
|
|
9457
|
-
return options.store.append(event);
|
|
9458
|
-
}
|
|
9459
|
-
const metadata = {
|
|
9460
|
-
...event.metadata ?? {},
|
|
9461
|
-
benchmarkProfileId: profile.id,
|
|
9462
|
-
profileDescription: event.metadata?.profileDescription ?? profile.description,
|
|
9463
|
-
profileId: profile.id,
|
|
9464
|
-
profileLabel: event.metadata?.profileLabel ?? profile.label
|
|
9408
|
+
var deliverVoiceTraceEventsToSinks = async (input) => {
|
|
9409
|
+
const events = input.redact ? redactVoiceTraceEvents(input.events, input.redact) : input.events;
|
|
9410
|
+
const sinkDeliveries = {};
|
|
9411
|
+
for (const sink of input.sinks) {
|
|
9412
|
+
const sinkEvents = sink.eventTypes?.length ? events.filter((event) => sink.eventTypes?.includes(event.type)) : events;
|
|
9413
|
+
if (sinkEvents.length === 0) {
|
|
9414
|
+
sinkDeliveries[sink.id] = {
|
|
9415
|
+
attempts: 0,
|
|
9416
|
+
eventCount: 0,
|
|
9417
|
+
status: "skipped"
|
|
9465
9418
|
};
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
profileLabel: event.payload.profileLabel ?? profile.label
|
|
9472
|
-
} : event.payload;
|
|
9473
|
-
return options.store.append({
|
|
9474
|
-
...event,
|
|
9475
|
-
metadata,
|
|
9476
|
-
payload
|
|
9419
|
+
continue;
|
|
9420
|
+
}
|
|
9421
|
+
try {
|
|
9422
|
+
sinkDeliveries[sink.id] = await sink.deliver({
|
|
9423
|
+
events: sinkEvents
|
|
9477
9424
|
});
|
|
9478
|
-
}
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9425
|
+
} catch (error) {
|
|
9426
|
+
sinkDeliveries[sink.id] = {
|
|
9427
|
+
attempts: 1,
|
|
9428
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9429
|
+
eventCount: sinkEvents.length,
|
|
9430
|
+
status: "failed"
|
|
9431
|
+
};
|
|
9432
|
+
}
|
|
9433
|
+
}
|
|
9434
|
+
return {
|
|
9435
|
+
deliveredAt: Date.now(),
|
|
9436
|
+
eventCount: events.length,
|
|
9437
|
+
sinkDeliveries,
|
|
9438
|
+
status: aggregateVoiceTraceSinkDeliveryStatus(sinkDeliveries)
|
|
9439
|
+
};
|
|
9440
|
+
};
|
|
9441
|
+
var normalizeVoiceProfileTraceTaggerProfile = (profile) => typeof profile === "string" ? { id: profile } : profile?.id ? profile : undefined;
|
|
9442
|
+
var createVoiceMemoryTraceEventStore = () => {
|
|
9443
|
+
const events = new Map;
|
|
9444
|
+
const append = async (event) => {
|
|
9445
|
+
const stored = createVoiceTraceEvent(event);
|
|
9446
|
+
events.set(stored.id, stored);
|
|
9447
|
+
return stored;
|
|
9448
|
+
};
|
|
9449
|
+
const get = async (id) => events.get(id);
|
|
9450
|
+
const list = async (filter) => filterVoiceTraceEvents([...events.values()], filter);
|
|
9451
|
+
const remove = async (id) => {
|
|
9452
|
+
events.delete(id);
|
|
9482
9453
|
};
|
|
9454
|
+
return { append, get, list, remove };
|
|
9483
9455
|
};
|
|
9484
9456
|
var createVoiceMemoryTraceSinkDeliveryStore = () => {
|
|
9485
9457
|
const deliveries = new Map;
|
|
@@ -9494,19 +9466,44 @@ var createVoiceMemoryTraceSinkDeliveryStore = () => {
|
|
|
9494
9466
|
}
|
|
9495
9467
|
};
|
|
9496
9468
|
};
|
|
9497
|
-
var
|
|
9498
|
-
const
|
|
9499
|
-
const
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9469
|
+
var createVoiceProfileTraceTagger = (options) => {
|
|
9470
|
+
const profiles = new Map((options.profiles ?? []).map((profile) => [profile.id, profile]));
|
|
9471
|
+
const defaultProfile = normalizeVoiceProfileTraceTaggerProfile(options.defaultProfile);
|
|
9472
|
+
const resolveProfile = async (event) => {
|
|
9473
|
+
const resolved = normalizeVoiceProfileTraceTaggerProfile(await options.resolveProfile?.(event));
|
|
9474
|
+
const profile = resolved ?? defaultProfile;
|
|
9475
|
+
return profile ? profiles.get(profile.id) ?? profile : undefined;
|
|
9503
9476
|
};
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9477
|
+
return {
|
|
9478
|
+
append: async (event) => {
|
|
9479
|
+
const profile = await resolveProfile(event);
|
|
9480
|
+
if (!profile) {
|
|
9481
|
+
return options.store.append(event);
|
|
9482
|
+
}
|
|
9483
|
+
const metadata = {
|
|
9484
|
+
...event.metadata ?? {},
|
|
9485
|
+
benchmarkProfileId: profile.id,
|
|
9486
|
+
profileDescription: event.metadata?.profileDescription ?? profile.description,
|
|
9487
|
+
profileId: profile.id,
|
|
9488
|
+
profileLabel: event.metadata?.profileLabel ?? profile.label
|
|
9489
|
+
};
|
|
9490
|
+
const payload = event.payload && typeof event.payload === "object" ? {
|
|
9491
|
+
...event.payload,
|
|
9492
|
+
benchmarkProfileId: event.payload.benchmarkProfileId ?? profile.id,
|
|
9493
|
+
profileDescription: event.payload.profileDescription ?? profile.description,
|
|
9494
|
+
profileId: event.payload.profileId ?? profile.id,
|
|
9495
|
+
profileLabel: event.payload.profileLabel ?? profile.label
|
|
9496
|
+
} : event.payload;
|
|
9497
|
+
return options.store.append({
|
|
9498
|
+
...event,
|
|
9499
|
+
metadata,
|
|
9500
|
+
payload
|
|
9501
|
+
});
|
|
9502
|
+
},
|
|
9503
|
+
get: (id) => options.store.get(id),
|
|
9504
|
+
list: (filter) => options.store.list(filter),
|
|
9505
|
+
remove: (id) => options.store.remove(id)
|
|
9508
9506
|
};
|
|
9509
|
-
return { append, get, list, remove };
|
|
9510
9507
|
};
|
|
9511
9508
|
var exportVoiceTrace = async (input) => {
|
|
9512
9509
|
const events = await input.store.list(input.filter);
|
|
@@ -9618,53 +9615,6 @@ var redactTraceValue = (value, options, path) => {
|
|
|
9618
9615
|
}
|
|
9619
9616
|
return value;
|
|
9620
9617
|
};
|
|
9621
|
-
var redactVoiceTraceEvent = (event, options = {}) => {
|
|
9622
|
-
const resolved = resolveVoiceTraceRedactionOptions(options);
|
|
9623
|
-
return {
|
|
9624
|
-
...event,
|
|
9625
|
-
metadata: redactTraceValue(event.metadata, resolved, [
|
|
9626
|
-
"metadata"
|
|
9627
|
-
]),
|
|
9628
|
-
payload: redactTraceValue(event.payload, resolved, [
|
|
9629
|
-
"payload"
|
|
9630
|
-
])
|
|
9631
|
-
};
|
|
9632
|
-
};
|
|
9633
|
-
var redactVoiceTraceEvents = (events, options = {}) => events.map((event) => redactVoiceTraceEvent(event, options));
|
|
9634
|
-
var summarizeVoiceTrace = (events) => {
|
|
9635
|
-
const sorted = filterVoiceTraceEvents(events);
|
|
9636
|
-
const firstEvent = sorted[0];
|
|
9637
|
-
const lastEvent = sorted.at(-1);
|
|
9638
|
-
const lifecycleEvents = sorted.filter((event) => event.type === "call.lifecycle");
|
|
9639
|
-
const startEvent = lifecycleEvents.find((event) => event.payload.type === "start");
|
|
9640
|
-
const endEvent = lifecycleEvents.toReversed().find((event) => event.payload.type === "end");
|
|
9641
|
-
const costEvents = sorted.filter((event) => event.type === "turn.cost");
|
|
9642
|
-
const toolEvents = sorted.filter((event) => event.type === "agent.tool");
|
|
9643
|
-
const startedAt = startEvent?.at ?? firstEvent?.at;
|
|
9644
|
-
const endedAt = endEvent?.at ?? lastEvent?.at;
|
|
9645
|
-
const failed = sorted.some((event) => event.type === "session.error") || endEvent?.payload.disposition === "failed";
|
|
9646
|
-
return {
|
|
9647
|
-
assistantReplyCount: sorted.filter((event) => event.type === "turn.assistant").length,
|
|
9648
|
-
callDurationMs: startedAt !== undefined && endedAt !== undefined ? Math.max(0, endedAt - startedAt) : undefined,
|
|
9649
|
-
cost: {
|
|
9650
|
-
estimatedRelativeCostUnits: costEvents.reduce((total, event) => total + toNumber(event.payload.estimatedRelativeCostUnits), 0),
|
|
9651
|
-
totalBillableAudioMs: costEvents.reduce((total, event) => total + toNumber(event.payload.totalBillableAudioMs), 0)
|
|
9652
|
-
},
|
|
9653
|
-
endedAt,
|
|
9654
|
-
errorCount: sorted.filter((event) => event.type === "session.error").length,
|
|
9655
|
-
eventCount: sorted.length,
|
|
9656
|
-
failed,
|
|
9657
|
-
handoffCount: sorted.filter((event) => event.type === "agent.handoff").length,
|
|
9658
|
-
modelCallCount: sorted.filter((event) => event.type === "agent.model").length,
|
|
9659
|
-
sessionId: firstEvent?.sessionId,
|
|
9660
|
-
startedAt,
|
|
9661
|
-
toolCallCount: toolEvents.length,
|
|
9662
|
-
toolErrorCount: toolEvents.filter((event) => event.payload.status === "error").length,
|
|
9663
|
-
traceId: firstEvent?.traceId,
|
|
9664
|
-
transcriptCount: sorted.filter((event) => event.type === "turn.transcript").length,
|
|
9665
|
-
turnCount: sorted.filter((event) => event.type === "turn.committed").length
|
|
9666
|
-
};
|
|
9667
|
-
};
|
|
9668
9618
|
var evaluateVoiceTrace = (events, options = {}) => {
|
|
9669
9619
|
const summary = summarizeVoiceTrace(events);
|
|
9670
9620
|
const issues = [];
|
|
@@ -9734,6 +9684,53 @@ var evaluateVoiceTrace = (events, options = {}) => {
|
|
|
9734
9684
|
summary
|
|
9735
9685
|
};
|
|
9736
9686
|
};
|
|
9687
|
+
var redactVoiceTraceEvent = (event, options = {}) => {
|
|
9688
|
+
const resolved = resolveVoiceTraceRedactionOptions(options);
|
|
9689
|
+
return {
|
|
9690
|
+
...event,
|
|
9691
|
+
metadata: redactTraceValue(event.metadata, resolved, [
|
|
9692
|
+
"metadata"
|
|
9693
|
+
]),
|
|
9694
|
+
payload: redactTraceValue(event.payload, resolved, [
|
|
9695
|
+
"payload"
|
|
9696
|
+
])
|
|
9697
|
+
};
|
|
9698
|
+
};
|
|
9699
|
+
var redactVoiceTraceEvents = (events, options = {}) => events.map((event) => redactVoiceTraceEvent(event, options));
|
|
9700
|
+
var summarizeVoiceTrace = (events) => {
|
|
9701
|
+
const sorted = filterVoiceTraceEvents(events);
|
|
9702
|
+
const firstEvent = sorted[0];
|
|
9703
|
+
const lastEvent = sorted.at(-1);
|
|
9704
|
+
const lifecycleEvents = sorted.filter((event) => event.type === "call.lifecycle");
|
|
9705
|
+
const startEvent = lifecycleEvents.find((event) => event.payload.type === "start");
|
|
9706
|
+
const endEvent = lifecycleEvents.toReversed().find((event) => event.payload.type === "end");
|
|
9707
|
+
const costEvents = sorted.filter((event) => event.type === "turn.cost");
|
|
9708
|
+
const toolEvents = sorted.filter((event) => event.type === "agent.tool");
|
|
9709
|
+
const startedAt = startEvent?.at ?? firstEvent?.at;
|
|
9710
|
+
const endedAt = endEvent?.at ?? lastEvent?.at;
|
|
9711
|
+
const failed = sorted.some((event) => event.type === "session.error") || endEvent?.payload.disposition === "failed";
|
|
9712
|
+
return {
|
|
9713
|
+
assistantReplyCount: sorted.filter((event) => event.type === "turn.assistant").length,
|
|
9714
|
+
callDurationMs: startedAt !== undefined && endedAt !== undefined ? Math.max(0, endedAt - startedAt) : undefined,
|
|
9715
|
+
cost: {
|
|
9716
|
+
estimatedRelativeCostUnits: costEvents.reduce((total, event) => total + toNumber(event.payload.estimatedRelativeCostUnits), 0),
|
|
9717
|
+
totalBillableAudioMs: costEvents.reduce((total, event) => total + toNumber(event.payload.totalBillableAudioMs), 0)
|
|
9718
|
+
},
|
|
9719
|
+
endedAt,
|
|
9720
|
+
errorCount: sorted.filter((event) => event.type === "session.error").length,
|
|
9721
|
+
eventCount: sorted.length,
|
|
9722
|
+
failed,
|
|
9723
|
+
handoffCount: sorted.filter((event) => event.type === "agent.handoff").length,
|
|
9724
|
+
modelCallCount: sorted.filter((event) => event.type === "agent.model").length,
|
|
9725
|
+
sessionId: firstEvent?.sessionId,
|
|
9726
|
+
startedAt,
|
|
9727
|
+
toolCallCount: toolEvents.length,
|
|
9728
|
+
toolErrorCount: toolEvents.filter((event) => event.payload.status === "error").length,
|
|
9729
|
+
traceId: firstEvent?.traceId,
|
|
9730
|
+
transcriptCount: sorted.filter((event) => event.type === "turn.transcript").length,
|
|
9731
|
+
turnCount: sorted.filter((event) => event.type === "turn.committed").length
|
|
9732
|
+
};
|
|
9733
|
+
};
|
|
9737
9734
|
var renderTraceEventMarkdown = (event, startedAt) => {
|
|
9738
9735
|
const offset = startedAt === undefined ? `${event.at}` : `+${Math.max(0, event.at - startedAt)}ms`;
|
|
9739
9736
|
const label = `- ${offset} [${event.type}]`;
|
|
@@ -9758,40 +9755,12 @@ var renderTraceEventMarkdown = (event, startedAt) => {
|
|
|
9758
9755
|
return `${label} ${formatTraceValue(event.payload)}`;
|
|
9759
9756
|
}
|
|
9760
9757
|
};
|
|
9761
|
-
var
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
"",
|
|
9768
|
-
`Pass: ${evaluation.pass ? "yes" : "no"}`,
|
|
9769
|
-
`Session: ${summary.sessionId ?? "unknown"}`,
|
|
9770
|
-
`Events: ${summary.eventCount}`,
|
|
9771
|
-
`Turns: ${summary.turnCount}`,
|
|
9772
|
-
`Transcripts: ${summary.transcriptCount}`,
|
|
9773
|
-
`Assistant replies: ${summary.assistantReplyCount}`,
|
|
9774
|
-
`Model calls: ${summary.modelCallCount}`,
|
|
9775
|
-
`Tool calls: ${summary.toolCallCount}`,
|
|
9776
|
-
`Handoffs: ${summary.handoffCount}`,
|
|
9777
|
-
`Errors: ${summary.errorCount}`,
|
|
9778
|
-
`Estimated cost units: ${summary.cost.estimatedRelativeCostUnits}`,
|
|
9779
|
-
""
|
|
9780
|
-
];
|
|
9781
|
-
if (evaluation.issues.length > 0) {
|
|
9782
|
-
lines.push("## Issues", "");
|
|
9783
|
-
for (const issue of evaluation.issues) {
|
|
9784
|
-
lines.push(`- [${issue.severity}] ${issue.code}: ${issue.message}`);
|
|
9785
|
-
}
|
|
9786
|
-
lines.push("");
|
|
9787
|
-
}
|
|
9788
|
-
lines.push("## Timeline", "");
|
|
9789
|
-
for (const event of sorted) {
|
|
9790
|
-
lines.push(renderTraceEventMarkdown(event, summary.startedAt));
|
|
9791
|
-
}
|
|
9792
|
-
return lines.join(`
|
|
9793
|
-
`);
|
|
9794
|
-
};
|
|
9758
|
+
var buildVoiceTraceReplay = (events, options = {}) => ({
|
|
9759
|
+
evaluation: evaluateVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events, options.evaluation),
|
|
9760
|
+
html: renderVoiceTraceHTML(events, options),
|
|
9761
|
+
markdown: renderVoiceTraceMarkdown(events, options),
|
|
9762
|
+
summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
|
|
9763
|
+
});
|
|
9795
9764
|
var renderVoiceTraceHTML = (events, options = {}) => {
|
|
9796
9765
|
const markdown = renderVoiceTraceMarkdown(events, options);
|
|
9797
9766
|
const renderEvents = options.redact ? redactVoiceTraceEvents(events, options.redact) : events;
|
|
@@ -9848,22 +9817,50 @@ var renderVoiceTraceHTML = (events, options = {}) => {
|
|
|
9848
9817
|
].join(`
|
|
9849
9818
|
`);
|
|
9850
9819
|
};
|
|
9851
|
-
var
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
})
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9820
|
+
var renderVoiceTraceMarkdown = (events, options = {}) => {
|
|
9821
|
+
const sorted = filterVoiceTraceEvents(options.redact ? redactVoiceTraceEvents(events, options.redact) : events);
|
|
9822
|
+
const summary = summarizeVoiceTrace(sorted);
|
|
9823
|
+
const evaluation = evaluateVoiceTrace(sorted, options.evaluation);
|
|
9824
|
+
const lines = [
|
|
9825
|
+
`# ${options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim()}`,
|
|
9826
|
+
"",
|
|
9827
|
+
`Pass: ${evaluation.pass ? "yes" : "no"}`,
|
|
9828
|
+
`Session: ${summary.sessionId ?? "unknown"}`,
|
|
9829
|
+
`Events: ${summary.eventCount}`,
|
|
9830
|
+
`Turns: ${summary.turnCount}`,
|
|
9831
|
+
`Transcripts: ${summary.transcriptCount}`,
|
|
9832
|
+
`Assistant replies: ${summary.assistantReplyCount}`,
|
|
9833
|
+
`Model calls: ${summary.modelCallCount}`,
|
|
9834
|
+
`Tool calls: ${summary.toolCallCount}`,
|
|
9835
|
+
`Handoffs: ${summary.handoffCount}`,
|
|
9836
|
+
`Errors: ${summary.errorCount}`,
|
|
9837
|
+
`Estimated cost units: ${summary.cost.estimatedRelativeCostUnits}`,
|
|
9838
|
+
""
|
|
9839
|
+
];
|
|
9840
|
+
if (evaluation.issues.length > 0) {
|
|
9841
|
+
lines.push("## Issues", "");
|
|
9842
|
+
for (const issue of evaluation.issues) {
|
|
9843
|
+
lines.push(`- [${issue.severity}] ${issue.code}: ${issue.message}`);
|
|
9844
|
+
}
|
|
9845
|
+
lines.push("");
|
|
9846
|
+
}
|
|
9847
|
+
lines.push("## Timeline", "");
|
|
9848
|
+
for (const event of sorted) {
|
|
9849
|
+
lines.push(renderTraceEventMarkdown(event, summary.startedAt));
|
|
9850
|
+
}
|
|
9851
|
+
return lines.join(`
|
|
9852
|
+
`);
|
|
9853
|
+
};
|
|
9854
|
+
|
|
9855
|
+
// src/core/auditRoutes.ts
|
|
9856
|
+
import { Elysia } from "elysia";
|
|
9857
|
+
var getString = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
9858
|
+
var getNumber = (value) => {
|
|
9859
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
9860
|
+
return value;
|
|
9861
|
+
}
|
|
9862
|
+
if (typeof value === "string" && value.trim()) {
|
|
9863
|
+
const parsed = Number(value);
|
|
9867
9864
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
9868
9865
|
}
|
|
9869
9866
|
return;
|
|
@@ -9903,42 +9900,6 @@ var increment = (counts, key) => {
|
|
|
9903
9900
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
9904
9901
|
};
|
|
9905
9902
|
var sortedCounts = (counts) => [...counts.entries()].sort(([leftKey, leftCount], [rightKey, rightCount]) => rightCount - leftCount || leftKey.localeCompare(rightKey));
|
|
9906
|
-
var resolveVoiceAuditTrailFilter = (query = {}, base = {}) => ({
|
|
9907
|
-
...base,
|
|
9908
|
-
actorId: getString(query.actorId) ?? base.actorId,
|
|
9909
|
-
after: getNumber(query.after) ?? base.after,
|
|
9910
|
-
afterOrAt: getNumber(query.afterOrAt) ?? base.afterOrAt,
|
|
9911
|
-
before: getNumber(query.before) ?? base.before,
|
|
9912
|
-
beforeOrAt: getNumber(query.beforeOrAt) ?? base.beforeOrAt,
|
|
9913
|
-
limit: getNumber(query.limit) ?? base.limit,
|
|
9914
|
-
outcome: parseOutcome(query.outcome) ?? base.outcome,
|
|
9915
|
-
resourceId: getString(query.resourceId) ?? base.resourceId,
|
|
9916
|
-
resourceType: getString(query.resourceType) ?? base.resourceType,
|
|
9917
|
-
sessionId: getString(query.sessionId) ?? base.sessionId,
|
|
9918
|
-
traceId: getString(query.traceId) ?? base.traceId,
|
|
9919
|
-
type: parseType(query.type) ?? base.type
|
|
9920
|
-
});
|
|
9921
|
-
var summarizeVoiceAuditTrail = (events) => {
|
|
9922
|
-
const byActor = new Map;
|
|
9923
|
-
const byOutcome = new Map;
|
|
9924
|
-
const byResourceType = new Map;
|
|
9925
|
-
const byType = new Map;
|
|
9926
|
-
for (const event of events) {
|
|
9927
|
-
increment(byActor, event.actor?.id);
|
|
9928
|
-
increment(byOutcome, event.outcome);
|
|
9929
|
-
increment(byResourceType, event.resource?.type);
|
|
9930
|
-
increment(byType, event.type);
|
|
9931
|
-
}
|
|
9932
|
-
return {
|
|
9933
|
-
byActor: sortedCounts(byActor),
|
|
9934
|
-
byOutcome: sortedCounts(byOutcome),
|
|
9935
|
-
byResourceType: sortedCounts(byResourceType),
|
|
9936
|
-
byType: sortedCounts(byType),
|
|
9937
|
-
errors: events.filter((event) => event.outcome === "error").length,
|
|
9938
|
-
latestAt: events.at(-1)?.at,
|
|
9939
|
-
total: events.length
|
|
9940
|
-
};
|
|
9941
|
-
};
|
|
9942
9903
|
var buildVoiceAuditTrailReport = async (options) => {
|
|
9943
9904
|
const filter = {
|
|
9944
9905
|
...options.filter,
|
|
@@ -9952,17 +9913,6 @@ var buildVoiceAuditTrailReport = async (options) => {
|
|
|
9952
9913
|
summary: summarizeVoiceAuditTrail(events)
|
|
9953
9914
|
};
|
|
9954
9915
|
};
|
|
9955
|
-
var renderVoiceAuditTrailHTML = (report, options = {}) => {
|
|
9956
|
-
const title = options.title ?? "AbsoluteJS Voice Audit Trail";
|
|
9957
|
-
const chips = report.summary.byType.map(([type, count]) => `<span>${escapeHtml(type)} <strong>${count}</strong></span>`).join("");
|
|
9958
|
-
const rows = report.events.map((event) => {
|
|
9959
|
-
const actor = event.actor ? `${event.actor.kind}:${event.actor.id}` : "unknown";
|
|
9960
|
-
const resource = event.resource ? `${event.resource.type}${event.resource.id ? `:${event.resource.id}` : ""}` : "";
|
|
9961
|
-
const payload = event.payload ? JSON.stringify(event.payload, null, 2) : "";
|
|
9962
|
-
return `<article class="event ${escapeHtml(event.outcome ?? "unknown")}"><div><span>${escapeHtml(event.type)}</span><h2>${escapeHtml(event.action)}</h2><p>${escapeHtml(new Date(event.at).toLocaleString())}</p><p>Actor: ${escapeHtml(actor)}${resource ? ` \xB7 Resource: ${escapeHtml(resource)}` : ""}</p>${event.sessionId ? `<p>Session: ${escapeHtml(event.sessionId)}</p>` : ""}</div><strong>${escapeHtml(event.outcome ?? "recorded")}</strong>${payload ? `<pre>${escapeHtml(payload)}</pre>` : ""}</article>`;
|
|
9963
|
-
}).join("");
|
|
9964
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(title)}</title><style>body{background:#11140f;color:#f7f1df;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.18),rgba(245,158,11,.12));border:1px solid #2c3327;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#facc15;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,4.8rem);line-height:.92;margin:.2rem 0 1rem}.chips{display:flex;flex-wrap:wrap;gap:10px}.chips span{border:1px solid #46513b;border-radius:999px;padding:8px 12px}.events{display:grid;gap:14px}.event{background:#181d15;border:1px solid #2c3327;border-radius:22px;display:grid;gap:16px;grid-template-columns:1fr auto;padding:18px}.event.error{border-color:rgba(239,68,68,.75)}.event.skipped{border-color:rgba(245,158,11,.7)}.event.success{border-color:rgba(34,197,94,.55)}.event span{color:#facc15;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.event h2{margin:.2rem 0}.event p{color:#c8ccb8;margin:.2rem 0}.event strong{text-transform:uppercase}pre{background:#0c0f0a;border-radius:14px;grid-column:1/-1;overflow:auto;padding:14px;white-space:pre-wrap}@media(max-width:760px){main{padding:20px}.event{grid-template-columns:1fr}}</style></head><body><main><section class="hero"><p class="eyebrow">Self-hosted evidence</p><h1>${escapeHtml(title)}</h1><p>${report.summary.total} event(s), ${report.summary.errors} error(s). Latest ${report.summary.latestAt ? escapeHtml(new Date(report.summary.latestAt).toLocaleString()) : "never"}.</p><div class="chips">${chips}</div></section><section class="events">${rows || "<p>No audit events match this filter.</p>"}</section></main></body></html>`;
|
|
9965
|
-
};
|
|
9966
9916
|
var createVoiceAuditTrailRoutes = (options) => {
|
|
9967
9917
|
const path = options.path ?? "/api/voice-audit";
|
|
9968
9918
|
const htmlPath = options.htmlPath ?? "/audit";
|
|
@@ -10051,6 +10001,53 @@ var createVoiceAuditTrailRoutes = (options) => {
|
|
|
10051
10001
|
}
|
|
10052
10002
|
return routes;
|
|
10053
10003
|
};
|
|
10004
|
+
var renderVoiceAuditTrailHTML = (report, options = {}) => {
|
|
10005
|
+
const title = options.title ?? "AbsoluteJS Voice Audit Trail";
|
|
10006
|
+
const chips = report.summary.byType.map(([type, count]) => `<span>${escapeHtml(type)} <strong>${count}</strong></span>`).join("");
|
|
10007
|
+
const rows = report.events.map((event) => {
|
|
10008
|
+
const actor = event.actor ? `${event.actor.kind}:${event.actor.id}` : "unknown";
|
|
10009
|
+
const resource = event.resource ? `${event.resource.type}${event.resource.id ? `:${event.resource.id}` : ""}` : "";
|
|
10010
|
+
const payload = event.payload ? JSON.stringify(event.payload, null, 2) : "";
|
|
10011
|
+
return `<article class="event ${escapeHtml(event.outcome ?? "unknown")}"><div><span>${escapeHtml(event.type)}</span><h2>${escapeHtml(event.action)}</h2><p>${escapeHtml(new Date(event.at).toLocaleString())}</p><p>Actor: ${escapeHtml(actor)}${resource ? ` \xB7 Resource: ${escapeHtml(resource)}` : ""}</p>${event.sessionId ? `<p>Session: ${escapeHtml(event.sessionId)}</p>` : ""}</div><strong>${escapeHtml(event.outcome ?? "recorded")}</strong>${payload ? `<pre>${escapeHtml(payload)}</pre>` : ""}</article>`;
|
|
10012
|
+
}).join("");
|
|
10013
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(title)}</title><style>body{background:#11140f;color:#f7f1df;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.18),rgba(245,158,11,.12));border:1px solid #2c3327;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#facc15;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,4.8rem);line-height:.92;margin:.2rem 0 1rem}.chips{display:flex;flex-wrap:wrap;gap:10px}.chips span{border:1px solid #46513b;border-radius:999px;padding:8px 12px}.events{display:grid;gap:14px}.event{background:#181d15;border:1px solid #2c3327;border-radius:22px;display:grid;gap:16px;grid-template-columns:1fr auto;padding:18px}.event.error{border-color:rgba(239,68,68,.75)}.event.skipped{border-color:rgba(245,158,11,.7)}.event.success{border-color:rgba(34,197,94,.55)}.event span{color:#facc15;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.event h2{margin:.2rem 0}.event p{color:#c8ccb8;margin:.2rem 0}.event strong{text-transform:uppercase}pre{background:#0c0f0a;border-radius:14px;grid-column:1/-1;overflow:auto;padding:14px;white-space:pre-wrap}@media(max-width:760px){main{padding:20px}.event{grid-template-columns:1fr}}</style></head><body><main><section class="hero"><p class="eyebrow">Self-hosted evidence</p><h1>${escapeHtml(title)}</h1><p>${report.summary.total} event(s), ${report.summary.errors} error(s). Latest ${report.summary.latestAt ? escapeHtml(new Date(report.summary.latestAt).toLocaleString()) : "never"}.</p><div class="chips">${chips}</div></section><section class="events">${rows || "<p>No audit events match this filter.</p>"}</section></main></body></html>`;
|
|
10014
|
+
};
|
|
10015
|
+
var resolveVoiceAuditTrailFilter = (query = {}, base = {}) => ({
|
|
10016
|
+
...base,
|
|
10017
|
+
actorId: getString(query.actorId) ?? base.actorId,
|
|
10018
|
+
after: getNumber(query.after) ?? base.after,
|
|
10019
|
+
afterOrAt: getNumber(query.afterOrAt) ?? base.afterOrAt,
|
|
10020
|
+
before: getNumber(query.before) ?? base.before,
|
|
10021
|
+
beforeOrAt: getNumber(query.beforeOrAt) ?? base.beforeOrAt,
|
|
10022
|
+
limit: getNumber(query.limit) ?? base.limit,
|
|
10023
|
+
outcome: parseOutcome(query.outcome) ?? base.outcome,
|
|
10024
|
+
resourceId: getString(query.resourceId) ?? base.resourceId,
|
|
10025
|
+
resourceType: getString(query.resourceType) ?? base.resourceType,
|
|
10026
|
+
sessionId: getString(query.sessionId) ?? base.sessionId,
|
|
10027
|
+
traceId: getString(query.traceId) ?? base.traceId,
|
|
10028
|
+
type: parseType(query.type) ?? base.type
|
|
10029
|
+
});
|
|
10030
|
+
var summarizeVoiceAuditTrail = (events) => {
|
|
10031
|
+
const byActor = new Map;
|
|
10032
|
+
const byOutcome = new Map;
|
|
10033
|
+
const byResourceType = new Map;
|
|
10034
|
+
const byType = new Map;
|
|
10035
|
+
for (const event of events) {
|
|
10036
|
+
increment(byActor, event.actor?.id);
|
|
10037
|
+
increment(byOutcome, event.outcome);
|
|
10038
|
+
increment(byResourceType, event.resource?.type);
|
|
10039
|
+
increment(byType, event.type);
|
|
10040
|
+
}
|
|
10041
|
+
return {
|
|
10042
|
+
byActor: sortedCounts(byActor),
|
|
10043
|
+
byOutcome: sortedCounts(byOutcome),
|
|
10044
|
+
byResourceType: sortedCounts(byResourceType),
|
|
10045
|
+
byType: sortedCounts(byType),
|
|
10046
|
+
errors: events.filter((event) => event.outcome === "error").length,
|
|
10047
|
+
latestAt: events.at(-1)?.at,
|
|
10048
|
+
total: events.length
|
|
10049
|
+
};
|
|
10050
|
+
};
|
|
10054
10051
|
|
|
10055
10052
|
// src/core/auditExport.ts
|
|
10056
10053
|
var normalizeRedactionKey2 = (key) => key.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
@@ -10090,6 +10087,17 @@ var redactAuditValue = (value, config, options, path) => {
|
|
|
10090
10087
|
}
|
|
10091
10088
|
return value;
|
|
10092
10089
|
};
|
|
10090
|
+
var exportVoiceAuditTrail = async (input) => {
|
|
10091
|
+
const events = await input.store.list(input.filter);
|
|
10092
|
+
const exportedEvents = input.redact ? redactVoiceAuditEvents(events, input.redact) : events;
|
|
10093
|
+
return {
|
|
10094
|
+
events: exportedEvents,
|
|
10095
|
+
exportedAt: Date.now(),
|
|
10096
|
+
filter: input.filter,
|
|
10097
|
+
redacted: Boolean(input.redact),
|
|
10098
|
+
summary: summarizeVoiceAuditTrail(exportedEvents)
|
|
10099
|
+
};
|
|
10100
|
+
};
|
|
10093
10101
|
var redactVoiceAuditEvent = (event, options = {}) => {
|
|
10094
10102
|
const resolved = resolveVoiceTraceRedactionOptions(options);
|
|
10095
10103
|
return {
|
|
@@ -10109,17 +10117,6 @@ var redactVoiceAuditEvent = (event, options = {}) => {
|
|
|
10109
10117
|
};
|
|
10110
10118
|
};
|
|
10111
10119
|
var redactVoiceAuditEvents = (events, options = {}) => events.map((event) => redactVoiceAuditEvent(event, options));
|
|
10112
|
-
var exportVoiceAuditTrail = async (input) => {
|
|
10113
|
-
const events = await input.store.list(input.filter);
|
|
10114
|
-
const exportedEvents = input.redact ? redactVoiceAuditEvents(events, input.redact) : events;
|
|
10115
|
-
return {
|
|
10116
|
-
events: exportedEvents,
|
|
10117
|
-
exportedAt: Date.now(),
|
|
10118
|
-
filter: input.filter,
|
|
10119
|
-
redacted: Boolean(input.redact),
|
|
10120
|
-
summary: summarizeVoiceAuditTrail(exportedEvents)
|
|
10121
|
-
};
|
|
10122
|
-
};
|
|
10123
10120
|
var formatAuditValue = (value) => {
|
|
10124
10121
|
if (value === undefined || value === null || value === "") {
|
|
10125
10122
|
return "";
|
|
@@ -10144,6 +10141,23 @@ var renderAuditEventMarkdown = (event) => {
|
|
|
10144
10141
|
].filter(Boolean).join(" ");
|
|
10145
10142
|
return `- ${new Date(event.at).toISOString()} [${event.type}] ${event.action} ${event.outcome ?? "recorded"} ${detail} ${formatAuditValue(event.payload)}`.trim();
|
|
10146
10143
|
};
|
|
10144
|
+
var buildVoiceAuditExport = (events, options = {}) => {
|
|
10145
|
+
const exportEvents = options.redact ? redactVoiceAuditEvents(events, options.redact) : events;
|
|
10146
|
+
return {
|
|
10147
|
+
events: exportEvents,
|
|
10148
|
+
html: renderVoiceAuditHTML(events, options),
|
|
10149
|
+
markdown: renderVoiceAuditMarkdown(events, options),
|
|
10150
|
+
summary: summarizeVoiceAuditTrail(exportEvents)
|
|
10151
|
+
};
|
|
10152
|
+
};
|
|
10153
|
+
var renderVoiceAuditHTML = (events, options = {}) => {
|
|
10154
|
+
const title = options.title ?? "Voice Audit Trail";
|
|
10155
|
+
const markdown = renderVoiceAuditMarkdown(events, options);
|
|
10156
|
+
const renderEvents = options.redact ? redactVoiceAuditEvents(events, options.redact) : events;
|
|
10157
|
+
const summary = summarizeVoiceAuditTrail(renderEvents);
|
|
10158
|
+
const rows = renderEvents.map((event) => `<tr><td>${escapeHtml(new Date(event.at).toISOString())}</td><td>${escapeHtml(event.type)}</td><td>${escapeHtml(event.action)}</td><td>${escapeHtml(event.outcome ?? "")}</td><td><code>${escapeHtml(JSON.stringify(event.payload ?? {}))}</code></td></tr>`).join("");
|
|
10159
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}.summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:.75rem;margin:1rem 0}.card{background:white;border:1px solid #ded9cc;border-radius:12px;padding:1rem}table{border-collapse:collapse;width:100%;background:white;border:1px solid #ded9cc}th,td{border-bottom:1px solid #eee8dc;padding:.65rem;text-align:left;vertical-align:top}code{white-space:pre-wrap;word-break:break-word}pre{background:#181713;color:#f8f7f2;padding:1rem;border-radius:12px;overflow:auto}</style></head><body><main><h1>${escapeHtml(title)}</h1><section class="summary"><div class="card"><strong>Events</strong><br>${summary.total}</div><div class="card"><strong>Errors</strong><br>${summary.errors}</div><div class="card"><strong>Latest</strong><br>${summary.latestAt ? escapeHtml(new Date(summary.latestAt).toLocaleString()) : "never"}</div></section><table><thead><tr><th>At</th><th>Type</th><th>Action</th><th>Outcome</th><th>Payload</th></tr></thead><tbody>${rows}</tbody></table><h2>Markdown Export</h2><pre>${escapeHtml(markdown)}</pre></main></body></html>`;
|
|
10160
|
+
};
|
|
10147
10161
|
var renderVoiceAuditMarkdown = (events, options = {}) => {
|
|
10148
10162
|
const renderEvents = options.redact ? redactVoiceAuditEvents(events, options.redact) : events;
|
|
10149
10163
|
const summary = summarizeVoiceAuditTrail(renderEvents);
|
|
@@ -10165,23 +10179,6 @@ var renderVoiceAuditMarkdown = (events, options = {}) => {
|
|
|
10165
10179
|
return lines.join(`
|
|
10166
10180
|
`);
|
|
10167
10181
|
};
|
|
10168
|
-
var renderVoiceAuditHTML = (events, options = {}) => {
|
|
10169
|
-
const title = options.title ?? "Voice Audit Trail";
|
|
10170
|
-
const markdown = renderVoiceAuditMarkdown(events, options);
|
|
10171
|
-
const renderEvents = options.redact ? redactVoiceAuditEvents(events, options.redact) : events;
|
|
10172
|
-
const summary = summarizeVoiceAuditTrail(renderEvents);
|
|
10173
|
-
const rows = renderEvents.map((event) => `<tr><td>${escapeHtml(new Date(event.at).toISOString())}</td><td>${escapeHtml(event.type)}</td><td>${escapeHtml(event.action)}</td><td>${escapeHtml(event.outcome ?? "")}</td><td><code>${escapeHtml(JSON.stringify(event.payload ?? {}))}</code></td></tr>`).join("");
|
|
10174
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}.summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:.75rem;margin:1rem 0}.card{background:white;border:1px solid #ded9cc;border-radius:12px;padding:1rem}table{border-collapse:collapse;width:100%;background:white;border:1px solid #ded9cc}th,td{border-bottom:1px solid #eee8dc;padding:.65rem;text-align:left;vertical-align:top}code{white-space:pre-wrap;word-break:break-word}pre{background:#181713;color:#f8f7f2;padding:1rem;border-radius:12px;overflow:auto}</style></head><body><main><h1>${escapeHtml(title)}</h1><section class="summary"><div class="card"><strong>Events</strong><br>${summary.total}</div><div class="card"><strong>Errors</strong><br>${summary.errors}</div><div class="card"><strong>Latest</strong><br>${summary.latestAt ? escapeHtml(new Date(summary.latestAt).toLocaleString()) : "never"}</div></section><table><thead><tr><th>At</th><th>Type</th><th>Action</th><th>Outcome</th><th>Payload</th></tr></thead><tbody>${rows}</tbody></table><h2>Markdown Export</h2><pre>${escapeHtml(markdown)}</pre></main></body></html>`;
|
|
10175
|
-
};
|
|
10176
|
-
var buildVoiceAuditExport = (events, options = {}) => {
|
|
10177
|
-
const exportEvents = options.redact ? redactVoiceAuditEvents(events, options.redact) : events;
|
|
10178
|
-
return {
|
|
10179
|
-
events: exportEvents,
|
|
10180
|
-
html: renderVoiceAuditHTML(events, options),
|
|
10181
|
-
markdown: renderVoiceAuditMarkdown(events, options),
|
|
10182
|
-
summary: summarizeVoiceAuditTrail(exportEvents)
|
|
10183
|
-
};
|
|
10184
|
-
};
|
|
10185
10182
|
|
|
10186
10183
|
// src/core/sessionReplay.ts
|
|
10187
10184
|
import { Elysia as Elysia2 } from "elysia";
|
|
@@ -10274,6 +10271,89 @@ var buildReplayTurns = (events) => {
|
|
|
10274
10271
|
}
|
|
10275
10272
|
return [...turns.values()];
|
|
10276
10273
|
};
|
|
10274
|
+
var createVoiceSessionListRoutes = (options = {}) => {
|
|
10275
|
+
const path = options.path ?? "/api/voice-sessions";
|
|
10276
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
10277
|
+
const routes = new Elysia2({
|
|
10278
|
+
name: options.name ?? "absolutejs-voice-session-list"
|
|
10279
|
+
}).get(path, createVoiceSessionsJSONHandler(options));
|
|
10280
|
+
if (htmlPath) {
|
|
10281
|
+
routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
|
|
10282
|
+
}
|
|
10283
|
+
return routes;
|
|
10284
|
+
};
|
|
10285
|
+
var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
|
|
10286
|
+
const replay = await summarizeVoiceSessionReplay({
|
|
10287
|
+
...options,
|
|
10288
|
+
sessionId: params.sessionId ?? ""
|
|
10289
|
+
});
|
|
10290
|
+
const body = await (options.render?.(replay) ?? replay.html);
|
|
10291
|
+
return new Response(body, {
|
|
10292
|
+
headers: {
|
|
10293
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10294
|
+
...options.headers
|
|
10295
|
+
}
|
|
10296
|
+
});
|
|
10297
|
+
};
|
|
10298
|
+
var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
|
|
10299
|
+
...options,
|
|
10300
|
+
sessionId: params.sessionId ?? ""
|
|
10301
|
+
});
|
|
10302
|
+
var createVoiceSessionReplayRoutes = (options) => {
|
|
10303
|
+
const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
|
|
10304
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
10305
|
+
const routes = new Elysia2({
|
|
10306
|
+
name: options.name ?? "absolutejs-voice-session-replay"
|
|
10307
|
+
}).get(path, createVoiceSessionReplayJSONHandler(options));
|
|
10308
|
+
if (htmlPath) {
|
|
10309
|
+
routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
|
|
10310
|
+
}
|
|
10311
|
+
return routes;
|
|
10312
|
+
};
|
|
10313
|
+
var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
|
|
10314
|
+
const sessions = await summarizeVoiceSessions({
|
|
10315
|
+
...options,
|
|
10316
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
10317
|
+
provider: query?.provider ?? options.provider,
|
|
10318
|
+
q: query?.q ?? options.q,
|
|
10319
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
10320
|
+
});
|
|
10321
|
+
const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
|
|
10322
|
+
return new Response(body, {
|
|
10323
|
+
headers: {
|
|
10324
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10325
|
+
...options.headers
|
|
10326
|
+
}
|
|
10327
|
+
});
|
|
10328
|
+
};
|
|
10329
|
+
var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
|
|
10330
|
+
...options,
|
|
10331
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
10332
|
+
provider: query?.provider ?? options.provider,
|
|
10333
|
+
q: query?.q ?? options.q,
|
|
10334
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
10335
|
+
});
|
|
10336
|
+
var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
|
|
10337
|
+
'<div class="voice-sessions-list">',
|
|
10338
|
+
...sessions.map((session) => [
|
|
10339
|
+
`<article class="voice-session-card ${escapeHtml(session.status)}">`,
|
|
10340
|
+
'<div class="voice-session-card-header">',
|
|
10341
|
+
`<strong>${escapeHtml(session.sessionId)}</strong>`,
|
|
10342
|
+
`<span>${escapeHtml(session.status)}</span>`,
|
|
10343
|
+
"</div>",
|
|
10344
|
+
"<dl>",
|
|
10345
|
+
`<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
|
|
10346
|
+
`<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
|
|
10347
|
+
`<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
|
|
10348
|
+
`<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
|
|
10349
|
+
"</dl>",
|
|
10350
|
+
session.latestOutcome ? `<p>Outcome: ${escapeHtml(session.latestOutcome)}</p>` : "",
|
|
10351
|
+
session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml).join(", ")}</p>` : "",
|
|
10352
|
+
session.replayHref ? `<p>${session.operationsRecordHref ? `<a href="${escapeHtml(session.operationsRecordHref)}">Open operations record</a> \xB7 ` : ""}<a href="${escapeHtml(session.replayHref)}">Open replay</a></p>` : "",
|
|
10353
|
+
"</article>"
|
|
10354
|
+
].join("")),
|
|
10355
|
+
"</div>"
|
|
10356
|
+
].join("");
|
|
10277
10357
|
var summarizeVoiceSessionReplay = async (options) => {
|
|
10278
10358
|
const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
|
|
10279
10359
|
const events = filterVoiceTraceEvents(sourceEvents, {
|
|
@@ -10284,7 +10364,7 @@ var summarizeVoiceSessionReplay = async (options) => {
|
|
|
10284
10364
|
redact: options.redact,
|
|
10285
10365
|
title: options.title ?? `Voice Session ${options.sessionId}`
|
|
10286
10366
|
});
|
|
10287
|
-
const startedAt = replay.summary
|
|
10367
|
+
const { startedAt } = replay.summary;
|
|
10288
10368
|
return {
|
|
10289
10369
|
evaluation: replay.evaluation,
|
|
10290
10370
|
events,
|
|
@@ -10313,14 +10393,14 @@ var summarizeVoiceSessions = async (options = {}) => {
|
|
|
10313
10393
|
}
|
|
10314
10394
|
const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
|
|
10315
10395
|
const sorted = filterVoiceTraceEvents(sessionEvents);
|
|
10316
|
-
const summary = buildVoiceTraceReplay(sorted, {
|
|
10396
|
+
const { summary } = buildVoiceTraceReplay(sorted, {
|
|
10317
10397
|
evaluation: {
|
|
10318
10398
|
requireAssistantReply: false,
|
|
10319
10399
|
requireCompletedCall: false,
|
|
10320
10400
|
requireTranscript: false,
|
|
10321
10401
|
requireTurn: false
|
|
10322
10402
|
}
|
|
10323
|
-
})
|
|
10403
|
+
});
|
|
10324
10404
|
const providerErrors = {};
|
|
10325
10405
|
const providers = new Set;
|
|
10326
10406
|
let latestOutcome;
|
|
@@ -10337,130 +10417,47 @@ var summarizeVoiceSessions = async (options = {}) => {
|
|
|
10337
10417
|
}
|
|
10338
10418
|
const outcome = getString2(event.payload.outcome);
|
|
10339
10419
|
if (outcome) {
|
|
10340
|
-
latestOutcome = outcome;
|
|
10341
|
-
}
|
|
10342
|
-
}
|
|
10343
|
-
const item = {
|
|
10344
|
-
endedAt: summary.endedAt,
|
|
10345
|
-
errorCount,
|
|
10346
|
-
eventCount: summary.eventCount,
|
|
10347
|
-
latestOutcome,
|
|
10348
|
-
providerErrors,
|
|
10349
|
-
providers: [...providers].sort(),
|
|
10350
|
-
sessionId,
|
|
10351
|
-
startedAt: summary.startedAt,
|
|
10352
|
-
status: errorCount > 0 ? "failed" : "healthy",
|
|
10353
|
-
transcriptCount: summary.transcriptCount,
|
|
10354
|
-
turnCount: summary.turnCount
|
|
10355
|
-
};
|
|
10356
|
-
const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
|
|
10357
|
-
return {
|
|
10358
|
-
...item,
|
|
10359
|
-
operationsRecordHref: resolveSessionHref(options.operationsRecordHref, item),
|
|
10360
|
-
replayHref
|
|
10361
|
-
};
|
|
10362
|
-
});
|
|
10363
|
-
const search = options.q?.trim().toLowerCase();
|
|
10364
|
-
return sessions.filter((session) => {
|
|
10365
|
-
if (options.status && options.status !== "all" && session.status !== options.status) {
|
|
10366
|
-
return false;
|
|
10367
|
-
}
|
|
10368
|
-
if (options.provider && !session.providers.includes(options.provider)) {
|
|
10369
|
-
return false;
|
|
10370
|
-
}
|
|
10371
|
-
if (!search) {
|
|
10372
|
-
return true;
|
|
10373
|
-
}
|
|
10374
|
-
return [
|
|
10375
|
-
session.sessionId,
|
|
10376
|
-
session.latestOutcome,
|
|
10377
|
-
session.status,
|
|
10378
|
-
...session.providers
|
|
10379
|
-
].some((value) => value?.toLowerCase().includes(search));
|
|
10380
|
-
}).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
|
|
10381
|
-
};
|
|
10382
|
-
var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
|
|
10383
|
-
'<div class="voice-sessions-list">',
|
|
10384
|
-
...sessions.map((session) => [
|
|
10385
|
-
`<article class="voice-session-card ${escapeHtml(session.status)}">`,
|
|
10386
|
-
'<div class="voice-session-card-header">',
|
|
10387
|
-
`<strong>${escapeHtml(session.sessionId)}</strong>`,
|
|
10388
|
-
`<span>${escapeHtml(session.status)}</span>`,
|
|
10389
|
-
"</div>",
|
|
10390
|
-
"<dl>",
|
|
10391
|
-
`<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
|
|
10392
|
-
`<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
|
|
10393
|
-
`<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
|
|
10394
|
-
`<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
|
|
10395
|
-
"</dl>",
|
|
10396
|
-
session.latestOutcome ? `<p>Outcome: ${escapeHtml(session.latestOutcome)}</p>` : "",
|
|
10397
|
-
session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml).join(", ")}</p>` : "",
|
|
10398
|
-
session.replayHref ? `<p>${session.operationsRecordHref ? `<a href="${escapeHtml(session.operationsRecordHref)}">Open operations record</a> \xB7 ` : ""}<a href="${escapeHtml(session.replayHref)}">Open replay</a></p>` : "",
|
|
10399
|
-
"</article>"
|
|
10400
|
-
].join("")),
|
|
10401
|
-
"</div>"
|
|
10402
|
-
].join("");
|
|
10403
|
-
var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
|
|
10404
|
-
...options,
|
|
10405
|
-
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
10406
|
-
provider: query?.provider ?? options.provider,
|
|
10407
|
-
q: query?.q ?? options.q,
|
|
10408
|
-
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
10409
|
-
});
|
|
10410
|
-
var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
|
|
10411
|
-
const sessions = await summarizeVoiceSessions({
|
|
10412
|
-
...options,
|
|
10413
|
-
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
10414
|
-
provider: query?.provider ?? options.provider,
|
|
10415
|
-
q: query?.q ?? options.q,
|
|
10416
|
-
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
10417
|
-
});
|
|
10418
|
-
const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
|
|
10419
|
-
return new Response(body, {
|
|
10420
|
-
headers: {
|
|
10421
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
10422
|
-
...options.headers
|
|
10420
|
+
latestOutcome = outcome;
|
|
10421
|
+
}
|
|
10423
10422
|
}
|
|
10423
|
+
const item = {
|
|
10424
|
+
endedAt: summary.endedAt,
|
|
10425
|
+
errorCount,
|
|
10426
|
+
eventCount: summary.eventCount,
|
|
10427
|
+
latestOutcome,
|
|
10428
|
+
providerErrors,
|
|
10429
|
+
providers: [...providers].sort(),
|
|
10430
|
+
sessionId,
|
|
10431
|
+
startedAt: summary.startedAt,
|
|
10432
|
+
status: errorCount > 0 ? "failed" : "healthy",
|
|
10433
|
+
transcriptCount: summary.transcriptCount,
|
|
10434
|
+
turnCount: summary.turnCount
|
|
10435
|
+
};
|
|
10436
|
+
const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
|
|
10437
|
+
return {
|
|
10438
|
+
...item,
|
|
10439
|
+
operationsRecordHref: resolveSessionHref(options.operationsRecordHref, item),
|
|
10440
|
+
replayHref
|
|
10441
|
+
};
|
|
10424
10442
|
});
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
const routes = new Elysia2({
|
|
10430
|
-
name: options.name ?? "absolutejs-voice-session-list"
|
|
10431
|
-
}).get(path, createVoiceSessionsJSONHandler(options));
|
|
10432
|
-
if (htmlPath) {
|
|
10433
|
-
routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
|
|
10434
|
-
}
|
|
10435
|
-
return routes;
|
|
10436
|
-
};
|
|
10437
|
-
var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
|
|
10438
|
-
...options,
|
|
10439
|
-
sessionId: params.sessionId ?? ""
|
|
10440
|
-
});
|
|
10441
|
-
var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
|
|
10442
|
-
const replay = await summarizeVoiceSessionReplay({
|
|
10443
|
-
...options,
|
|
10444
|
-
sessionId: params.sessionId ?? ""
|
|
10445
|
-
});
|
|
10446
|
-
const body = await (options.render?.(replay) ?? replay.html);
|
|
10447
|
-
return new Response(body, {
|
|
10448
|
-
headers: {
|
|
10449
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
10450
|
-
...options.headers
|
|
10443
|
+
const search = options.q?.trim().toLowerCase();
|
|
10444
|
+
return sessions.filter((session) => {
|
|
10445
|
+
if (options.status && options.status !== "all" && session.status !== options.status) {
|
|
10446
|
+
return false;
|
|
10451
10447
|
}
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10448
|
+
if (options.provider && !session.providers.includes(options.provider)) {
|
|
10449
|
+
return false;
|
|
10450
|
+
}
|
|
10451
|
+
if (!search) {
|
|
10452
|
+
return true;
|
|
10453
|
+
}
|
|
10454
|
+
return [
|
|
10455
|
+
session.sessionId,
|
|
10456
|
+
session.latestOutcome,
|
|
10457
|
+
session.status,
|
|
10458
|
+
...session.providers
|
|
10459
|
+
].some((value) => value?.toLowerCase().includes(search));
|
|
10460
|
+
}).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
|
|
10464
10461
|
};
|
|
10465
10462
|
|
|
10466
10463
|
// src/core/traceTimeline.ts
|
|
@@ -10659,35 +10656,6 @@ var renderVoiceTraceTimelineSessionHTML = (session, options = {}) => {
|
|
|
10659
10656
|
};
|
|
10660
10657
|
var renderSessionRows = (report) => report.sessions.length === 0 ? '<tr><td colspan="7">No trace events recorded yet.</td></tr>' : report.sessions.map((session) => `<tr class="${escapeHtml(session.status)}"><td>${session.operationsRecordHref ? `<a href="${escapeHtml(session.operationsRecordHref)}">${escapeHtml(session.sessionId)}</a>` : `<a href="/traces/${encodeURIComponent(session.sessionId)}">${escapeHtml(session.sessionId)}</a>`}</td><td>${escapeHtml(session.status)}</td><td>${String(session.summary.eventCount)}</td><td>${String(session.summary.turnCount)}</td><td>${String(session.summary.errorCount)}</td><td>${formatMs(session.summary.callDurationMs)}</td><td>${session.providers.map((provider) => escapeHtml(provider.provider)).join(", ")}</td></tr>`).join("");
|
|
10661
10658
|
var timelineCSS = "body{background:#0f1318;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}a{color:#fbbf24}.eyebrow{color:#fbbf24;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.5rem);line-height:.92;margin:.2rem 0 1rem}.status{border:1px solid #475569;border-radius:999px;display:inline-flex;padding:8px 12px}.healthy{color:#86efac}.warning{color:#fbbf24}.failed,.error{color:#fca5a5}.metrics,.providers{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));margin:20px 0}.metrics article,.providers article{background:#181f27;border:1px solid #2b3642;border-radius:20px;padding:16px}.metrics span,dt,.muted{color:#a8b0b8}.metrics strong{display:block;font-size:2rem}dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:12px 0 0}dd{font-weight:800;margin:4px 0 0}table{background:#181f27;border-collapse:collapse;border-radius:18px;overflow:hidden;width:100%}td,th{border-bottom:1px solid #2b3642;padding:12px;text-align:left}section{margin-top:28px}@media(max-width:760px){main{padding:20px}table{font-size:.9rem}}";
|
|
10662
|
-
var renderVoiceTraceTimelineHTML = (report, options = {}) => {
|
|
10663
|
-
const snippet = escapeHtml(`const traceStore = createVoiceTraceSinkStore({
|
|
10664
|
-
store: runtimeStorage.traces,
|
|
10665
|
-
sinks: [
|
|
10666
|
-
createVoiceTraceHTTPSink({
|
|
10667
|
-
endpoint: process.env.VOICE_TRACE_WEBHOOK_URL
|
|
10668
|
-
})
|
|
10669
|
-
]
|
|
10670
|
-
});
|
|
10671
|
-
|
|
10672
|
-
app.use(
|
|
10673
|
-
createVoiceTraceTimelineRoutes({
|
|
10674
|
-
htmlPath: '/traces',
|
|
10675
|
-
path: '/api/voice-traces',
|
|
10676
|
-
redact: {
|
|
10677
|
-
keys: ['authorization', 'apiKey', 'token']
|
|
10678
|
-
},
|
|
10679
|
-
store: traceStore
|
|
10680
|
-
})
|
|
10681
|
-
);
|
|
10682
|
-
|
|
10683
|
-
app.use(
|
|
10684
|
-
createVoiceProductionReadinessRoutes({
|
|
10685
|
-
store: traceStore,
|
|
10686
|
-
traceDeliveries: runtimeStorage.traceDeliveries
|
|
10687
|
-
})
|
|
10688
|
-
);`);
|
|
10689
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(options.title ?? "Voice Trace Timelines")}</title><style>${timelineCSS}.primitive{background:#181f27;border:1px solid #334155;border-radius:20px;margin:20px 0;padding:18px}.primitive p{line-height:1.55}.primitive pre{background:#0b1118;border:1px solid #2b3642;border-radius:16px;color:#dbeafe;overflow:auto;padding:14px}.primitive code{color:#bfdbfe}</style></head><body><main><header><p class="eyebrow">Self-hosted voice debugging</p><h1>${escapeHtml(options.title ?? "Voice Trace Timelines")}</h1><p class="muted">Per-call event timelines with provider latency, fallback, timeout, handoff, and error context.</p></header><section class="metrics"><article><span>Sessions</span><strong>${String(report.total)}</strong></article><article><span>Failed</span><strong>${String(report.failed)}</strong></article><article><span>Warnings</span><strong>${String(report.warnings)}</strong></article></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceTraceTimelineRoutes(...)</code> makes traces the proof backbone</h2><p class="muted">Mount trace timelines from the same trace store used by readiness, simulations, provider recovery, delivery sinks, and phone-agent smoke proof.</p><pre><code>${snippet}</code></pre></section><table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Turns</th><th>Errors</th><th>Duration</th><th>Providers</th></tr></thead><tbody>${renderSessionRows(report)}</tbody></table></main></body></html>`;
|
|
10690
|
-
};
|
|
10691
10659
|
var createVoiceTraceTimelineRoutes = (options) => {
|
|
10692
10660
|
const path = options.path ?? "/api/voice-traces";
|
|
10693
10661
|
const htmlPath = options.htmlPath ?? "/traces";
|
|
@@ -10740,6 +10708,35 @@ var createVoiceTraceTimelineRoutes = (options) => {
|
|
|
10740
10708
|
});
|
|
10741
10709
|
return routes;
|
|
10742
10710
|
};
|
|
10711
|
+
var renderVoiceTraceTimelineHTML = (report, options = {}) => {
|
|
10712
|
+
const snippet = escapeHtml(`const traceStore = createVoiceTraceSinkStore({
|
|
10713
|
+
store: runtimeStorage.traces,
|
|
10714
|
+
sinks: [
|
|
10715
|
+
createVoiceTraceHTTPSink({
|
|
10716
|
+
endpoint: process.env.VOICE_TRACE_WEBHOOK_URL
|
|
10717
|
+
})
|
|
10718
|
+
]
|
|
10719
|
+
});
|
|
10720
|
+
|
|
10721
|
+
app.use(
|
|
10722
|
+
createVoiceTraceTimelineRoutes({
|
|
10723
|
+
htmlPath: '/traces',
|
|
10724
|
+
path: '/api/voice-traces',
|
|
10725
|
+
redact: {
|
|
10726
|
+
keys: ['authorization', 'apiKey', 'token']
|
|
10727
|
+
},
|
|
10728
|
+
store: traceStore
|
|
10729
|
+
})
|
|
10730
|
+
);
|
|
10731
|
+
|
|
10732
|
+
app.use(
|
|
10733
|
+
createVoiceProductionReadinessRoutes({
|
|
10734
|
+
store: traceStore,
|
|
10735
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
10736
|
+
})
|
|
10737
|
+
);`);
|
|
10738
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(options.title ?? "Voice Trace Timelines")}</title><style>${timelineCSS}.primitive{background:#181f27;border:1px solid #334155;border-radius:20px;margin:20px 0;padding:18px}.primitive p{line-height:1.55}.primitive pre{background:#0b1118;border:1px solid #2b3642;border-radius:16px;color:#dbeafe;overflow:auto;padding:14px}.primitive code{color:#bfdbfe}</style></head><body><main><header><p class="eyebrow">Self-hosted voice debugging</p><h1>${escapeHtml(options.title ?? "Voice Trace Timelines")}</h1><p class="muted">Per-call event timelines with provider latency, fallback, timeout, handoff, and error context.</p></header><section class="metrics"><article><span>Sessions</span><strong>${String(report.total)}</strong></article><article><span>Failed</span><strong>${String(report.failed)}</strong></article><article><span>Warnings</span><strong>${String(report.warnings)}</strong></article></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceTraceTimelineRoutes(...)</code> makes traces the proof backbone</h2><p class="muted">Mount trace timelines from the same trace store used by readiness, simulations, provider recovery, delivery sinks, and phone-agent smoke proof.</p><pre><code>${snippet}</code></pre></section><table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Turns</th><th>Errors</th><th>Duration</th><th>Providers</th></tr></thead><tbody>${renderSessionRows(report)}</tbody></table></main></body></html>`;
|
|
10739
|
+
};
|
|
10743
10740
|
|
|
10744
10741
|
// src/core/operationsRecord.ts
|
|
10745
10742
|
var getString4 = (value) => typeof value === "string" ? value : undefined;
|
|
@@ -10918,8 +10915,8 @@ var toProviderDecision = (event) => {
|
|
|
10918
10915
|
selectedProvider: getString4(event.payload.selectedProvider),
|
|
10919
10916
|
status,
|
|
10920
10917
|
surface: getString4(event.payload.surface),
|
|
10921
|
-
|
|
10922
|
-
|
|
10918
|
+
turnId: event.turnId,
|
|
10919
|
+
type: event.type
|
|
10923
10920
|
};
|
|
10924
10921
|
};
|
|
10925
10922
|
var summarizeProviderDecisions = (decisions) => {
|
|
@@ -10963,6 +10960,20 @@ var resolveOutcome = (events) => {
|
|
|
10963
10960
|
voicemail: agentResults.some((event) => event.payload.voicemail === true)
|
|
10964
10961
|
};
|
|
10965
10962
|
};
|
|
10963
|
+
var assertVoiceOperationsRecordGuardrails = (record, input = {}) => {
|
|
10964
|
+
const report = evaluateVoiceOperationsRecordGuardrails(record, input);
|
|
10965
|
+
if (!report.ok) {
|
|
10966
|
+
throw new Error(`Voice operations record guardrail assertion failed for ${record.sessionId}: ${report.issues.join(" ")}`);
|
|
10967
|
+
}
|
|
10968
|
+
return report;
|
|
10969
|
+
};
|
|
10970
|
+
var assertVoiceOperationsRecordProviderRecovery = (record, input = {}) => {
|
|
10971
|
+
const report = evaluateVoiceOperationsRecordProviderRecovery(record, input);
|
|
10972
|
+
if (!report.ok) {
|
|
10973
|
+
throw new Error(`Voice operations record provider recovery assertion failed for ${record.sessionId}: ${report.issues.join(" ")}`);
|
|
10974
|
+
}
|
|
10975
|
+
return report;
|
|
10976
|
+
};
|
|
10966
10977
|
var buildVoiceOperationsRecord = async (options) => {
|
|
10967
10978
|
const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
|
|
10968
10979
|
const rawTraceEvents = filterVoiceTraceEvents(sourceEvents, {
|
|
@@ -11039,7 +11050,7 @@ var buildVoiceOperationsRecord = async (options) => {
|
|
|
11039
11050
|
};
|
|
11040
11051
|
var evaluateVoiceOperationsRecordGuardrails = (record, input = {}) => {
|
|
11041
11052
|
const issues = [];
|
|
11042
|
-
const decisions = record.guardrails
|
|
11053
|
+
const { decisions } = record.guardrails;
|
|
11043
11054
|
const proofs = uniqueSorted(decisions.map((decision) => decision.proof));
|
|
11044
11055
|
const ruleIds = uniqueSorted(decisions.flatMap((decision) => decision.findings.map((finding) => finding.ruleId)));
|
|
11045
11056
|
const stages = uniqueSorted(decisions.map((decision) => decision.stage));
|
|
@@ -11102,13 +11113,6 @@ var evaluateVoiceOperationsRecordGuardrails = (record, input = {}) => {
|
|
|
11102
11113
|
warned: record.guardrails.warned
|
|
11103
11114
|
};
|
|
11104
11115
|
};
|
|
11105
|
-
var assertVoiceOperationsRecordGuardrails = (record, input = {}) => {
|
|
11106
|
-
const report = evaluateVoiceOperationsRecordGuardrails(record, input);
|
|
11107
|
-
if (!report.ok) {
|
|
11108
|
-
throw new Error(`Voice operations record guardrail assertion failed for ${record.sessionId}: ${report.issues.join(" ")}`);
|
|
11109
|
-
}
|
|
11110
|
-
return report;
|
|
11111
|
-
};
|
|
11112
11116
|
var evaluateVoiceOperationsRecordProviderRecovery = (record, input = {}) => {
|
|
11113
11117
|
const issues = [];
|
|
11114
11118
|
const summary = record.providerDecisionSummary;
|
|
@@ -11195,13 +11199,6 @@ var evaluateVoiceOperationsRecordProviderRecovery = (record, input = {}) => {
|
|
|
11195
11199
|
total: summary.total
|
|
11196
11200
|
};
|
|
11197
11201
|
};
|
|
11198
|
-
var assertVoiceOperationsRecordProviderRecovery = (record, input = {}) => {
|
|
11199
|
-
const report = evaluateVoiceOperationsRecordProviderRecovery(record, input);
|
|
11200
|
-
if (!report.ok) {
|
|
11201
|
-
throw new Error(`Voice operations record provider recovery assertion failed for ${record.sessionId}: ${report.issues.join(" ")}`);
|
|
11202
|
-
}
|
|
11203
|
-
return report;
|
|
11204
|
-
};
|
|
11205
11202
|
var getAssistantRepliesForTurn = (record, turnId) => turnId ? record.transcript.find((turn) => turn.id === turnId)?.assistantReplies ?? [] : [];
|
|
11206
11203
|
var mediaIssueForStep = (step) => {
|
|
11207
11204
|
const event = step.event.toLowerCase();
|
|
@@ -11356,14 +11353,141 @@ var renderVoiceFailureReplayMarkdown = (report) => {
|
|
|
11356
11353
|
].join(`
|
|
11357
11354
|
`);
|
|
11358
11355
|
};
|
|
11359
|
-
var formatMs2 = (value) => value === undefined ? "n/a" : `${String(value)}ms`;
|
|
11360
|
-
var outcomeLabels = (outcome) => [
|
|
11361
|
-
outcome.complete ? "complete" : undefined,
|
|
11362
|
-
outcome.escalated ? "escalated" : undefined,
|
|
11363
|
-
outcome.transferred ? "transferred" : undefined,
|
|
11364
|
-
outcome.voicemail ? "voicemail" : undefined,
|
|
11365
|
-
outcome.noAnswer ? "no-answer" : undefined
|
|
11366
|
-
].filter((label) => label !== undefined);
|
|
11356
|
+
var formatMs2 = (value) => value === undefined ? "n/a" : `${String(value)}ms`;
|
|
11357
|
+
var outcomeLabels = (outcome) => [
|
|
11358
|
+
outcome.complete ? "complete" : undefined,
|
|
11359
|
+
outcome.escalated ? "escalated" : undefined,
|
|
11360
|
+
outcome.transferred ? "transferred" : undefined,
|
|
11361
|
+
outcome.voicemail ? "voicemail" : undefined,
|
|
11362
|
+
outcome.noAnswer ? "no-answer" : undefined
|
|
11363
|
+
].filter((label) => label !== undefined);
|
|
11364
|
+
var createVoiceOperationsRecordRoutes = (options) => {
|
|
11365
|
+
const path = options.path ?? "/api/voice-operations/:sessionId";
|
|
11366
|
+
const htmlPath = options.htmlPath === undefined ? "/voice-operations/:sessionId" : options.htmlPath;
|
|
11367
|
+
const incidentPath = options.incidentPath === undefined ? `${path}/incident.md` : options.incidentPath;
|
|
11368
|
+
const incidentHtmlPath = options.incidentHtmlPath === undefined && htmlPath ? `${htmlPath}/incident.md` : options.incidentHtmlPath;
|
|
11369
|
+
const routes = new Elysia4({
|
|
11370
|
+
name: options.name ?? "absolutejs-voice-operations-record"
|
|
11371
|
+
});
|
|
11372
|
+
const resolveMediaPipeline = async (sessionId) => {
|
|
11373
|
+
if (options.mediaPipeline === undefined)
|
|
11374
|
+
return;
|
|
11375
|
+
return typeof options.mediaPipeline === "function" ? await options.mediaPipeline({ sessionId }) : options.mediaPipeline;
|
|
11376
|
+
};
|
|
11377
|
+
const buildRecord = async (sessionId) => buildVoiceOperationsRecord({
|
|
11378
|
+
audit: options.audit,
|
|
11379
|
+
evaluation: options.evaluation,
|
|
11380
|
+
events: options.events,
|
|
11381
|
+
integrationEvents: options.integrationEvents,
|
|
11382
|
+
mediaPipeline: await resolveMediaPipeline(sessionId),
|
|
11383
|
+
redact: options.redact,
|
|
11384
|
+
reviews: options.reviews,
|
|
11385
|
+
sessionId,
|
|
11386
|
+
store: options.store,
|
|
11387
|
+
tasks: options.tasks
|
|
11388
|
+
});
|
|
11389
|
+
const getSessionId = (params) => params.sessionId ?? "";
|
|
11390
|
+
routes.get(path, async ({ params }) => Response.json(await buildRecord(getSessionId(params))));
|
|
11391
|
+
const incidentHandler = async ({
|
|
11392
|
+
params
|
|
11393
|
+
}) => {
|
|
11394
|
+
const record = await buildRecord(getSessionId(params));
|
|
11395
|
+
const body = await (options.renderIncidentMarkdown ?? renderVoiceOperationsRecordIncidentMarkdown)(record);
|
|
11396
|
+
return new Response(body, {
|
|
11397
|
+
headers: {
|
|
11398
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
11399
|
+
...options.headers
|
|
11400
|
+
}
|
|
11401
|
+
});
|
|
11402
|
+
};
|
|
11403
|
+
if (incidentPath) {
|
|
11404
|
+
routes.get(incidentPath, incidentHandler);
|
|
11405
|
+
}
|
|
11406
|
+
if (htmlPath) {
|
|
11407
|
+
routes.get(htmlPath, async ({ params }) => {
|
|
11408
|
+
const record = await buildRecord(getSessionId(params));
|
|
11409
|
+
const body = await (options.render ?? ((input) => renderVoiceOperationsRecordHTML(input, {
|
|
11410
|
+
incidentHref: incidentHtmlPath ? resolveRoutePath(incidentHtmlPath, input.sessionId) : undefined,
|
|
11411
|
+
title: options.title
|
|
11412
|
+
})))(record);
|
|
11413
|
+
return new Response(body, {
|
|
11414
|
+
headers: {
|
|
11415
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
11416
|
+
...options.headers
|
|
11417
|
+
}
|
|
11418
|
+
});
|
|
11419
|
+
});
|
|
11420
|
+
}
|
|
11421
|
+
if (incidentHtmlPath && incidentHtmlPath !== incidentPath) {
|
|
11422
|
+
routes.get(incidentHtmlPath, incidentHandler);
|
|
11423
|
+
}
|
|
11424
|
+
return routes;
|
|
11425
|
+
};
|
|
11426
|
+
var renderVoiceOperationsRecordGuardrailMarkdown = (record) => {
|
|
11427
|
+
if (record.guardrails.total === 0) {
|
|
11428
|
+
return [
|
|
11429
|
+
"## Guardrail evidence",
|
|
11430
|
+
"",
|
|
11431
|
+
"- No assistant.guardrail events were recorded for this session."
|
|
11432
|
+
].join(`
|
|
11433
|
+
`);
|
|
11434
|
+
}
|
|
11435
|
+
return [
|
|
11436
|
+
"## Guardrail evidence",
|
|
11437
|
+
"",
|
|
11438
|
+
...record.guardrails.decisions.map((decision) => {
|
|
11439
|
+
const findings = decision.findings.map((finding) => [finding.action, finding.ruleId, finding.label].filter((value) => typeof value === "string").join(":")).filter(Boolean).join(", ");
|
|
11440
|
+
return `- assistant.guardrail ${decision.stage ?? "unknown"}: ${decision.status ?? "unknown"}; allowed=${String(decision.allowed ?? "unknown")}; proof=${decision.proof ?? "runtime"}; findings=${findings || "none"}`;
|
|
11441
|
+
})
|
|
11442
|
+
].join(`
|
|
11443
|
+
`);
|
|
11444
|
+
};
|
|
11445
|
+
var renderVoiceOperationsRecordHTML = (record, options = {}) => {
|
|
11446
|
+
const providers = record.providers.length ? record.providers.map((provider) => `<article><strong>${escapeHtml(provider.provider)}</strong><span>${String(provider.eventCount)} events</span><span>${formatMs2(provider.averageElapsedMs)} avg</span><span>${String(provider.errorCount)} errors</span></article>`).join("") : '<p class="muted">No provider events recorded.</p>';
|
|
11447
|
+
const transcript = record.transcript.length ? record.transcript.map((turn) => `<li><strong>${escapeHtml(turn.id)}</strong>${turn.committedText ? `<p><span class="label">Caller</span>${escapeHtml(turn.committedText)}</p>` : ""}${turn.assistantReplies.map((reply) => `<p><span class="label">Assistant</span>${escapeHtml(reply)}</p>`).join("")}${turn.errors.map((error) => `<p class="error"><span class="label">Error</span>${escapeHtml(error)}</p>`).join("")}</li>`).join("") : "<li>No transcript turns recorded.</li>";
|
|
11448
|
+
const providerDecisions = record.providerDecisions.length ? record.providerDecisions.map((decision) => `<li><strong>${escapeHtml(decision.provider ?? decision.selectedProvider ?? decision.fallbackProvider ?? "provider")}</strong> <span>${escapeHtml(decision.status ?? decision.type)}</span> ${formatMs2(decision.elapsedMs)}${decision.surface ? `<p><span class="label">Surface</span>${escapeHtml(decision.surface)}</p>` : ""}${decision.kind ? `<p><span class="label">Kind</span>${escapeHtml(decision.kind)}</p>` : ""}${decision.selectedProvider ? `<p>Selected: ${escapeHtml(decision.selectedProvider)}</p>` : ""}${decision.fallbackProvider ? `<p>Fallback: ${escapeHtml(decision.fallbackProvider)}</p>` : ""}${decision.error ? `<p class="error">${escapeHtml(decision.error)}</p>` : ""}${decision.reason ? `<p>${escapeHtml(decision.reason)}</p>` : ""}</li>`).join("") : "<li>No provider decisions recorded.</li>";
|
|
11449
|
+
const { providerDecisionSummary } = record;
|
|
11450
|
+
const handoffs = record.handoffs.length ? record.handoffs.map((handoff) => `<li><strong>${escapeHtml(handoff.fromAgentId ?? "unknown")}</strong> to <strong>${escapeHtml(handoff.targetAgentId ?? "unknown")}</strong> <span>${escapeHtml(handoff.status ?? "")}</span><p>${escapeHtml(handoff.summary ?? handoff.reason ?? "")}</p></li>`).join("") : "<li>No agent handoffs recorded.</li>";
|
|
11451
|
+
const tools = record.tools.length ? record.tools.map((tool) => `<li><strong>${escapeHtml(tool.toolName ?? "tool")}</strong> <span>${escapeHtml(tool.status ?? "")}</span> ${formatMs2(tool.elapsedMs)} ${tool.error ? `<p>${escapeHtml(tool.error)}</p>` : ""}</li>`).join("") : "<li>No tool calls recorded.</li>";
|
|
11452
|
+
const reviews = record.reviews?.reviews.length ? record.reviews.reviews.map((review) => `<li><strong>${escapeHtml(review.title)}</strong> <span>${escapeHtml(review.summary.outcome ?? "")}</span><p>${escapeHtml(review.postCall?.summary ?? review.transcript.actual)}</p></li>`).join("") : "<li>No call reviews recorded.</li>";
|
|
11453
|
+
const tasks = record.tasks?.tasks.length ? record.tasks.tasks.map((task) => `<li><strong>${escapeHtml(task.title)}</strong> <span>${escapeHtml(task.status)}</span><p>${escapeHtml(task.recommendedAction)}</p></li>`).join("") : "<li>No ops tasks recorded.</li>";
|
|
11454
|
+
const integrationEvents = record.integrationEvents?.events.length ? record.integrationEvents.events.map((event) => `<li><strong>${escapeHtml(event.type)}</strong> <span>${escapeHtml(event.deliveryStatus ?? "local")}</span><p>${escapeHtml(event.deliveryError ?? event.deliveredTo ?? "")}</p></li>`).join("") : "<li>No integration events recorded.</li>";
|
|
11455
|
+
const guardrails = record.guardrails.total ? record.guardrails.decisions.map((decision) => {
|
|
11456
|
+
const findings = decision.findings.map((finding) => finding.label ?? finding.ruleId ?? finding.action).filter((value) => typeof value === "string").join(", ") || "none";
|
|
11457
|
+
return `<li><strong>assistant.guardrail ${escapeHtml(decision.stage ?? "unknown")}</strong> <span>${escapeHtml(decision.status ?? "")}</span><p>Allowed: ${escapeHtml(String(decision.allowed ?? "unknown"))} \xB7 Proof: ${escapeHtml(decision.proof ?? "runtime")}${decision.turnId ? ` \xB7 Turn: ${escapeHtml(decision.turnId)}` : ""}</p><p>${escapeHtml(findings)}</p></li>`;
|
|
11458
|
+
}).join("") : "<li>No assistant.guardrail events recorded.</li>";
|
|
11459
|
+
const telephonyMedia = record.telephonyMedia.events.length ? record.telephonyMedia.events.slice(0, 50).map((event) => {
|
|
11460
|
+
const details = [
|
|
11461
|
+
event.carrier ? `Carrier: ${event.carrier}` : undefined,
|
|
11462
|
+
event.streamId ? `Stream: ${event.streamId}` : undefined,
|
|
11463
|
+
event.callSid ? `Call: ${event.callSid}` : undefined,
|
|
11464
|
+
event.direction ? `Direction: ${event.direction}` : undefined,
|
|
11465
|
+
event.sequenceNumber ? `Seq: ${event.sequenceNumber}` : undefined,
|
|
11466
|
+
`Audio bytes: ${String(event.audioBytes)}`
|
|
11467
|
+
].filter((detail) => typeof detail === "string");
|
|
11468
|
+
return `<li><strong>${escapeHtml(event.event)}</strong> <span>${escapeHtml(new Date(event.at).toLocaleString())}</span><p>${escapeHtml(details.join(" \xB7 "))}</p></li>`;
|
|
11469
|
+
}).join("") : "<li>No telephony media trace events recorded.</li>";
|
|
11470
|
+
const mediaPipelineSection = record.mediaPipeline ? `<section id="media-pipeline"><h2>Media Pipeline</h2><p class="muted">Surface: ${escapeHtml(record.mediaPipeline.surface)} \xB7 Status: ${escapeHtml(record.mediaPipeline.status)} \xB7 Quality: ${escapeHtml(record.mediaPipeline.qualityStatus)} \xB7 Transport: ${escapeHtml(record.mediaPipeline.transportStatus ?? "n/a")} \xB7 Graph: ${escapeHtml(record.mediaPipeline.processorGraphStatus ?? "n/a")} \xB7 Frames: ${String(record.mediaPipeline.frames)} \xB7 Jitter: ${record.mediaPipeline.jitterMs === undefined ? "n/a" : `${String(record.mediaPipeline.jitterMs)}ms`}</p><ul>${record.mediaPipeline.issueCodes.length ? record.mediaPipeline.issueCodes.map((code) => `<li><strong>${escapeHtml(code)}</strong></li>`).join("") : "<li>No media pipeline issue codes.</li>"}</ul></section>` : "";
|
|
11471
|
+
const mediaPipelineCard = record.mediaPipeline ? `<div class="card"><span>Media pipeline</span><strong>${escapeHtml(record.mediaPipeline.status)}</strong><span>${String(record.mediaPipeline.issueCodes.length)} issue code(s)</span></div>` : "";
|
|
11472
|
+
const mediaPipelineNavLink = record.mediaPipeline ? '<a href="#media-pipeline">Media pipeline</a>' : "";
|
|
11473
|
+
const snippet = escapeHtml(`app.use(
|
|
11474
|
+
createVoiceOperationsRecordRoutes({
|
|
11475
|
+
audit: auditStore,
|
|
11476
|
+
integrationEvents: opsEvents,
|
|
11477
|
+
htmlPath: '/voice-ops/:sessionId',
|
|
11478
|
+
path: '/api/voice-ops/:sessionId',
|
|
11479
|
+
redact: {
|
|
11480
|
+
keys: ['authorization', 'apiKey', 'token']
|
|
11481
|
+
},
|
|
11482
|
+
reviews: callReviews,
|
|
11483
|
+
store: traceStore,
|
|
11484
|
+
tasks: opsTasks
|
|
11485
|
+
})
|
|
11486
|
+
);`);
|
|
11487
|
+
const incidentMarkdown = escapeHtml(renderVoiceOperationsRecordIncidentMarkdown(record));
|
|
11488
|
+
const incidentLink = options.incidentHref ? `<a href="${escapeHtml(options.incidentHref)}">Download incident.md</a>` : "";
|
|
11489
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(options.title ?? "Voice Operations Record")}</title><style>body{background:#101417;color:#f9f4e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.eyebrow{color:#fbbf24;font-size:.8rem;font-weight:900;letter-spacing:.14em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.8rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #475569;border-radius:999px;display:inline-flex;padding:8px 12px}.healthy{color:#86efac}.warning{color:#fbbf24}.failed,.error{color:#fca5a5}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.card,.primitive{background:#182025;border:1px solid #2d3a43;border-radius:20px;padding:16px}.card span,.muted,.label{color:#a9b4bd}.label{display:block;font-size:.72rem;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.card strong{display:block;font-size:2rem}section{margin-top:28px}article{display:grid;gap:8px}ul{display:grid;gap:10px;list-style:none;padding:0}li{background:#182025;border:1px solid #2d3a43;border-radius:16px;padding:14px}pre{background:#080d10;border:1px solid #2d3a43;border-radius:16px;color:#dbeafe;overflow:auto;padding:14px}.hero-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}.hero-actions a{background:#fbbf24;border-radius:999px;color:#111827;font-weight:900;padding:10px 14px;text-decoration:none}.two-column{display:grid;gap:18px;grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr)}@media(max-width:860px){main{padding:20px}.two-column{grid-template-columns:1fr}}</style></head><body><main><p class="eyebrow">Call log replacement</p><h1>${escapeHtml(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml(record.status)}">${escapeHtml(record.status)}</p><div class="hero-actions"><a href="#transcript">Transcript</a><a href="#provider-decisions">Provider decisions</a><a href="#telephony-media">Telephony media</a>${mediaPipelineNavLink}<a href="#guardrails">Guardrails</a><a href="#incident-handoff">Incident handoff</a>${incidentLink}</div><section class="grid"><div class="card"><span>Events</span><strong>${String(record.summary.eventCount)}</strong></div><div class="card"><span>Turns</span><strong>${String(record.summary.turnCount)}</strong></div><div class="card"><span>Errors</span><strong>${String(record.summary.errorCount)}</strong></div><div class="card"><span>Duration</span><strong>${formatMs2(record.summary.callDurationMs)}</strong></div><div class="card"><span>Provider recovery</span><strong>${escapeHtml(providerDecisionSummary.recoveryStatus)}</strong><span>${String(providerDecisionSummary.fallbacks)} fallback / ${String(providerDecisionSummary.degraded)} degraded / ${String(providerDecisionSummary.errors)} errors</span></div><div class="card"><span>Telephony media</span><strong>${String(record.telephonyMedia.media)}</strong><span>${String(record.telephonyMedia.inbound)} inbound / ${String(record.telephonyMedia.outbound)} outbound / ${String(record.telephonyMedia.clears)} clears</span></div><div class="card"><span>Guardrails</span><strong>${String(record.guardrails.blocked)}</strong></div><div class="card"><span>Audit</span><strong>${String(record.audit?.total ?? 0)}</strong></div><div class="card"><span>Reviews</span><strong>${String(record.reviews?.total ?? 0)}</strong></div><div class="card"><span>Tasks</span><strong>${String(record.tasks?.total ?? 0)}</strong></div><div class="card"><span>Integrations</span><strong>${String(record.integrationEvents?.total ?? 0)}</strong></div>${mediaPipelineCard}</section><section class="two-column"><div><h2 id="transcript">Transcript</h2><ul>${transcript}</ul></div><div><h2 id="provider-decisions">Provider Decisions</h2><ul>${providerDecisions}</ul></div></section><section id="telephony-media"><h2>Telephony Media</h2><p class="muted">Live <code>client.telephony_media</code> stream lifecycle evidence attached to this session. Carriers: ${escapeHtml(record.telephonyMedia.carriers.join(", ") || "none")}. Streams: ${escapeHtml(record.telephonyMedia.streamIds.join(", ") || "none")}. Inbound: ${String(record.telephonyMedia.inbound)}. Outbound: ${String(record.telephonyMedia.outbound)}. Marks: ${String(record.telephonyMedia.marks)}. Clears: ${String(record.telephonyMedia.clears)}.</p><ul>${telephonyMedia}</ul></section>${mediaPipelineSection}<section id="guardrails"><h2>Guardrail Evidence</h2><p class="muted">Live <code>assistant.guardrail</code> decisions attached to this session.</p><ul>${guardrails}</ul></section><section id="incident-handoff"><h2>Copyable Incident Handoff</h2><p class="muted">Paste this into Slack, Linear, Zendesk, or an incident review. ${incidentLink}</p><pre><code>${incidentMarkdown}</code></pre></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceOperationsRecordRoutes(...)</code> gives every call one debuggable object</h2><p class="muted">Use this as the support/debug payload across traces, provider routing, tools, handoffs, guardrails, audit, latency, replay, reviews, tasks, media streams, and webhook delivery.</p><pre><code>${snippet}</code></pre></section><section><h2>Provider Summary</h2><div class="grid">${providers}</div></section><section><h2>Handoffs</h2><ul>${handoffs}</ul></section><section><h2>Tools</h2><ul>${tools}</ul></section><section><h2>Reviews</h2><ul>${reviews}</ul></section><section><h2>Tasks</h2><ul>${tasks}</ul></section><section><h2>Integration Events</h2><ul>${integrationEvents}</ul></section></main></body></html>`;
|
|
11490
|
+
};
|
|
11367
11491
|
var renderVoiceOperationsRecordIncidentMarkdown = (record) => {
|
|
11368
11492
|
const outcomes = outcomeLabels(record.outcome);
|
|
11369
11493
|
const topErrors = record.traceEvents.filter((event) => event.type === "session.error").map((event) => getString4(event.payload.error)).filter((error) => typeof error === "string").slice(0, 3);
|
|
@@ -11380,7 +11504,7 @@ var renderVoiceOperationsRecordIncidentMarkdown = (record) => {
|
|
|
11380
11504
|
].filter((part) => typeof part === "string");
|
|
11381
11505
|
return `- ${provider}: ${parts.join("; ") || "decision recorded"}`;
|
|
11382
11506
|
}) : ["- none recorded"];
|
|
11383
|
-
const providerDecisionSummary = record
|
|
11507
|
+
const { providerDecisionSummary } = record;
|
|
11384
11508
|
const providerRecoveryLine = [
|
|
11385
11509
|
`status=${providerDecisionSummary.recoveryStatus}`,
|
|
11386
11510
|
`selected=${String(providerDecisionSummary.selected)}`,
|
|
@@ -11461,133 +11585,6 @@ var renderVoiceOperationsRecordIncidentMarkdown = (record) => {
|
|
|
11461
11585
|
].join(`
|
|
11462
11586
|
`);
|
|
11463
11587
|
};
|
|
11464
|
-
var renderVoiceOperationsRecordGuardrailMarkdown = (record) => {
|
|
11465
|
-
if (record.guardrails.total === 0) {
|
|
11466
|
-
return [
|
|
11467
|
-
"## Guardrail evidence",
|
|
11468
|
-
"",
|
|
11469
|
-
"- No assistant.guardrail events were recorded for this session."
|
|
11470
|
-
].join(`
|
|
11471
|
-
`);
|
|
11472
|
-
}
|
|
11473
|
-
return [
|
|
11474
|
-
"## Guardrail evidence",
|
|
11475
|
-
"",
|
|
11476
|
-
...record.guardrails.decisions.map((decision) => {
|
|
11477
|
-
const findings = decision.findings.map((finding) => [finding.action, finding.ruleId, finding.label].filter((value) => typeof value === "string").join(":")).filter(Boolean).join(", ");
|
|
11478
|
-
return `- assistant.guardrail ${decision.stage ?? "unknown"}: ${decision.status ?? "unknown"}; allowed=${String(decision.allowed ?? "unknown")}; proof=${decision.proof ?? "runtime"}; findings=${findings || "none"}`;
|
|
11479
|
-
})
|
|
11480
|
-
].join(`
|
|
11481
|
-
`);
|
|
11482
|
-
};
|
|
11483
|
-
var renderVoiceOperationsRecordHTML = (record, options = {}) => {
|
|
11484
|
-
const providers = record.providers.length ? record.providers.map((provider) => `<article><strong>${escapeHtml(provider.provider)}</strong><span>${String(provider.eventCount)} events</span><span>${formatMs2(provider.averageElapsedMs)} avg</span><span>${String(provider.errorCount)} errors</span></article>`).join("") : '<p class="muted">No provider events recorded.</p>';
|
|
11485
|
-
const transcript = record.transcript.length ? record.transcript.map((turn) => `<li><strong>${escapeHtml(turn.id)}</strong>${turn.committedText ? `<p><span class="label">Caller</span>${escapeHtml(turn.committedText)}</p>` : ""}${turn.assistantReplies.map((reply) => `<p><span class="label">Assistant</span>${escapeHtml(reply)}</p>`).join("")}${turn.errors.map((error) => `<p class="error"><span class="label">Error</span>${escapeHtml(error)}</p>`).join("")}</li>`).join("") : "<li>No transcript turns recorded.</li>";
|
|
11486
|
-
const providerDecisions = record.providerDecisions.length ? record.providerDecisions.map((decision) => `<li><strong>${escapeHtml(decision.provider ?? decision.selectedProvider ?? decision.fallbackProvider ?? "provider")}</strong> <span>${escapeHtml(decision.status ?? decision.type)}</span> ${formatMs2(decision.elapsedMs)}${decision.surface ? `<p><span class="label">Surface</span>${escapeHtml(decision.surface)}</p>` : ""}${decision.kind ? `<p><span class="label">Kind</span>${escapeHtml(decision.kind)}</p>` : ""}${decision.selectedProvider ? `<p>Selected: ${escapeHtml(decision.selectedProvider)}</p>` : ""}${decision.fallbackProvider ? `<p>Fallback: ${escapeHtml(decision.fallbackProvider)}</p>` : ""}${decision.error ? `<p class="error">${escapeHtml(decision.error)}</p>` : ""}${decision.reason ? `<p>${escapeHtml(decision.reason)}</p>` : ""}</li>`).join("") : "<li>No provider decisions recorded.</li>";
|
|
11487
|
-
const providerDecisionSummary = record.providerDecisionSummary;
|
|
11488
|
-
const handoffs = record.handoffs.length ? record.handoffs.map((handoff) => `<li><strong>${escapeHtml(handoff.fromAgentId ?? "unknown")}</strong> to <strong>${escapeHtml(handoff.targetAgentId ?? "unknown")}</strong> <span>${escapeHtml(handoff.status ?? "")}</span><p>${escapeHtml(handoff.summary ?? handoff.reason ?? "")}</p></li>`).join("") : "<li>No agent handoffs recorded.</li>";
|
|
11489
|
-
const tools = record.tools.length ? record.tools.map((tool) => `<li><strong>${escapeHtml(tool.toolName ?? "tool")}</strong> <span>${escapeHtml(tool.status ?? "")}</span> ${formatMs2(tool.elapsedMs)} ${tool.error ? `<p>${escapeHtml(tool.error)}</p>` : ""}</li>`).join("") : "<li>No tool calls recorded.</li>";
|
|
11490
|
-
const reviews = record.reviews?.reviews.length ? record.reviews.reviews.map((review) => `<li><strong>${escapeHtml(review.title)}</strong> <span>${escapeHtml(review.summary.outcome ?? "")}</span><p>${escapeHtml(review.postCall?.summary ?? review.transcript.actual)}</p></li>`).join("") : "<li>No call reviews recorded.</li>";
|
|
11491
|
-
const tasks = record.tasks?.tasks.length ? record.tasks.tasks.map((task) => `<li><strong>${escapeHtml(task.title)}</strong> <span>${escapeHtml(task.status)}</span><p>${escapeHtml(task.recommendedAction)}</p></li>`).join("") : "<li>No ops tasks recorded.</li>";
|
|
11492
|
-
const integrationEvents = record.integrationEvents?.events.length ? record.integrationEvents.events.map((event) => `<li><strong>${escapeHtml(event.type)}</strong> <span>${escapeHtml(event.deliveryStatus ?? "local")}</span><p>${escapeHtml(event.deliveryError ?? event.deliveredTo ?? "")}</p></li>`).join("") : "<li>No integration events recorded.</li>";
|
|
11493
|
-
const guardrails = record.guardrails.total ? record.guardrails.decisions.map((decision) => {
|
|
11494
|
-
const findings = decision.findings.map((finding) => finding.label ?? finding.ruleId ?? finding.action).filter((value) => typeof value === "string").join(", ") || "none";
|
|
11495
|
-
return `<li><strong>assistant.guardrail ${escapeHtml(decision.stage ?? "unknown")}</strong> <span>${escapeHtml(decision.status ?? "")}</span><p>Allowed: ${escapeHtml(String(decision.allowed ?? "unknown"))} \xB7 Proof: ${escapeHtml(decision.proof ?? "runtime")}${decision.turnId ? ` \xB7 Turn: ${escapeHtml(decision.turnId)}` : ""}</p><p>${escapeHtml(findings)}</p></li>`;
|
|
11496
|
-
}).join("") : "<li>No assistant.guardrail events recorded.</li>";
|
|
11497
|
-
const telephonyMedia = record.telephonyMedia.events.length ? record.telephonyMedia.events.slice(0, 50).map((event) => {
|
|
11498
|
-
const details = [
|
|
11499
|
-
event.carrier ? `Carrier: ${event.carrier}` : undefined,
|
|
11500
|
-
event.streamId ? `Stream: ${event.streamId}` : undefined,
|
|
11501
|
-
event.callSid ? `Call: ${event.callSid}` : undefined,
|
|
11502
|
-
event.direction ? `Direction: ${event.direction}` : undefined,
|
|
11503
|
-
event.sequenceNumber ? `Seq: ${event.sequenceNumber}` : undefined,
|
|
11504
|
-
`Audio bytes: ${String(event.audioBytes)}`
|
|
11505
|
-
].filter((detail) => typeof detail === "string");
|
|
11506
|
-
return `<li><strong>${escapeHtml(event.event)}</strong> <span>${escapeHtml(new Date(event.at).toLocaleString())}</span><p>${escapeHtml(details.join(" \xB7 "))}</p></li>`;
|
|
11507
|
-
}).join("") : "<li>No telephony media trace events recorded.</li>";
|
|
11508
|
-
const mediaPipelineSection = record.mediaPipeline ? `<section id="media-pipeline"><h2>Media Pipeline</h2><p class="muted">Surface: ${escapeHtml(record.mediaPipeline.surface)} \xB7 Status: ${escapeHtml(record.mediaPipeline.status)} \xB7 Quality: ${escapeHtml(record.mediaPipeline.qualityStatus)} \xB7 Transport: ${escapeHtml(record.mediaPipeline.transportStatus ?? "n/a")} \xB7 Graph: ${escapeHtml(record.mediaPipeline.processorGraphStatus ?? "n/a")} \xB7 Frames: ${String(record.mediaPipeline.frames)} \xB7 Jitter: ${record.mediaPipeline.jitterMs === undefined ? "n/a" : `${String(record.mediaPipeline.jitterMs)}ms`}</p><ul>${record.mediaPipeline.issueCodes.length ? record.mediaPipeline.issueCodes.map((code) => `<li><strong>${escapeHtml(code)}</strong></li>`).join("") : "<li>No media pipeline issue codes.</li>"}</ul></section>` : "";
|
|
11509
|
-
const mediaPipelineCard = record.mediaPipeline ? `<div class="card"><span>Media pipeline</span><strong>${escapeHtml(record.mediaPipeline.status)}</strong><span>${String(record.mediaPipeline.issueCodes.length)} issue code(s)</span></div>` : "";
|
|
11510
|
-
const mediaPipelineNavLink = record.mediaPipeline ? '<a href="#media-pipeline">Media pipeline</a>' : "";
|
|
11511
|
-
const snippet = escapeHtml(`app.use(
|
|
11512
|
-
createVoiceOperationsRecordRoutes({
|
|
11513
|
-
audit: auditStore,
|
|
11514
|
-
integrationEvents: opsEvents,
|
|
11515
|
-
htmlPath: '/voice-ops/:sessionId',
|
|
11516
|
-
path: '/api/voice-ops/:sessionId',
|
|
11517
|
-
redact: {
|
|
11518
|
-
keys: ['authorization', 'apiKey', 'token']
|
|
11519
|
-
},
|
|
11520
|
-
reviews: callReviews,
|
|
11521
|
-
store: traceStore,
|
|
11522
|
-
tasks: opsTasks
|
|
11523
|
-
})
|
|
11524
|
-
);`);
|
|
11525
|
-
const incidentMarkdown = escapeHtml(renderVoiceOperationsRecordIncidentMarkdown(record));
|
|
11526
|
-
const incidentLink = options.incidentHref ? `<a href="${escapeHtml(options.incidentHref)}">Download incident.md</a>` : "";
|
|
11527
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(options.title ?? "Voice Operations Record")}</title><style>body{background:#101417;color:#f9f4e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.eyebrow{color:#fbbf24;font-size:.8rem;font-weight:900;letter-spacing:.14em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.8rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #475569;border-radius:999px;display:inline-flex;padding:8px 12px}.healthy{color:#86efac}.warning{color:#fbbf24}.failed,.error{color:#fca5a5}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.card,.primitive{background:#182025;border:1px solid #2d3a43;border-radius:20px;padding:16px}.card span,.muted,.label{color:#a9b4bd}.label{display:block;font-size:.72rem;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.card strong{display:block;font-size:2rem}section{margin-top:28px}article{display:grid;gap:8px}ul{display:grid;gap:10px;list-style:none;padding:0}li{background:#182025;border:1px solid #2d3a43;border-radius:16px;padding:14px}pre{background:#080d10;border:1px solid #2d3a43;border-radius:16px;color:#dbeafe;overflow:auto;padding:14px}.hero-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}.hero-actions a{background:#fbbf24;border-radius:999px;color:#111827;font-weight:900;padding:10px 14px;text-decoration:none}.two-column{display:grid;gap:18px;grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr)}@media(max-width:860px){main{padding:20px}.two-column{grid-template-columns:1fr}}</style></head><body><main><p class="eyebrow">Call log replacement</p><h1>${escapeHtml(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml(record.status)}">${escapeHtml(record.status)}</p><div class="hero-actions"><a href="#transcript">Transcript</a><a href="#provider-decisions">Provider decisions</a><a href="#telephony-media">Telephony media</a>${mediaPipelineNavLink}<a href="#guardrails">Guardrails</a><a href="#incident-handoff">Incident handoff</a>${incidentLink}</div><section class="grid"><div class="card"><span>Events</span><strong>${String(record.summary.eventCount)}</strong></div><div class="card"><span>Turns</span><strong>${String(record.summary.turnCount)}</strong></div><div class="card"><span>Errors</span><strong>${String(record.summary.errorCount)}</strong></div><div class="card"><span>Duration</span><strong>${formatMs2(record.summary.callDurationMs)}</strong></div><div class="card"><span>Provider recovery</span><strong>${escapeHtml(providerDecisionSummary.recoveryStatus)}</strong><span>${String(providerDecisionSummary.fallbacks)} fallback / ${String(providerDecisionSummary.degraded)} degraded / ${String(providerDecisionSummary.errors)} errors</span></div><div class="card"><span>Telephony media</span><strong>${String(record.telephonyMedia.media)}</strong><span>${String(record.telephonyMedia.inbound)} inbound / ${String(record.telephonyMedia.outbound)} outbound / ${String(record.telephonyMedia.clears)} clears</span></div><div class="card"><span>Guardrails</span><strong>${String(record.guardrails.blocked)}</strong></div><div class="card"><span>Audit</span><strong>${String(record.audit?.total ?? 0)}</strong></div><div class="card"><span>Reviews</span><strong>${String(record.reviews?.total ?? 0)}</strong></div><div class="card"><span>Tasks</span><strong>${String(record.tasks?.total ?? 0)}</strong></div><div class="card"><span>Integrations</span><strong>${String(record.integrationEvents?.total ?? 0)}</strong></div>${mediaPipelineCard}</section><section class="two-column"><div><h2 id="transcript">Transcript</h2><ul>${transcript}</ul></div><div><h2 id="provider-decisions">Provider Decisions</h2><ul>${providerDecisions}</ul></div></section><section id="telephony-media"><h2>Telephony Media</h2><p class="muted">Live <code>client.telephony_media</code> stream lifecycle evidence attached to this session. Carriers: ${escapeHtml(record.telephonyMedia.carriers.join(", ") || "none")}. Streams: ${escapeHtml(record.telephonyMedia.streamIds.join(", ") || "none")}. Inbound: ${String(record.telephonyMedia.inbound)}. Outbound: ${String(record.telephonyMedia.outbound)}. Marks: ${String(record.telephonyMedia.marks)}. Clears: ${String(record.telephonyMedia.clears)}.</p><ul>${telephonyMedia}</ul></section>${mediaPipelineSection}<section id="guardrails"><h2>Guardrail Evidence</h2><p class="muted">Live <code>assistant.guardrail</code> decisions attached to this session.</p><ul>${guardrails}</ul></section><section id="incident-handoff"><h2>Copyable Incident Handoff</h2><p class="muted">Paste this into Slack, Linear, Zendesk, or an incident review. ${incidentLink}</p><pre><code>${incidentMarkdown}</code></pre></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceOperationsRecordRoutes(...)</code> gives every call one debuggable object</h2><p class="muted">Use this as the support/debug payload across traces, provider routing, tools, handoffs, guardrails, audit, latency, replay, reviews, tasks, media streams, and webhook delivery.</p><pre><code>${snippet}</code></pre></section><section><h2>Provider Summary</h2><div class="grid">${providers}</div></section><section><h2>Handoffs</h2><ul>${handoffs}</ul></section><section><h2>Tools</h2><ul>${tools}</ul></section><section><h2>Reviews</h2><ul>${reviews}</ul></section><section><h2>Tasks</h2><ul>${tasks}</ul></section><section><h2>Integration Events</h2><ul>${integrationEvents}</ul></section></main></body></html>`;
|
|
11528
|
-
};
|
|
11529
|
-
var createVoiceOperationsRecordRoutes = (options) => {
|
|
11530
|
-
const path = options.path ?? "/api/voice-operations/:sessionId";
|
|
11531
|
-
const htmlPath = options.htmlPath === undefined ? "/voice-operations/:sessionId" : options.htmlPath;
|
|
11532
|
-
const incidentPath = options.incidentPath === undefined ? `${path}/incident.md` : options.incidentPath;
|
|
11533
|
-
const incidentHtmlPath = options.incidentHtmlPath === undefined && htmlPath ? `${htmlPath}/incident.md` : options.incidentHtmlPath;
|
|
11534
|
-
const routes = new Elysia4({
|
|
11535
|
-
name: options.name ?? "absolutejs-voice-operations-record"
|
|
11536
|
-
});
|
|
11537
|
-
const resolveMediaPipeline = async (sessionId) => {
|
|
11538
|
-
if (options.mediaPipeline === undefined)
|
|
11539
|
-
return;
|
|
11540
|
-
return typeof options.mediaPipeline === "function" ? await options.mediaPipeline({ sessionId }) : options.mediaPipeline;
|
|
11541
|
-
};
|
|
11542
|
-
const buildRecord = async (sessionId) => buildVoiceOperationsRecord({
|
|
11543
|
-
audit: options.audit,
|
|
11544
|
-
evaluation: options.evaluation,
|
|
11545
|
-
events: options.events,
|
|
11546
|
-
integrationEvents: options.integrationEvents,
|
|
11547
|
-
mediaPipeline: await resolveMediaPipeline(sessionId),
|
|
11548
|
-
redact: options.redact,
|
|
11549
|
-
reviews: options.reviews,
|
|
11550
|
-
sessionId,
|
|
11551
|
-
store: options.store,
|
|
11552
|
-
tasks: options.tasks
|
|
11553
|
-
});
|
|
11554
|
-
const getSessionId = (params) => params.sessionId ?? "";
|
|
11555
|
-
routes.get(path, async ({ params }) => Response.json(await buildRecord(getSessionId(params))));
|
|
11556
|
-
const incidentHandler = async ({
|
|
11557
|
-
params
|
|
11558
|
-
}) => {
|
|
11559
|
-
const record = await buildRecord(getSessionId(params));
|
|
11560
|
-
const body = await (options.renderIncidentMarkdown ?? renderVoiceOperationsRecordIncidentMarkdown)(record);
|
|
11561
|
-
return new Response(body, {
|
|
11562
|
-
headers: {
|
|
11563
|
-
"Content-Type": "text/markdown; charset=utf-8",
|
|
11564
|
-
...options.headers
|
|
11565
|
-
}
|
|
11566
|
-
});
|
|
11567
|
-
};
|
|
11568
|
-
if (incidentPath) {
|
|
11569
|
-
routes.get(incidentPath, incidentHandler);
|
|
11570
|
-
}
|
|
11571
|
-
if (htmlPath) {
|
|
11572
|
-
routes.get(htmlPath, async ({ params }) => {
|
|
11573
|
-
const record = await buildRecord(getSessionId(params));
|
|
11574
|
-
const body = await (options.render ?? ((input) => renderVoiceOperationsRecordHTML(input, {
|
|
11575
|
-
incidentHref: incidentHtmlPath ? resolveRoutePath(incidentHtmlPath, input.sessionId) : undefined,
|
|
11576
|
-
title: options.title
|
|
11577
|
-
})))(record);
|
|
11578
|
-
return new Response(body, {
|
|
11579
|
-
headers: {
|
|
11580
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
11581
|
-
...options.headers
|
|
11582
|
-
}
|
|
11583
|
-
});
|
|
11584
|
-
});
|
|
11585
|
-
}
|
|
11586
|
-
if (incidentHtmlPath && incidentHtmlPath !== incidentPath) {
|
|
11587
|
-
routes.get(incidentHtmlPath, incidentHandler);
|
|
11588
|
-
}
|
|
11589
|
-
return routes;
|
|
11590
|
-
};
|
|
11591
11588
|
|
|
11592
11589
|
// src/telephony/twilio.ts
|
|
11593
11590
|
import { Buffer as Buffer3 } from "buffer";
|
|
@@ -11672,6 +11669,7 @@ var createMemoryVoiceTelephonyWebhookIdempotencyStore = () => {
|
|
|
11672
11669
|
var isTelephonyWebhookProvider = (value) => value === "generic" || value === "plivo" || value === "telnyx" || value === "twilio";
|
|
11673
11670
|
var isTelephonyOutcomeAction = (value) => value === "complete" || value === "escalate" || value === "ignore" || value === "no-answer" || value === "transfer" || value === "voicemail";
|
|
11674
11671
|
var isCallDisposition = (value) => value === "completed" || value === "escalated" || value === "failed" || value === "no-answer" || value === "transferred" || value === "voicemail";
|
|
11672
|
+
var assertVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => assertVoiceEvidence("Voice telephony webhook normalization evidence assertion failed", evaluateVoiceTelephonyWebhookNormalizationEvidence(input));
|
|
11675
11673
|
var evaluateVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
|
|
11676
11674
|
const issues = [];
|
|
11677
11675
|
const decisions = input.decisions ?? [];
|
|
@@ -11765,9 +11763,6 @@ var evaluateVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
|
|
|
11765
11763
|
verificationAttempts: verificationAttempts.length
|
|
11766
11764
|
};
|
|
11767
11765
|
};
|
|
11768
|
-
var assertVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
|
|
11769
|
-
return assertVoiceEvidence("Voice telephony webhook normalization evidence assertion failed", evaluateVoiceTelephonyWebhookNormalizationEvidence(input));
|
|
11770
|
-
};
|
|
11771
11766
|
var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
|
|
11772
11767
|
var firstString2 = (source, keys) => {
|
|
11773
11768
|
for (const key of keys) {
|
|
@@ -11921,6 +11916,45 @@ var buildDecision = (action, input) => ({
|
|
|
11921
11916
|
source: input.source,
|
|
11922
11917
|
target: action === "transfer" ? resolveTransferTarget(input.event, input.policy) : undefined
|
|
11923
11918
|
});
|
|
11919
|
+
var applyVoiceTelephonyOutcome = async (api, decision, result) => {
|
|
11920
|
+
switch (decision.action) {
|
|
11921
|
+
case "complete":
|
|
11922
|
+
await api.complete(result);
|
|
11923
|
+
break;
|
|
11924
|
+
case "escalate":
|
|
11925
|
+
await api.escalate({
|
|
11926
|
+
metadata: decision.metadata,
|
|
11927
|
+
reason: decision.reason ?? "telephony-escalation",
|
|
11928
|
+
result
|
|
11929
|
+
});
|
|
11930
|
+
break;
|
|
11931
|
+
case "no-answer":
|
|
11932
|
+
await api.markNoAnswer({
|
|
11933
|
+
metadata: decision.metadata,
|
|
11934
|
+
result
|
|
11935
|
+
});
|
|
11936
|
+
break;
|
|
11937
|
+
case "transfer":
|
|
11938
|
+
if (!decision.target) {
|
|
11939
|
+
return;
|
|
11940
|
+
}
|
|
11941
|
+
await api.transfer({
|
|
11942
|
+
metadata: decision.metadata,
|
|
11943
|
+
reason: decision.reason,
|
|
11944
|
+
result,
|
|
11945
|
+
target: decision.target
|
|
11946
|
+
});
|
|
11947
|
+
break;
|
|
11948
|
+
case "voicemail":
|
|
11949
|
+
await api.markVoicemail({
|
|
11950
|
+
metadata: decision.metadata,
|
|
11951
|
+
result
|
|
11952
|
+
});
|
|
11953
|
+
break;
|
|
11954
|
+
default:
|
|
11955
|
+
break;
|
|
11956
|
+
}
|
|
11957
|
+
};
|
|
11924
11958
|
var createVoiceTelephonyOutcomePolicy = (policy = {}) => ({
|
|
11925
11959
|
completedStatuses: policy.completedStatuses ?? DEFAULT_COMPLETED_STATUSES,
|
|
11926
11960
|
escalationStatuses: policy.escalationStatuses ?? DEFAULT_ESCALATION_STATUSES,
|
|
@@ -12033,48 +12067,9 @@ var voiceTelephonyOutcomeToRouteResult = (decision, result) => {
|
|
|
12033
12067
|
voicemail: {
|
|
12034
12068
|
metadata: decision.metadata
|
|
12035
12069
|
}
|
|
12036
|
-
};
|
|
12037
|
-
default:
|
|
12038
|
-
return { result };
|
|
12039
|
-
}
|
|
12040
|
-
};
|
|
12041
|
-
var applyVoiceTelephonyOutcome = async (api, decision, result) => {
|
|
12042
|
-
switch (decision.action) {
|
|
12043
|
-
case "complete":
|
|
12044
|
-
await api.complete(result);
|
|
12045
|
-
break;
|
|
12046
|
-
case "escalate":
|
|
12047
|
-
await api.escalate({
|
|
12048
|
-
metadata: decision.metadata,
|
|
12049
|
-
reason: decision.reason ?? "telephony-escalation",
|
|
12050
|
-
result
|
|
12051
|
-
});
|
|
12052
|
-
break;
|
|
12053
|
-
case "no-answer":
|
|
12054
|
-
await api.markNoAnswer({
|
|
12055
|
-
metadata: decision.metadata,
|
|
12056
|
-
result
|
|
12057
|
-
});
|
|
12058
|
-
break;
|
|
12059
|
-
case "transfer":
|
|
12060
|
-
if (!decision.target) {
|
|
12061
|
-
return;
|
|
12062
|
-
}
|
|
12063
|
-
await api.transfer({
|
|
12064
|
-
metadata: decision.metadata,
|
|
12065
|
-
reason: decision.reason,
|
|
12066
|
-
result,
|
|
12067
|
-
target: decision.target
|
|
12068
|
-
});
|
|
12069
|
-
break;
|
|
12070
|
-
case "voicemail":
|
|
12071
|
-
await api.markVoicemail({
|
|
12072
|
-
metadata: decision.metadata,
|
|
12073
|
-
result
|
|
12074
|
-
});
|
|
12075
|
-
break;
|
|
12070
|
+
};
|
|
12076
12071
|
default:
|
|
12077
|
-
|
|
12072
|
+
return { result };
|
|
12078
12073
|
}
|
|
12079
12074
|
};
|
|
12080
12075
|
var parseRequestBodyText = (input) => {
|
|
@@ -12694,11 +12689,6 @@ var encodeTwilioMulawBase64 = (samples) => {
|
|
|
12694
12689
|
}
|
|
12695
12690
|
return Buffer3.from(bytes).toString("base64");
|
|
12696
12691
|
};
|
|
12697
|
-
var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
|
|
12698
|
-
const narrowband = decodeTwilioMulawBase64(payload);
|
|
12699
|
-
const wideband = linearResample(narrowband, TWILIO_MULAW_SAMPLE_RATE, VOICE_PCM_SAMPLE_RATE);
|
|
12700
|
-
return int16ArrayToBytes(wideband);
|
|
12701
|
-
};
|
|
12702
12692
|
var transcodePCMToTwilioOutboundPayload = (chunk, format) => {
|
|
12703
12693
|
if (format.container === "raw" && format.encoding === "mulaw" && format.channels === 1 && format.sampleRateHz === TWILIO_MULAW_SAMPLE_RATE) {
|
|
12704
12694
|
return Buffer3.from(chunk).toString("base64");
|
|
@@ -12715,6 +12705,11 @@ var transcodePCMToTwilioOutboundPayload = (chunk, format) => {
|
|
|
12715
12705
|
const telephony = linearResample(mono, format.sampleRateHz, TWILIO_MULAW_SAMPLE_RATE);
|
|
12716
12706
|
return encodeTwilioMulawBase64(telephony);
|
|
12717
12707
|
};
|
|
12708
|
+
var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
|
|
12709
|
+
const narrowband = decodeTwilioMulawBase64(payload);
|
|
12710
|
+
const wideband = linearResample(narrowband, TWILIO_MULAW_SAMPLE_RATE, VOICE_PCM_SAMPLE_RATE);
|
|
12711
|
+
return int16ArrayToBytes(wideband);
|
|
12712
|
+
};
|
|
12718
12713
|
var parseTwilioMessage = (raw) => {
|
|
12719
12714
|
if (typeof raw !== "string") {
|
|
12720
12715
|
return raw;
|
|
@@ -12811,10 +12806,6 @@ var createTwilioSocketAdapter = (socket, getState) => ({
|
|
|
12811
12806
|
}
|
|
12812
12807
|
}
|
|
12813
12808
|
});
|
|
12814
|
-
var createTwilioVoiceResponse = (options) => {
|
|
12815
|
-
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
|
|
12816
|
-
return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${escapeXml2(options.streamUrl)}"${options.track ? ` track="${escapeXml2(options.track)}"` : ""}${options.streamName ? ` name="${escapeXml2(options.streamName)}"` : ""}>${parameters}</Stream></Connect></Response>`;
|
|
12817
|
-
};
|
|
12818
12809
|
var createTwilioMediaStreamBridge = (socket, options) => {
|
|
12819
12810
|
const runtimePreset = resolveVoiceRuntimePreset(options.preset);
|
|
12820
12811
|
const turnDetection = resolveTurnDetectionConfig({
|
|
@@ -12857,7 +12848,7 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
12857
12848
|
let reviewArtifactDelivered = false;
|
|
12858
12849
|
const telephonyMediaCarrier = bridgeState.carrier;
|
|
12859
12850
|
const appendTelephonyMediaTrace = async (message, override) => {
|
|
12860
|
-
const trace = options
|
|
12851
|
+
const { trace } = options;
|
|
12861
12852
|
const sessionId = override?.sessionId ?? bridgeState.sessionId ?? (message.event === "start" ? message.start.customParameters?.sessionId : undefined) ?? (message.event === "start" ? message.start.streamSid : "telephony-media");
|
|
12862
12853
|
const streamSid = override?.streamSid ?? (message.event === "start" ? message.start.streamSid : ("streamSid" in message) ? message.streamSid : undefined);
|
|
12863
12854
|
await trace?.append({
|
|
@@ -12909,15 +12900,15 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
12909
12900
|
onSession: options.onSession,
|
|
12910
12901
|
onTurn: async (input) => {
|
|
12911
12902
|
bridgeState.reviewRecorder?.recordVoiceMessage({
|
|
12912
|
-
|
|
12913
|
-
|
|
12903
|
+
turn: input.turn,
|
|
12904
|
+
type: "turn"
|
|
12914
12905
|
});
|
|
12915
12906
|
const result = await normalizedOnTurn(input);
|
|
12916
12907
|
if (result?.assistantText) {
|
|
12917
12908
|
bridgeState.reviewRecorder?.recordVoiceMessage({
|
|
12918
|
-
type: "assistant",
|
|
12919
12909
|
text: result.assistantText,
|
|
12920
|
-
turnId: input.turn.id
|
|
12910
|
+
turnId: input.turn.id,
|
|
12911
|
+
type: "assistant"
|
|
12921
12912
|
});
|
|
12922
12913
|
}
|
|
12923
12914
|
return result;
|
|
@@ -13036,11 +13027,14 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
13036
13027
|
streamSid: bridgeState.streamSid ?? undefined
|
|
13037
13028
|
});
|
|
13038
13029
|
await sessionHandle?.close("twilio-stop");
|
|
13039
|
-
return;
|
|
13040
13030
|
}
|
|
13041
13031
|
}
|
|
13042
13032
|
};
|
|
13043
13033
|
};
|
|
13034
|
+
var createTwilioVoiceResponse = (options) => {
|
|
13035
|
+
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
|
|
13036
|
+
return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${escapeXml2(options.streamUrl)}"${options.track ? ` track="${escapeXml2(options.track)}"` : ""}${options.streamName ? ` name="${escapeXml2(options.streamName)}"` : ""}>${parameters}</Stream></Connect></Response>`;
|
|
13037
|
+
};
|
|
13044
13038
|
var createTwilioVoiceRoutes = (options) => {
|
|
13045
13039
|
const streamPath = options.streamPath ?? "/api/voice/twilio/stream";
|
|
13046
13040
|
const twimlPath = options.twiml?.path ?? "/api/voice/twilio";
|
|
@@ -13275,8 +13269,8 @@ var createFakeSTTAdapter = (inputSpy, sttDelayMs) => ({
|
|
|
13275
13269
|
}
|
|
13276
13270
|
for (const handler of listeners.endOfTurn) {
|
|
13277
13271
|
handler({
|
|
13278
|
-
receivedAt,
|
|
13279
13272
|
reason: "vendor",
|
|
13273
|
+
receivedAt,
|
|
13280
13274
|
type: "endOfTurn"
|
|
13281
13275
|
});
|
|
13282
13276
|
}
|
|
@@ -13331,101 +13325,18 @@ var defaultOperationsRecordHref = ({
|
|
|
13331
13325
|
sessionId
|
|
13332
13326
|
}) => `/voice-operations/${encodeURIComponent(sessionId)}`;
|
|
13333
13327
|
var resolveOperationsRecordHref2 = (href, input) => typeof href === "function" ? href(input) : href === undefined ? defaultOperationsRecordHref(input) : href;
|
|
13334
|
-
var
|
|
13335
|
-
|
|
13336
|
-
const
|
|
13337
|
-
const
|
|
13338
|
-
|
|
13339
|
-
|
|
13340
|
-
const trace = options.store ?? createVoiceMemoryTraceEventStore();
|
|
13341
|
-
const sentEvents = [];
|
|
13342
|
-
const receivedAudio = [];
|
|
13343
|
-
const bridge = createTwilioMediaStreamBridge({
|
|
13344
|
-
close: () => {},
|
|
13345
|
-
send: (data) => {
|
|
13346
|
-
sentEvents.push(JSON.parse(data));
|
|
13347
|
-
}
|
|
13348
|
-
}, {
|
|
13349
|
-
context: {},
|
|
13350
|
-
onComplete: async () => {},
|
|
13351
|
-
onTurn: async () => ({
|
|
13352
|
-
assistantText: "Confirmed. Two way carrier media is visible."
|
|
13353
|
-
}),
|
|
13354
|
-
session: createVoiceMemoryStore(),
|
|
13355
|
-
stt: createFakeSTTAdapter(receivedAudio, 0),
|
|
13356
|
-
trace,
|
|
13357
|
-
tts: createFakeTTSAdapter(1, 0),
|
|
13358
|
-
turnDetection: {
|
|
13359
|
-
transcriptStabilityMs: 0
|
|
13360
|
-
}
|
|
13361
|
-
});
|
|
13362
|
-
const payload = encodeTwilioMulawBase64(new Int16Array([500, -500, 1500, -1500, 2500, -2500]));
|
|
13363
|
-
await bridge.handleMessage({
|
|
13364
|
-
event: "start",
|
|
13365
|
-
start: {
|
|
13366
|
-
callSid,
|
|
13367
|
-
customParameters: {
|
|
13368
|
-
scenarioId,
|
|
13369
|
-
sessionId
|
|
13370
|
-
},
|
|
13371
|
-
streamSid
|
|
13372
|
-
},
|
|
13373
|
-
streamSid
|
|
13374
|
-
});
|
|
13375
|
-
await bridge.handleMessage({
|
|
13376
|
-
event: "media",
|
|
13377
|
-
media: {
|
|
13378
|
-
payload,
|
|
13379
|
-
track: "inbound"
|
|
13380
|
-
},
|
|
13381
|
-
streamSid
|
|
13382
|
-
});
|
|
13383
|
-
await waitFor(() => sentEvents.some((message) => message.event === "media"), timeoutMs);
|
|
13384
|
-
await bridge.handleMessage({
|
|
13385
|
-
event: "media",
|
|
13386
|
-
media: {
|
|
13387
|
-
payload,
|
|
13388
|
-
track: "inbound"
|
|
13389
|
-
},
|
|
13390
|
-
streamSid
|
|
13391
|
-
});
|
|
13392
|
-
await waitFor(() => sentEvents.some((message) => message.event === "clear"), timeoutMs);
|
|
13393
|
-
await bridge.handleMessage({
|
|
13394
|
-
event: "stop",
|
|
13395
|
-
stop: {
|
|
13396
|
-
callSid
|
|
13397
|
-
},
|
|
13398
|
-
streamSid
|
|
13399
|
-
});
|
|
13400
|
-
await bridge.close("telephony-media-operations-smoke-complete");
|
|
13401
|
-
const operationsRecord = await buildVoiceOperationsRecord({
|
|
13402
|
-
sessionId,
|
|
13403
|
-
store: trace
|
|
13404
|
-
});
|
|
13405
|
-
const media = operationsRecord.telephonyMedia;
|
|
13406
|
-
const issues = [
|
|
13407
|
-
media.starts < 1 ? "Missing telephony media start event." : undefined,
|
|
13408
|
-
media.stops < 1 ? "Missing telephony media stop event." : undefined,
|
|
13409
|
-
media.inbound < 2 ? "Expected at least two inbound telephony media events." : undefined,
|
|
13410
|
-
media.outbound < 2 ? "Expected outbound assistant media/control evidence." : undefined,
|
|
13411
|
-
media.media < 3 ? "Expected inbound and outbound telephony media packet evidence." : undefined,
|
|
13412
|
-
media.clears < 1 ? "Missing outbound clear evidence." : undefined,
|
|
13413
|
-
media.audioBytes <= 0 ? "Missing telephony media audio bytes." : undefined,
|
|
13414
|
-
media.carriers.includes("twilio") ? undefined : "Missing Twilio carrier evidence.",
|
|
13415
|
-
media.streamIds.includes(streamSid) ? undefined : "Missing telephony media stream ID evidence."
|
|
13416
|
-
].filter((issue) => typeof issue === "string");
|
|
13328
|
+
var getDefaultVoiceTelephonyBenchmarkScenarios = () => DEFAULT_SCENARIOS2.map((scenario) => ({ ...scenario }));
|
|
13329
|
+
var runVoiceTelephonyBenchmark = async (scenarios = getDefaultVoiceTelephonyBenchmarkScenarios(), options = {}) => {
|
|
13330
|
+
const fixtures = [];
|
|
13331
|
+
for (const scenario of scenarios) {
|
|
13332
|
+
fixtures.push(await runVoiceTelephonyBenchmarkScenario(scenario, options));
|
|
13333
|
+
}
|
|
13417
13334
|
return {
|
|
13418
|
-
|
|
13419
|
-
|
|
13420
|
-
|
|
13421
|
-
operationsRecordHref: resolveOperationsRecordHref2(options.operationsRecordHref, { sessionId, streamSid }),
|
|
13422
|
-
sentEvents: sentEvents.map((message) => message.event).filter((event) => typeof event === "string"),
|
|
13423
|
-
sessionId,
|
|
13424
|
-
streamSid,
|
|
13425
|
-
telephonyMedia: media
|
|
13335
|
+
fixtures,
|
|
13336
|
+
generatedAt: Date.now(),
|
|
13337
|
+
summary: summarizeVoiceTelephonyBenchmark(fixtures)
|
|
13426
13338
|
};
|
|
13427
13339
|
};
|
|
13428
|
-
var getDefaultVoiceTelephonyBenchmarkScenarios = () => DEFAULT_SCENARIOS2.map((scenario) => ({ ...scenario }));
|
|
13429
13340
|
var runVoiceTelephonyBenchmarkScenario = async (scenario, options = {}) => {
|
|
13430
13341
|
const timeoutMs = options.timeoutMs ?? 1000;
|
|
13431
13342
|
const sentEvents = [];
|
|
@@ -13517,6 +13428,100 @@ var runVoiceTelephonyBenchmarkScenario = async (scenario, options = {}) => {
|
|
|
13517
13428
|
title: scenario.title
|
|
13518
13429
|
};
|
|
13519
13430
|
};
|
|
13431
|
+
var runVoiceTelephonyMediaOperationsSmoke = async (options = {}) => {
|
|
13432
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
13433
|
+
const sessionId = options.sessionId ?? `telephony-media-ops-${Date.now()}`;
|
|
13434
|
+
const streamSid = options.streamSid ?? "telephony-media-ops-stream";
|
|
13435
|
+
const callSid = options.callSid ?? "telephony-media-ops-call";
|
|
13436
|
+
const scenarioId = options.scenarioId ?? "telephony-media-operations-smoke";
|
|
13437
|
+
const trace = options.store ?? createVoiceMemoryTraceEventStore();
|
|
13438
|
+
const sentEvents = [];
|
|
13439
|
+
const receivedAudio = [];
|
|
13440
|
+
const bridge = createTwilioMediaStreamBridge({
|
|
13441
|
+
close: () => {},
|
|
13442
|
+
send: (data) => {
|
|
13443
|
+
sentEvents.push(JSON.parse(data));
|
|
13444
|
+
}
|
|
13445
|
+
}, {
|
|
13446
|
+
context: {},
|
|
13447
|
+
onComplete: async () => {},
|
|
13448
|
+
onTurn: async () => ({
|
|
13449
|
+
assistantText: "Confirmed. Two way carrier media is visible."
|
|
13450
|
+
}),
|
|
13451
|
+
session: createVoiceMemoryStore(),
|
|
13452
|
+
stt: createFakeSTTAdapter(receivedAudio, 0),
|
|
13453
|
+
trace,
|
|
13454
|
+
tts: createFakeTTSAdapter(1, 0),
|
|
13455
|
+
turnDetection: {
|
|
13456
|
+
transcriptStabilityMs: 0
|
|
13457
|
+
}
|
|
13458
|
+
});
|
|
13459
|
+
const payload = encodeTwilioMulawBase64(new Int16Array([500, -500, 1500, -1500, 2500, -2500]));
|
|
13460
|
+
await bridge.handleMessage({
|
|
13461
|
+
event: "start",
|
|
13462
|
+
start: {
|
|
13463
|
+
callSid,
|
|
13464
|
+
customParameters: {
|
|
13465
|
+
scenarioId,
|
|
13466
|
+
sessionId
|
|
13467
|
+
},
|
|
13468
|
+
streamSid
|
|
13469
|
+
},
|
|
13470
|
+
streamSid
|
|
13471
|
+
});
|
|
13472
|
+
await bridge.handleMessage({
|
|
13473
|
+
event: "media",
|
|
13474
|
+
media: {
|
|
13475
|
+
payload,
|
|
13476
|
+
track: "inbound"
|
|
13477
|
+
},
|
|
13478
|
+
streamSid
|
|
13479
|
+
});
|
|
13480
|
+
await waitFor(() => sentEvents.some((message) => message.event === "media"), timeoutMs);
|
|
13481
|
+
await bridge.handleMessage({
|
|
13482
|
+
event: "media",
|
|
13483
|
+
media: {
|
|
13484
|
+
payload,
|
|
13485
|
+
track: "inbound"
|
|
13486
|
+
},
|
|
13487
|
+
streamSid
|
|
13488
|
+
});
|
|
13489
|
+
await waitFor(() => sentEvents.some((message) => message.event === "clear"), timeoutMs);
|
|
13490
|
+
await bridge.handleMessage({
|
|
13491
|
+
event: "stop",
|
|
13492
|
+
stop: {
|
|
13493
|
+
callSid
|
|
13494
|
+
},
|
|
13495
|
+
streamSid
|
|
13496
|
+
});
|
|
13497
|
+
await bridge.close("telephony-media-operations-smoke-complete");
|
|
13498
|
+
const operationsRecord = await buildVoiceOperationsRecord({
|
|
13499
|
+
sessionId,
|
|
13500
|
+
store: trace
|
|
13501
|
+
});
|
|
13502
|
+
const media = operationsRecord.telephonyMedia;
|
|
13503
|
+
const issues = [
|
|
13504
|
+
media.starts < 1 ? "Missing telephony media start event." : undefined,
|
|
13505
|
+
media.stops < 1 ? "Missing telephony media stop event." : undefined,
|
|
13506
|
+
media.inbound < 2 ? "Expected at least two inbound telephony media events." : undefined,
|
|
13507
|
+
media.outbound < 2 ? "Expected outbound assistant media/control evidence." : undefined,
|
|
13508
|
+
media.media < 3 ? "Expected inbound and outbound telephony media packet evidence." : undefined,
|
|
13509
|
+
media.clears < 1 ? "Missing outbound clear evidence." : undefined,
|
|
13510
|
+
media.audioBytes <= 0 ? "Missing telephony media audio bytes." : undefined,
|
|
13511
|
+
media.carriers.includes("twilio") ? undefined : "Missing Twilio carrier evidence.",
|
|
13512
|
+
media.streamIds.includes(streamSid) ? undefined : "Missing telephony media stream ID evidence."
|
|
13513
|
+
].filter((issue) => typeof issue === "string");
|
|
13514
|
+
return {
|
|
13515
|
+
issues,
|
|
13516
|
+
ok: issues.length === 0,
|
|
13517
|
+
operationsRecord,
|
|
13518
|
+
operationsRecordHref: resolveOperationsRecordHref2(options.operationsRecordHref, { sessionId, streamSid }),
|
|
13519
|
+
sentEvents: sentEvents.map((message) => message.event).filter((event) => typeof event === "string"),
|
|
13520
|
+
sessionId,
|
|
13521
|
+
streamSid,
|
|
13522
|
+
telephonyMedia: media
|
|
13523
|
+
};
|
|
13524
|
+
};
|
|
13520
13525
|
var summarizeVoiceTelephonyBenchmark = (fixtures) => {
|
|
13521
13526
|
const scenarioCount = fixtures.length;
|
|
13522
13527
|
const firstMediaSamples = fixtures.filter((fixture) => typeof fixture.firstOutboundMediaLatencyMs === "number");
|
|
@@ -13534,17 +13539,6 @@ var summarizeVoiceTelephonyBenchmark = (fixtures) => {
|
|
|
13534
13539
|
totalOutboundMediaCount: fixtures.reduce((sum, fixture) => sum + fixture.outboundMediaCount, 0)
|
|
13535
13540
|
};
|
|
13536
13541
|
};
|
|
13537
|
-
var runVoiceTelephonyBenchmark = async (scenarios = getDefaultVoiceTelephonyBenchmarkScenarios(), options = {}) => {
|
|
13538
|
-
const fixtures = [];
|
|
13539
|
-
for (const scenario of scenarios) {
|
|
13540
|
-
fixtures.push(await runVoiceTelephonyBenchmarkScenario(scenario, options));
|
|
13541
|
-
}
|
|
13542
|
-
return {
|
|
13543
|
-
fixtures,
|
|
13544
|
-
generatedAt: Date.now(),
|
|
13545
|
-
summary: summarizeVoiceTelephonyBenchmark(fixtures)
|
|
13546
|
-
};
|
|
13547
|
-
};
|
|
13548
13542
|
// src/testing/tts.ts
|
|
13549
13543
|
var DEFAULT_REALTIME_FORMAT2 = {
|
|
13550
13544
|
channels: 1,
|
|
@@ -13579,6 +13573,19 @@ var getAudioDurationMs = (chunk, format) => {
|
|
|
13579
13573
|
return chunk.byteLength / (format.sampleRateHz * format.channels * 2) * 1000;
|
|
13580
13574
|
};
|
|
13581
13575
|
var getDefaultTTSBenchmarkFixtures = () => DEFAULT_TTS_BENCHMARK_FIXTURES.map((fixture) => ({ ...fixture }));
|
|
13576
|
+
var runTTSAdapterBenchmark = async (adapterId, adapter, fixtures = getDefaultTTSBenchmarkFixtures(), options = {}) => {
|
|
13577
|
+
const results = [];
|
|
13578
|
+
for (const fixture of fixtures) {
|
|
13579
|
+
results.push(await runTTSAdapterFixture(adapter, fixture, options));
|
|
13580
|
+
}
|
|
13581
|
+
return {
|
|
13582
|
+
adapterId,
|
|
13583
|
+
fixtures: results,
|
|
13584
|
+
generatedAt: Date.now(),
|
|
13585
|
+
profileId: options.interruptAfterFirstAudioMs !== undefined ? "interrupt" : "default",
|
|
13586
|
+
summary: summarizeTTSBenchmark(adapterId, results)
|
|
13587
|
+
};
|
|
13588
|
+
};
|
|
13582
13589
|
var runTTSAdapterFixture = async (adapter, fixture, options = {}) => {
|
|
13583
13590
|
const openOptions = typeof options.openOptions === "function" ? options.openOptions(fixture) : options.openOptions;
|
|
13584
13591
|
const settleMs = options.settleMs ?? 300;
|
|
@@ -13702,19 +13709,6 @@ var runTTSAdapterFixture = async (adapter, fixture, options = {}) => {
|
|
|
13702
13709
|
totalAudioBytes
|
|
13703
13710
|
};
|
|
13704
13711
|
};
|
|
13705
|
-
var runTTSAdapterBenchmark = async (adapterId, adapter, fixtures = getDefaultTTSBenchmarkFixtures(), options = {}) => {
|
|
13706
|
-
const results = [];
|
|
13707
|
-
for (const fixture of fixtures) {
|
|
13708
|
-
results.push(await runTTSAdapterFixture(adapter, fixture, options));
|
|
13709
|
-
}
|
|
13710
|
-
return {
|
|
13711
|
-
adapterId,
|
|
13712
|
-
fixtures: results,
|
|
13713
|
-
generatedAt: Date.now(),
|
|
13714
|
-
profileId: options.interruptAfterFirstAudioMs !== undefined ? "interrupt" : "default",
|
|
13715
|
-
summary: summarizeTTSBenchmark(adapterId, results)
|
|
13716
|
-
};
|
|
13717
|
-
};
|
|
13718
13712
|
var summarizeTTSBenchmark = (adapterId, fixtures) => {
|
|
13719
13713
|
const fixtureCount = fixtures.length;
|
|
13720
13714
|
const totalAudioBytes = fixtures.reduce((sum, fixture) => sum + fixture.totalAudioBytes, 0);
|