@absolutejs/voice 0.0.22-beta.29 → 0.0.22-beta.30

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.
@@ -0,0 +1,94 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { StoredVoiceTraceEvent, VoiceTraceEventStore } from './trace';
3
+ export type VoiceHandoffHealthStatus = 'delivered' | 'failed' | 'skipped';
4
+ export type VoiceHandoffHealthDelivery = {
5
+ adapterId: string;
6
+ adapterKind?: string;
7
+ deliveredAt?: number;
8
+ deliveredTo?: string;
9
+ error?: string;
10
+ status: VoiceHandoffHealthStatus;
11
+ };
12
+ export type VoiceHandoffHealthEvent = {
13
+ action?: string;
14
+ at: number;
15
+ deliveries: VoiceHandoffHealthDelivery[];
16
+ reason?: string;
17
+ replayHref?: string;
18
+ sessionId: string;
19
+ status: VoiceHandoffHealthStatus;
20
+ target?: string;
21
+ };
22
+ export type VoiceHandoffHealthSummary = {
23
+ byAction: Record<string, number>;
24
+ byAdapter: Record<string, Record<VoiceHandoffHealthStatus, number>>;
25
+ byStatus: Record<VoiceHandoffHealthStatus, number>;
26
+ events: VoiceHandoffHealthEvent[];
27
+ failed: number;
28
+ total: number;
29
+ };
30
+ export type VoiceHandoffHealthSummaryOptions = {
31
+ events?: StoredVoiceTraceEvent[];
32
+ limit?: number;
33
+ q?: string;
34
+ replayHref?: false | string | ((event: Omit<VoiceHandoffHealthEvent, 'replayHref'>) => string);
35
+ status?: VoiceHandoffHealthStatus | 'all';
36
+ store?: VoiceTraceEventStore;
37
+ };
38
+ export type VoiceHandoffHealthHTMLHandlerOptions = VoiceHandoffHealthSummaryOptions & {
39
+ headers?: HeadersInit;
40
+ render?: (summary: VoiceHandoffHealthSummary) => string | Promise<string>;
41
+ };
42
+ export type VoiceHandoffHealthRoutesOptions = VoiceHandoffHealthHTMLHandlerOptions & {
43
+ htmlPath?: false | string;
44
+ name?: string;
45
+ path?: string;
46
+ };
47
+ export declare const summarizeVoiceHandoffHealth: (options?: VoiceHandoffHealthSummaryOptions) => Promise<VoiceHandoffHealthSummary>;
48
+ export declare const renderVoiceHandoffHealthHTML: (summary: VoiceHandoffHealthSummary) => string;
49
+ export declare const createVoiceHandoffHealthJSONHandler: (options?: VoiceHandoffHealthSummaryOptions) => ({ query }: {
50
+ query?: Record<string, string | undefined>;
51
+ }) => Promise<VoiceHandoffHealthSummary>;
52
+ export declare const createVoiceHandoffHealthHTMLHandler: (options?: VoiceHandoffHealthHTMLHandlerOptions) => ({ query }: {
53
+ query?: Record<string, string | undefined>;
54
+ }) => Promise<Response>;
55
+ export declare const createVoiceHandoffHealthRoutes: (options?: VoiceHandoffHealthRoutesOptions) => Elysia<"", {
56
+ decorator: {};
57
+ store: {};
58
+ derive: {};
59
+ resolve: {};
60
+ }, {
61
+ typebox: {};
62
+ error: {};
63
+ }, {
64
+ schema: {};
65
+ standaloneSchema: {};
66
+ macro: {};
67
+ macroFn: {};
68
+ parser: {};
69
+ response: {};
70
+ }, {
71
+ [x: string]: {
72
+ get: {
73
+ body: unknown;
74
+ params: {};
75
+ query: unknown;
76
+ headers: unknown;
77
+ response: {
78
+ 200: VoiceHandoffHealthSummary;
79
+ };
80
+ };
81
+ };
82
+ }, {
83
+ derive: {};
84
+ resolve: {};
85
+ schema: {};
86
+ standaloneSchema: {};
87
+ response: {};
88
+ }, {
89
+ derive: {};
90
+ resolve: {};
91
+ schema: {};
92
+ standaloneSchema: {};
93
+ response: {};
94
+ }>;
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export { createVoiceMemoryStore } from './memoryStore';
15
15
  export { createVoiceCRMActivitySink, createVoiceHelpdeskTicketSink, createVoiceIntegrationHTTPSink, createVoiceHubSpotTaskSink, createVoiceHubSpotTaskSyncSinks, createVoiceHubSpotTaskUpdateSink, createVoiceLinearIssueSink, createVoiceLinearIssueSyncSinks, createVoiceLinearIssueUpdateSink, createVoiceZendeskTicketSink, createVoiceZendeskTicketSyncSinks, createVoiceZendeskTicketUpdateSink, deliverVoiceIntegrationEventToSinks } from './opsSinks';
16
16
  export { createVoiceOpsWebhookEnvelope, createVoiceOpsWebhookReceiverRoutes, createVoiceOpsWebhookSink, verifyVoiceOpsWebhookSignature } from './opsWebhook';
17
17
  export { createVoiceTwilioRedirectHandoffAdapter, createVoiceWebhookHandoffAdapter, deliverVoiceHandoff } from './handoff';
18
+ export { createVoiceHandoffHealthHTMLHandler, createVoiceHandoffHealthJSONHandler, createVoiceHandoffHealthRoutes, renderVoiceHandoffHealthHTML, summarizeVoiceHandoffHealth } from './handoffHealth';
18
19
  export { createVoiceIntegrationSinkWorker, createVoiceIntegrationSinkWorkerLoop, createVoiceOpsTaskWorker, createVoiceOpsTaskProcessorWorker, createVoiceOpsTaskProcessorWorkerLoop, createVoiceRedisIdempotencyStore, createVoiceRedisTaskLeaseCoordinator, createVoiceTraceSinkDeliveryWorker, createVoiceTraceSinkDeliveryWorkerLoop, createVoiceWebhookDeliveryWorker, createVoiceWebhookDeliveryWorkerLoop, summarizeVoiceTraceSinkDeliveries, summarizeVoiceOpsTaskQueue, summarizeVoiceIntegrationEvents } from './queue';
19
20
  export { assignVoiceOpsTask, applyVoiceOpsTaskAssignmentRule, applyVoiceOpsTaskPolicy, buildVoiceOpsTaskFromReview, buildVoiceOpsTaskFromSLABreach, claimVoiceOpsTask, completeVoiceOpsTask, createVoiceExternalObjectMap, createVoiceExternalObjectMapId, createVoiceCallCompletedEvent, createVoiceTaskSLABreachedEvent, deadLetterVoiceOpsTask, deliverVoiceIntegrationEvent, failVoiceOpsTask, hasVoiceOpsTaskSLABreach, heartbeatVoiceOpsTask, isVoiceOpsTaskOverdue, markVoiceOpsTaskSLABreached, matchesVoiceOpsTaskAssignmentRule, resolveVoiceOpsTaskAgeBucket, createVoiceIntegrationEvent, createVoiceReviewSavedEvent, resolveVoiceOpsTaskAssignment, resolveVoiceOpsTaskPolicy, requeueVoiceOpsTask, createVoiceTaskCreatedEvent, createVoiceTaskUpdatedEvent, listVoiceOpsTasks, reopenVoiceOpsTask, startVoiceOpsTask, summarizeVoiceOpsTaskAnalytics, summarizeVoiceOpsTasks, withVoiceIntegrationEventId, withVoiceOpsTaskId } from './ops';
20
21
  export { createVoiceSession } from './session';
@@ -42,6 +43,7 @@ export type { VoiceOutcomeRecipe, VoiceOutcomeRecipeName, VoiceOutcomeRecipeOpti
42
43
  export type { VoiceCRMActivitySinkOptions, VoiceHubSpotTaskSinkOptions, VoiceHubSpotTaskUpdateSinkOptions, VoiceHelpdeskTicketSinkOptions, VoiceIntegrationHTTPSinkOptions, VoiceIntegrationSink, VoiceIntegrationSinkDeliveryResult, VoiceLinearIssueSinkOptions, VoiceLinearIssueUpdateSinkOptions, VoiceZendeskTicketSinkOptions, VoiceZendeskTicketUpdateSinkOptions } from './opsSinks';
43
44
  export type { VoiceOpsWebhookEnvelope, VoiceOpsWebhookEntity, VoiceOpsWebhookLinkResolver, VoiceOpsWebhookReceiverRoutesOptions, VoiceOpsWebhookSinkOptions, VoiceOpsWebhookVerificationResult } from './opsWebhook';
44
45
  export type { VoiceHandoffDelivery, VoiceHandoffFanoutResult, VoiceTwilioRedirectHandoffAdapterOptions, VoiceWebhookHandoffAdapterOptions } from './handoff';
46
+ export type { VoiceHandoffHealthDelivery, VoiceHandoffHealthEvent, VoiceHandoffHealthHTMLHandlerOptions, VoiceHandoffHealthRoutesOptions, VoiceHandoffHealthStatus, VoiceHandoffHealthSummary, VoiceHandoffHealthSummaryOptions } from './handoffHealth';
45
47
  export type { StoredVoiceCallReviewArtifact, VoiceCallReviewArtifact, VoiceCallReviewConfig, VoiceCallReviewPostCallSummary, VoiceCallReviewRecorder, VoiceCallReviewRecorderOptions, VoiceCallReviewStore, VoiceCallReviewSummary, VoiceCallReviewTimelineEvent } from './testing/review';
46
48
  export type { VoiceFileRuntimeStorage, VoiceFileStoreOptions } from './fileStore';
47
49
  export type { StoredVoiceTraceEvent, VoiceTraceEvaluation, VoiceTraceEvaluationOptions, VoiceTraceEvent, VoiceTraceEventFilter, VoiceTraceEventStore, VoiceTraceEventType, VoiceTraceIssue, VoiceTraceIssueSeverity, VoiceTraceHTTPSinkOptions, VoiceTracePruneFilter, VoiceTracePruneOptions, VoiceTracePruneResult, VoiceTraceRedactionConfig, VoiceTraceRedactionOptions, VoiceTraceRedactionReplacement, VoiceResolvedTraceRedactionOptions, VoiceTraceSink, VoiceTraceSinkDeliveryQueueStatus, VoiceTraceSinkDeliveryRecord, VoiceTraceSinkDeliveryResult, VoiceTraceSinkDeliveryStatus, VoiceTraceSinkDeliveryStore, VoiceTraceSinkFanoutResult, VoiceTraceSinkStoreOptions, VoiceTraceSummary } from './trace';
package/dist/index.js CHANGED
@@ -9362,6 +9362,198 @@ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
9362
9362
  parse: "text"
9363
9363
  });
9364
9364
  };
9365
+ // src/handoffHealth.ts
9366
+ import { Elysia as Elysia6 } from "elysia";
9367
+ var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9368
+ var getString4 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
9369
+ var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
9370
+ var increment3 = (record, key) => {
9371
+ record[key] = (record[key] ?? 0) + 1;
9372
+ };
9373
+ var normalizeDelivery = (adapterId, value) => {
9374
+ const record = value && typeof value === "object" ? value : {};
9375
+ return {
9376
+ adapterId: getString4(record.adapterId) ?? adapterId,
9377
+ adapterKind: getString4(record.adapterKind),
9378
+ deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
9379
+ deliveredTo: getString4(record.deliveredTo),
9380
+ error: getString4(record.error),
9381
+ status: isStatus(record.status) ? record.status : "failed"
9382
+ };
9383
+ };
9384
+ var normalizeDeliveries = (payload) => {
9385
+ const deliveries = payload.deliveries;
9386
+ if (!deliveries || typeof deliveries !== "object") {
9387
+ return [];
9388
+ }
9389
+ return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
9390
+ };
9391
+ var resolveReplayHref = (event, replayHref) => {
9392
+ if (replayHref === false) {
9393
+ return;
9394
+ }
9395
+ if (typeof replayHref === "function") {
9396
+ return replayHref(event);
9397
+ }
9398
+ return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
9399
+ };
9400
+ var summarizeVoiceHandoffHealth = async (options = {}) => {
9401
+ const sourceEvents = options.events ?? await options.store?.list() ?? [];
9402
+ const search = options.q?.trim().toLowerCase();
9403
+ const byAction = {};
9404
+ const byAdapter = {};
9405
+ const byStatus = {
9406
+ delivered: 0,
9407
+ failed: 0,
9408
+ skipped: 0
9409
+ };
9410
+ const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
9411
+ const status = isStatus(event.payload.status) ? event.payload.status : "failed";
9412
+ const deliveries = normalizeDeliveries(event.payload);
9413
+ const item = {
9414
+ action: getString4(event.payload.action),
9415
+ at: event.at,
9416
+ deliveries,
9417
+ reason: getString4(event.payload.reason),
9418
+ sessionId: event.sessionId,
9419
+ status,
9420
+ target: getString4(event.payload.target)
9421
+ };
9422
+ return {
9423
+ ...item,
9424
+ replayHref: resolveReplayHref(item, options.replayHref)
9425
+ };
9426
+ }).filter((event) => {
9427
+ if (options.status && options.status !== "all" && event.status !== options.status) {
9428
+ return false;
9429
+ }
9430
+ if (!search) {
9431
+ return true;
9432
+ }
9433
+ return [
9434
+ event.action,
9435
+ event.reason,
9436
+ event.sessionId,
9437
+ event.status,
9438
+ event.target,
9439
+ ...event.deliveries.flatMap((delivery) => [
9440
+ delivery.adapterId,
9441
+ delivery.adapterKind,
9442
+ delivery.deliveredTo,
9443
+ delivery.error,
9444
+ delivery.status
9445
+ ])
9446
+ ].some((value) => value?.toLowerCase().includes(search));
9447
+ }).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
9448
+ for (const event of events) {
9449
+ byStatus[event.status] += 1;
9450
+ if (event.action) {
9451
+ increment3(byAction, event.action);
9452
+ }
9453
+ for (const delivery of event.deliveries) {
9454
+ byAdapter[delivery.adapterId] ??= {
9455
+ delivered: 0,
9456
+ failed: 0,
9457
+ skipped: 0
9458
+ };
9459
+ byAdapter[delivery.adapterId][delivery.status] += 1;
9460
+ }
9461
+ }
9462
+ return {
9463
+ byAction,
9464
+ byAdapter,
9465
+ byStatus,
9466
+ events,
9467
+ failed: byStatus.failed,
9468
+ total: events.length
9469
+ };
9470
+ };
9471
+ var renderMetricGrid = (summary) => [
9472
+ '<section class="voice-handoff-health-grid">',
9473
+ `<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
9474
+ `<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
9475
+ `<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
9476
+ `<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
9477
+ "</section>"
9478
+ ].join("");
9479
+ var renderActionSummary = (summary) => {
9480
+ const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
9481
+ const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
9482
+ return [
9483
+ '<section class="voice-handoff-health-columns">',
9484
+ "<article><h3>Actions</h3>",
9485
+ actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml7(action)}: ${String(count)}</li>`).join("")}</ul>`,
9486
+ "</article>",
9487
+ "<article><h3>Adapters</h3>",
9488
+ adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml7(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
9489
+ "</article>",
9490
+ "</section>"
9491
+ ].join("");
9492
+ };
9493
+ var renderVoiceHandoffHealthHTML = (summary) => [
9494
+ '<div class="voice-handoff-health">',
9495
+ renderMetricGrid(summary),
9496
+ renderActionSummary(summary),
9497
+ "<section>",
9498
+ "<h3>Recent Handoffs</h3>",
9499
+ summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
9500
+ '<div class="voice-handoff-health-events">',
9501
+ ...summary.events.map((event) => [
9502
+ `<article class="${escapeHtml7(event.status)}">`,
9503
+ '<div class="voice-handoff-health-event-header">',
9504
+ `<strong>${escapeHtml7(event.action ?? "handoff")}</strong>`,
9505
+ `<span>${escapeHtml7(event.status)}</span>`,
9506
+ "</div>",
9507
+ `<p><small>${escapeHtml7(event.sessionId)}</small></p>`,
9508
+ event.target ? `<p>Target: ${escapeHtml7(event.target)}</p>` : "",
9509
+ event.reason ? `<p>Reason: ${escapeHtml7(event.reason)}</p>` : "",
9510
+ event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
9511
+ "<li>",
9512
+ `${escapeHtml7(delivery.adapterId)}: ${escapeHtml7(delivery.status)}`,
9513
+ delivery.deliveredTo ? ` to ${escapeHtml7(delivery.deliveredTo)}` : "",
9514
+ delivery.error ? ` (${escapeHtml7(delivery.error)})` : "",
9515
+ "</li>"
9516
+ ].join("")).join("")}</ul>` : "",
9517
+ event.replayHref ? `<p><a href="${escapeHtml7(event.replayHref)}">Open replay</a></p>` : "",
9518
+ "</article>"
9519
+ ].join("")),
9520
+ "</div>"
9521
+ ].join(""),
9522
+ "</section>",
9523
+ "</div>"
9524
+ ].join("");
9525
+ var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
9526
+ ...options,
9527
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
9528
+ q: query?.q ?? options.q,
9529
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
9530
+ });
9531
+ var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
9532
+ const summary = await summarizeVoiceHandoffHealth({
9533
+ ...options,
9534
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
9535
+ q: query?.q ?? options.q,
9536
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
9537
+ });
9538
+ const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
9539
+ return new Response(body, {
9540
+ headers: {
9541
+ "Content-Type": "text/html; charset=utf-8",
9542
+ ...options.headers
9543
+ }
9544
+ });
9545
+ };
9546
+ var createVoiceHandoffHealthRoutes = (options = {}) => {
9547
+ const path = options.path ?? "/api/voice-handoffs";
9548
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
9549
+ const routes = new Elysia6({
9550
+ name: options.name ?? "absolutejs-voice-handoff-health"
9551
+ }).get(path, createVoiceHandoffHealthJSONHandler(options));
9552
+ if (htmlPath) {
9553
+ routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
9554
+ }
9555
+ return routes;
9556
+ };
9365
9557
  // src/queue.ts
9366
9558
  var releaseLeaseScript = `
9367
9559
  if redis.call("GET", KEYS[1]) == ARGV[1] then
@@ -11339,6 +11531,7 @@ export {
11339
11531
  summarizeVoiceOpsTaskQueue,
11340
11532
  summarizeVoiceOpsTaskAnalytics,
11341
11533
  summarizeVoiceIntegrationEvents,
11534
+ summarizeVoiceHandoffHealth,
11342
11535
  summarizeVoiceAssistantRuns,
11343
11536
  summarizeVoiceAssistantHealth,
11344
11537
  startVoiceOpsTask,
@@ -11361,6 +11554,7 @@ export {
11361
11554
  renderVoiceTraceHTML,
11362
11555
  renderVoiceSessionsHTML,
11363
11556
  renderVoiceProviderHealthHTML,
11557
+ renderVoiceHandoffHealthHTML,
11364
11558
  renderVoiceCallReviewMarkdown,
11365
11559
  renderVoiceCallReviewHTML,
11366
11560
  renderVoiceAssistantHealthHTML,
@@ -11459,6 +11653,9 @@ export {
11459
11653
  createVoiceHubSpotTaskSyncSinks,
11460
11654
  createVoiceHubSpotTaskSink,
11461
11655
  createVoiceHelpdeskTicketSink,
11656
+ createVoiceHandoffHealthRoutes,
11657
+ createVoiceHandoffHealthJSONHandler,
11658
+ createVoiceHandoffHealthHTMLHandler,
11462
11659
  createVoiceFileTraceSinkDeliveryStore,
11463
11660
  createVoiceFileTraceEventStore,
11464
11661
  createVoiceFileTaskStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.29",
3
+ "version": "0.0.22-beta.30",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",