@absolutejs/voice 0.0.22-beta.112 → 0.0.22-beta.114
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/campaign.d.ts +107 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +274 -1
- package/package.json +1 -1
package/dist/campaign.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Elysia } from 'elysia';
|
|
2
2
|
import type { VoiceRedisTaskLeaseCoordinator } from './queue';
|
|
3
|
+
import type { VoiceTelephonyOutcomeDecision, VoiceTelephonyOutcomeProviderEvent, VoiceTelephonyWebhookDecision } from './telephonyOutcome';
|
|
3
4
|
export type VoiceCampaignStatus = 'canceled' | 'completed' | 'draft' | 'paused' | 'running';
|
|
4
5
|
export type VoiceCampaignRecipientStatus = 'canceled' | 'completed' | 'failed' | 'pending' | 'queued';
|
|
5
6
|
export type VoiceCampaignAttemptStatus = 'canceled' | 'failed' | 'queued' | 'running' | 'succeeded';
|
|
@@ -154,6 +155,7 @@ export type VoiceCampaignRuntime = {
|
|
|
154
155
|
export type VoiceCampaignRoutesOptions = VoiceCampaignRuntimeOptions & {
|
|
155
156
|
headers?: HeadersInit;
|
|
156
157
|
htmlPath?: false | string;
|
|
158
|
+
observability?: VoiceCampaignObservabilityOptions;
|
|
157
159
|
name?: string;
|
|
158
160
|
path?: string;
|
|
159
161
|
title?: string;
|
|
@@ -192,15 +194,106 @@ export type VoiceCampaignWorkerLoop = {
|
|
|
192
194
|
stop: () => void;
|
|
193
195
|
tick: () => Promise<VoiceCampaignWorkerResult>;
|
|
194
196
|
};
|
|
197
|
+
export type VoiceCampaignObservabilityOptions = {
|
|
198
|
+
leases?: VoiceRedisTaskLeaseCoordinator;
|
|
199
|
+
now?: number;
|
|
200
|
+
rateWindowMs?: number;
|
|
201
|
+
stuckAfterMs?: number;
|
|
202
|
+
};
|
|
203
|
+
export type VoiceCampaignObservabilityReport = {
|
|
204
|
+
attemptRate: {
|
|
205
|
+
failed: number;
|
|
206
|
+
started: number;
|
|
207
|
+
succeeded: number;
|
|
208
|
+
windowMs: number;
|
|
209
|
+
};
|
|
210
|
+
campaigns: Array<{
|
|
211
|
+
activeAttempts: number;
|
|
212
|
+
campaignId: string;
|
|
213
|
+
lease?: {
|
|
214
|
+
expiresAt: number;
|
|
215
|
+
workerId: string;
|
|
216
|
+
};
|
|
217
|
+
name: string;
|
|
218
|
+
queueDepth: number;
|
|
219
|
+
status: VoiceCampaignStatus;
|
|
220
|
+
stuckAttempts: number;
|
|
221
|
+
stuckRecipients: number;
|
|
222
|
+
updatedAt: number;
|
|
223
|
+
}>;
|
|
224
|
+
failureReasons: Array<{
|
|
225
|
+
count: number;
|
|
226
|
+
reason: string;
|
|
227
|
+
}>;
|
|
228
|
+
generatedAt: number;
|
|
229
|
+
leases: {
|
|
230
|
+
active: number;
|
|
231
|
+
known: boolean;
|
|
232
|
+
};
|
|
233
|
+
queue: {
|
|
234
|
+
activeAttempts: number;
|
|
235
|
+
queuedRecipients: number;
|
|
236
|
+
runningCampaigns: number;
|
|
237
|
+
};
|
|
238
|
+
stuck: {
|
|
239
|
+
attempts: Array<{
|
|
240
|
+
attemptId: string;
|
|
241
|
+
campaignId: string;
|
|
242
|
+
recipientId: string;
|
|
243
|
+
status: VoiceCampaignAttemptStatus;
|
|
244
|
+
updatedAt: number;
|
|
245
|
+
}>;
|
|
246
|
+
recipients: Array<{
|
|
247
|
+
campaignId: string;
|
|
248
|
+
recipientId: string;
|
|
249
|
+
status: VoiceCampaignRecipientStatus;
|
|
250
|
+
updatedAt: number;
|
|
251
|
+
}>;
|
|
252
|
+
};
|
|
253
|
+
summary: VoiceCampaignSummary;
|
|
254
|
+
};
|
|
255
|
+
export type VoiceCampaignTelephonyOutcomeInput<TResult = unknown> = {
|
|
256
|
+
campaignId?: string;
|
|
257
|
+
decision: VoiceTelephonyOutcomeDecision;
|
|
258
|
+
event?: VoiceTelephonyOutcomeProviderEvent;
|
|
259
|
+
externalCallId?: string;
|
|
260
|
+
routeResult?: TResult;
|
|
261
|
+
sessionId?: string;
|
|
262
|
+
attemptId?: string;
|
|
263
|
+
};
|
|
264
|
+
export type VoiceCampaignTelephonyOutcomeStatus = 'failed' | 'ignore' | 'succeeded';
|
|
265
|
+
export type VoiceCampaignTelephonyOutcomeOptions<TResult = unknown> = {
|
|
266
|
+
resolveCampaignId?: (input: VoiceCampaignTelephonyOutcomeInput<TResult>) => Promise<string | undefined> | string | undefined;
|
|
267
|
+
resolveExternalCallId?: (input: VoiceCampaignTelephonyOutcomeInput<TResult>) => Promise<string | undefined> | string | undefined;
|
|
268
|
+
resolveAttemptId?: (input: VoiceCampaignTelephonyOutcomeInput<TResult>) => Promise<string | undefined> | string | undefined;
|
|
269
|
+
runtime?: VoiceCampaignRuntime;
|
|
270
|
+
statusForDecision?: (input: VoiceCampaignTelephonyOutcomeInput<TResult>) => Promise<VoiceCampaignTelephonyOutcomeStatus> | VoiceCampaignTelephonyOutcomeStatus;
|
|
271
|
+
store?: VoiceCampaignStore;
|
|
272
|
+
};
|
|
273
|
+
export type VoiceCampaignTelephonyOutcomeResult = {
|
|
274
|
+
applied: boolean;
|
|
275
|
+
campaignId?: string;
|
|
276
|
+
error?: string;
|
|
277
|
+
externalCallId?: string;
|
|
278
|
+
reason?: 'ignored' | 'missing-attempt' | 'missing-campaign' | 'missing-runtime' | 'terminal-attempt';
|
|
279
|
+
status?: 'failed' | 'succeeded';
|
|
280
|
+
attemptId?: string;
|
|
281
|
+
};
|
|
195
282
|
export declare const createVoiceMemoryCampaignStore: () => VoiceCampaignStore;
|
|
196
283
|
export declare const summarizeVoiceCampaigns: (records: VoiceCampaignRecord[]) => VoiceCampaignSummary;
|
|
284
|
+
export declare const buildVoiceCampaignObservabilityReport: (records: VoiceCampaignRecord[], options?: VoiceCampaignObservabilityOptions) => Promise<VoiceCampaignObservabilityReport>;
|
|
197
285
|
export declare const createVoiceCampaign: (options: VoiceCampaignRuntimeOptions) => VoiceCampaignRuntime;
|
|
198
286
|
export declare const createVoiceCampaignWorker: (options: VoiceCampaignWorkerOptions) => VoiceCampaignWorker;
|
|
199
287
|
export declare const createVoiceCampaignWorkerLoop: (options: VoiceCampaignWorkerLoopOptions) => VoiceCampaignWorkerLoop;
|
|
288
|
+
export declare const applyVoiceCampaignTelephonyOutcome: <TResult = unknown>(input: VoiceCampaignTelephonyOutcomeInput<TResult>, options?: VoiceCampaignTelephonyOutcomeOptions<TResult>) => Promise<VoiceCampaignTelephonyOutcomeResult>;
|
|
289
|
+
export declare const createVoiceCampaignTelephonyOutcomeHandler: <TResult = unknown>(options: VoiceCampaignTelephonyOutcomeOptions<TResult>) => (input: VoiceTelephonyWebhookDecision<TResult>) => Promise<VoiceCampaignTelephonyOutcomeResult>;
|
|
200
290
|
export declare const runVoiceCampaignProof: (options?: VoiceCampaignProofOptions) => Promise<VoiceCampaignProofReport>;
|
|
201
291
|
export declare const renderVoiceCampaignsHTML: (records: VoiceCampaignRecord[], options?: {
|
|
202
292
|
title?: string;
|
|
203
293
|
}) => string;
|
|
294
|
+
export declare const renderVoiceCampaignObservabilityHTML: (report: VoiceCampaignObservabilityReport, options?: {
|
|
295
|
+
title?: string;
|
|
296
|
+
}) => string;
|
|
204
297
|
export declare const createVoiceCampaignRoutes: (options: VoiceCampaignRoutesOptions) => Elysia<"", {
|
|
205
298
|
decorator: {};
|
|
206
299
|
store: {};
|
|
@@ -231,6 +324,20 @@ export declare const createVoiceCampaignRoutes: (options: VoiceCampaignRoutesOpt
|
|
|
231
324
|
};
|
|
232
325
|
};
|
|
233
326
|
};
|
|
327
|
+
} & {
|
|
328
|
+
[x: string]: {
|
|
329
|
+
observability: {
|
|
330
|
+
get: {
|
|
331
|
+
body: unknown;
|
|
332
|
+
params: {};
|
|
333
|
+
query: unknown;
|
|
334
|
+
headers: unknown;
|
|
335
|
+
response: {
|
|
336
|
+
200: VoiceCampaignObservabilityReport;
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
};
|
|
234
341
|
} & {
|
|
235
342
|
[x: string]: {
|
|
236
343
|
post: {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { voice } from './plugin';
|
|
2
2
|
export { createVoiceAppKit, createVoiceAppKitRoutes, summarizeVoiceAppKitStatus } from './appKit';
|
|
3
|
-
export { createVoiceCampaign, createVoiceCampaignRoutes, createVoiceCampaignWorker, createVoiceCampaignWorkerLoop, createVoiceMemoryCampaignStore, renderVoiceCampaignsHTML, runVoiceCampaignProof, summarizeVoiceCampaigns } from './campaign';
|
|
3
|
+
export { applyVoiceCampaignTelephonyOutcome, buildVoiceCampaignObservabilityReport, createVoiceCampaignTelephonyOutcomeHandler, createVoiceCampaign, createVoiceCampaignRoutes, createVoiceCampaignWorker, createVoiceCampaignWorkerLoop, createVoiceMemoryCampaignStore, renderVoiceCampaignObservabilityHTML, renderVoiceCampaignsHTML, runVoiceCampaignProof, summarizeVoiceCampaigns } from './campaign';
|
|
4
4
|
export { createVoiceAssistant, createVoiceExperiment, summarizeVoiceAssistantRuns } from './assistant';
|
|
5
5
|
export { createVoiceAssistantHealthHTMLHandler, createVoiceAssistantHealthJSONHandler, createVoiceAssistantHealthRoutes, renderVoiceAssistantHealthHTML, summarizeVoiceAssistantHealth } from './assistantHealth';
|
|
6
6
|
export { createVoiceBargeInRoutes, renderVoiceBargeInHTML, summarizeVoiceBargeIn } from './bargeInRoutes';
|
package/dist/index.js
CHANGED
|
@@ -10628,6 +10628,112 @@ var summarizeVoiceCampaigns = (records) => {
|
|
|
10628
10628
|
}
|
|
10629
10629
|
return summary;
|
|
10630
10630
|
};
|
|
10631
|
+
var buildVoiceCampaignObservabilityReport = async (records, options = {}) => {
|
|
10632
|
+
const now = options.now ?? Date.now();
|
|
10633
|
+
const stuckAfterMs = Math.max(1, options.stuckAfterMs ?? 15 * 60000);
|
|
10634
|
+
const rateWindowMs = Math.max(1, options.rateWindowMs ?? 60 * 60000);
|
|
10635
|
+
const rateWindowStart = now - rateWindowMs;
|
|
10636
|
+
const failureReasons = new Map;
|
|
10637
|
+
const report = {
|
|
10638
|
+
attemptRate: {
|
|
10639
|
+
failed: 0,
|
|
10640
|
+
started: 0,
|
|
10641
|
+
succeeded: 0,
|
|
10642
|
+
windowMs: rateWindowMs
|
|
10643
|
+
},
|
|
10644
|
+
campaigns: [],
|
|
10645
|
+
failureReasons: [],
|
|
10646
|
+
generatedAt: now,
|
|
10647
|
+
leases: {
|
|
10648
|
+
active: 0,
|
|
10649
|
+
known: Boolean(options.leases)
|
|
10650
|
+
},
|
|
10651
|
+
queue: {
|
|
10652
|
+
activeAttempts: 0,
|
|
10653
|
+
queuedRecipients: 0,
|
|
10654
|
+
runningCampaigns: 0
|
|
10655
|
+
},
|
|
10656
|
+
stuck: {
|
|
10657
|
+
attempts: [],
|
|
10658
|
+
recipients: []
|
|
10659
|
+
},
|
|
10660
|
+
summary: summarizeVoiceCampaigns(records)
|
|
10661
|
+
};
|
|
10662
|
+
for (const record of records) {
|
|
10663
|
+
const campaignId = record.campaign.id;
|
|
10664
|
+
const queuedRecipients = record.recipients.filter((recipient) => recipient.status === "queued");
|
|
10665
|
+
const activeAttempts = record.attempts.filter((attempt) => attempt.status === "queued" || attempt.status === "running");
|
|
10666
|
+
const campaignReport = {
|
|
10667
|
+
activeAttempts: activeAttempts.length,
|
|
10668
|
+
campaignId,
|
|
10669
|
+
name: record.campaign.name,
|
|
10670
|
+
queueDepth: queuedRecipients.length,
|
|
10671
|
+
status: record.campaign.status,
|
|
10672
|
+
stuckAttempts: 0,
|
|
10673
|
+
stuckRecipients: 0,
|
|
10674
|
+
updatedAt: record.campaign.updatedAt
|
|
10675
|
+
};
|
|
10676
|
+
if (record.campaign.status === "running") {
|
|
10677
|
+
report.queue.runningCampaigns += 1;
|
|
10678
|
+
}
|
|
10679
|
+
report.queue.queuedRecipients += queuedRecipients.length;
|
|
10680
|
+
report.queue.activeAttempts += activeAttempts.length;
|
|
10681
|
+
for (const recipient of record.recipients) {
|
|
10682
|
+
if ((recipient.status === "pending" || recipient.status === "queued") && now - recipient.updatedAt >= stuckAfterMs) {
|
|
10683
|
+
campaignReport.stuckRecipients += 1;
|
|
10684
|
+
report.stuck.recipients.push({
|
|
10685
|
+
campaignId,
|
|
10686
|
+
recipientId: recipient.id,
|
|
10687
|
+
status: recipient.status,
|
|
10688
|
+
updatedAt: recipient.updatedAt
|
|
10689
|
+
});
|
|
10690
|
+
}
|
|
10691
|
+
if (recipient.error) {
|
|
10692
|
+
failureReasons.set(recipient.error, (failureReasons.get(recipient.error) ?? 0) + 1);
|
|
10693
|
+
}
|
|
10694
|
+
}
|
|
10695
|
+
for (const attempt of record.attempts) {
|
|
10696
|
+
if ((attempt.startedAt ?? attempt.createdAt) >= rateWindowStart) {
|
|
10697
|
+
report.attemptRate.started += 1;
|
|
10698
|
+
}
|
|
10699
|
+
if (attempt.status === "failed" && attempt.updatedAt >= rateWindowStart) {
|
|
10700
|
+
report.attemptRate.failed += 1;
|
|
10701
|
+
}
|
|
10702
|
+
if (attempt.status === "succeeded" && attempt.updatedAt >= rateWindowStart) {
|
|
10703
|
+
report.attemptRate.succeeded += 1;
|
|
10704
|
+
}
|
|
10705
|
+
if (attempt.error) {
|
|
10706
|
+
failureReasons.set(attempt.error, (failureReasons.get(attempt.error) ?? 0) + 1);
|
|
10707
|
+
}
|
|
10708
|
+
if ((attempt.status === "queued" || attempt.status === "running") && now - attempt.updatedAt >= stuckAfterMs) {
|
|
10709
|
+
campaignReport.stuckAttempts += 1;
|
|
10710
|
+
report.stuck.attempts.push({
|
|
10711
|
+
attemptId: attempt.id,
|
|
10712
|
+
campaignId,
|
|
10713
|
+
recipientId: attempt.recipientId,
|
|
10714
|
+
status: attempt.status,
|
|
10715
|
+
updatedAt: attempt.updatedAt
|
|
10716
|
+
});
|
|
10717
|
+
}
|
|
10718
|
+
}
|
|
10719
|
+
if (options.leases) {
|
|
10720
|
+
const lease = await options.leases.get(getCampaignLeaseTaskId(campaignId));
|
|
10721
|
+
if (lease) {
|
|
10722
|
+
report.leases.active += 1;
|
|
10723
|
+
campaignReport.lease = {
|
|
10724
|
+
expiresAt: lease.expiresAt,
|
|
10725
|
+
workerId: lease.workerId
|
|
10726
|
+
};
|
|
10727
|
+
}
|
|
10728
|
+
}
|
|
10729
|
+
report.campaigns.push(campaignReport);
|
|
10730
|
+
}
|
|
10731
|
+
report.failureReasons = [...failureReasons.entries()].map(([reason, count]) => ({ count, reason })).sort((left, right) => right.count === left.count ? left.reason.localeCompare(right.reason) : right.count - left.count);
|
|
10732
|
+
report.campaigns.sort((left, right) => right.updatedAt - left.updatedAt);
|
|
10733
|
+
report.stuck.attempts.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
10734
|
+
report.stuck.recipients.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
10735
|
+
return report;
|
|
10736
|
+
};
|
|
10631
10737
|
var createVoiceCampaign = (options) => {
|
|
10632
10738
|
const { store } = options;
|
|
10633
10739
|
return {
|
|
@@ -10878,6 +10984,155 @@ var createVoiceCampaignWorkerLoop = (options) => {
|
|
|
10878
10984
|
tick
|
|
10879
10985
|
};
|
|
10880
10986
|
};
|
|
10987
|
+
var firstOutcomeString = (values) => {
|
|
10988
|
+
for (const value of values) {
|
|
10989
|
+
if (typeof value === "string" && value.trim()) {
|
|
10990
|
+
return value.trim();
|
|
10991
|
+
}
|
|
10992
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
10993
|
+
return String(value);
|
|
10994
|
+
}
|
|
10995
|
+
}
|
|
10996
|
+
};
|
|
10997
|
+
var resolveDefaultCampaignOutcomeIds = (input) => {
|
|
10998
|
+
const metadata = input.event?.metadata ?? {};
|
|
10999
|
+
const decisionMetadata = input.decision.metadata ?? {};
|
|
11000
|
+
const routeResult = typeof input.routeResult === "object" && input.routeResult !== null ? input.routeResult : {};
|
|
11001
|
+
return {
|
|
11002
|
+
campaignId: input.campaignId ?? firstOutcomeString([
|
|
11003
|
+
metadata.campaignId,
|
|
11004
|
+
metadata.voiceCampaignId,
|
|
11005
|
+
decisionMetadata.campaignId,
|
|
11006
|
+
decisionMetadata.voiceCampaignId,
|
|
11007
|
+
routeResult.campaignId,
|
|
11008
|
+
routeResult.voiceCampaignId
|
|
11009
|
+
]),
|
|
11010
|
+
externalCallId: input.externalCallId ?? firstOutcomeString([
|
|
11011
|
+
metadata.externalCallId,
|
|
11012
|
+
metadata.callId,
|
|
11013
|
+
metadata.callSid,
|
|
11014
|
+
metadata.callUuid,
|
|
11015
|
+
decisionMetadata.externalCallId,
|
|
11016
|
+
decisionMetadata.callId,
|
|
11017
|
+
decisionMetadata.callSid,
|
|
11018
|
+
decisionMetadata.callUuid,
|
|
11019
|
+
routeResult.externalCallId,
|
|
11020
|
+
routeResult.callId,
|
|
11021
|
+
routeResult.callSid,
|
|
11022
|
+
routeResult.callUuid,
|
|
11023
|
+
input.sessionId
|
|
11024
|
+
]),
|
|
11025
|
+
attemptId: input.attemptId ?? firstOutcomeString([
|
|
11026
|
+
metadata.attemptId,
|
|
11027
|
+
metadata.voiceCampaignAttemptId,
|
|
11028
|
+
decisionMetadata.attemptId,
|
|
11029
|
+
decisionMetadata.voiceCampaignAttemptId,
|
|
11030
|
+
routeResult.attemptId,
|
|
11031
|
+
routeResult.voiceCampaignAttemptId
|
|
11032
|
+
])
|
|
11033
|
+
};
|
|
11034
|
+
};
|
|
11035
|
+
var defaultCampaignOutcomeStatus = (input) => {
|
|
11036
|
+
switch (input.decision.action) {
|
|
11037
|
+
case "complete":
|
|
11038
|
+
case "transfer":
|
|
11039
|
+
return "succeeded";
|
|
11040
|
+
case "escalate":
|
|
11041
|
+
case "no-answer":
|
|
11042
|
+
case "voicemail":
|
|
11043
|
+
return "failed";
|
|
11044
|
+
default:
|
|
11045
|
+
return "ignore";
|
|
11046
|
+
}
|
|
11047
|
+
};
|
|
11048
|
+
var findCampaignAttempt = async (input) => {
|
|
11049
|
+
const records = input.campaignId ? [await input.runtime.get(input.campaignId)].filter(Boolean) : await input.runtime.list();
|
|
11050
|
+
for (const record of records) {
|
|
11051
|
+
const attempt = record.attempts.find((item) => input.attemptId && item.id === input.attemptId || input.externalCallId && item.externalCallId === input.externalCallId);
|
|
11052
|
+
if (attempt) {
|
|
11053
|
+
return {
|
|
11054
|
+
attempt,
|
|
11055
|
+
record
|
|
11056
|
+
};
|
|
11057
|
+
}
|
|
11058
|
+
}
|
|
11059
|
+
};
|
|
11060
|
+
var applyVoiceCampaignTelephonyOutcome = async (input, options = {}) => {
|
|
11061
|
+
const runtime = options.runtime ?? (options.store ? createVoiceCampaign({
|
|
11062
|
+
store: options.store
|
|
11063
|
+
}) : undefined);
|
|
11064
|
+
if (!runtime) {
|
|
11065
|
+
return {
|
|
11066
|
+
applied: false,
|
|
11067
|
+
reason: "missing-runtime"
|
|
11068
|
+
};
|
|
11069
|
+
}
|
|
11070
|
+
const defaults = resolveDefaultCampaignOutcomeIds(input);
|
|
11071
|
+
const campaignId = await options.resolveCampaignId?.(input) ?? defaults.campaignId;
|
|
11072
|
+
const attemptId = await options.resolveAttemptId?.(input) ?? defaults.attemptId;
|
|
11073
|
+
const externalCallId = await options.resolveExternalCallId?.(input) ?? defaults.externalCallId;
|
|
11074
|
+
const status = await options.statusForDecision?.(input) ?? defaultCampaignOutcomeStatus(input);
|
|
11075
|
+
if (status === "ignore") {
|
|
11076
|
+
return {
|
|
11077
|
+
applied: false,
|
|
11078
|
+
campaignId,
|
|
11079
|
+
externalCallId,
|
|
11080
|
+
reason: "ignored",
|
|
11081
|
+
attemptId
|
|
11082
|
+
};
|
|
11083
|
+
}
|
|
11084
|
+
const match = await findCampaignAttempt({
|
|
11085
|
+
attemptId,
|
|
11086
|
+
campaignId,
|
|
11087
|
+
externalCallId,
|
|
11088
|
+
runtime
|
|
11089
|
+
});
|
|
11090
|
+
if (!match) {
|
|
11091
|
+
return {
|
|
11092
|
+
applied: false,
|
|
11093
|
+
campaignId,
|
|
11094
|
+
externalCallId,
|
|
11095
|
+
reason: campaignId ? "missing-attempt" : "missing-campaign",
|
|
11096
|
+
attemptId
|
|
11097
|
+
};
|
|
11098
|
+
}
|
|
11099
|
+
if (match.attempt.status === "failed" || match.attempt.status === "succeeded") {
|
|
11100
|
+
return {
|
|
11101
|
+
applied: false,
|
|
11102
|
+
campaignId: match.record.campaign.id,
|
|
11103
|
+
externalCallId: match.attempt.externalCallId ?? externalCallId,
|
|
11104
|
+
reason: "terminal-attempt",
|
|
11105
|
+
status: match.attempt.status,
|
|
11106
|
+
attemptId: match.attempt.id
|
|
11107
|
+
};
|
|
11108
|
+
}
|
|
11109
|
+
await runtime.completeAttempt(match.record.campaign.id, match.attempt.id, {
|
|
11110
|
+
error: status === "failed" ? input.decision.reason ?? input.decision.disposition ?? input.event?.reason ?? input.event?.status ?? input.decision.action : undefined,
|
|
11111
|
+
externalCallId: externalCallId ?? match.attempt.externalCallId,
|
|
11112
|
+
metadata: {
|
|
11113
|
+
telephonyAction: input.decision.action,
|
|
11114
|
+
telephonyConfidence: input.decision.confidence,
|
|
11115
|
+
telephonyDisposition: input.decision.disposition,
|
|
11116
|
+
telephonyProvider: input.event?.provider,
|
|
11117
|
+
telephonySource: input.decision.source,
|
|
11118
|
+
telephonyStatus: input.event?.status
|
|
11119
|
+
},
|
|
11120
|
+
status
|
|
11121
|
+
});
|
|
11122
|
+
return {
|
|
11123
|
+
applied: true,
|
|
11124
|
+
campaignId: match.record.campaign.id,
|
|
11125
|
+
externalCallId: externalCallId ?? match.attempt.externalCallId,
|
|
11126
|
+
status,
|
|
11127
|
+
attemptId: match.attempt.id
|
|
11128
|
+
};
|
|
11129
|
+
};
|
|
11130
|
+
var createVoiceCampaignTelephonyOutcomeHandler = (options) => (input) => applyVoiceCampaignTelephonyOutcome({
|
|
11131
|
+
decision: input.decision,
|
|
11132
|
+
event: input.event,
|
|
11133
|
+
routeResult: input.routeResult,
|
|
11134
|
+
sessionId: input.sessionId
|
|
11135
|
+
}, options);
|
|
10881
11136
|
var defaultProofRecipients = () => [
|
|
10882
11137
|
{
|
|
10883
11138
|
id: "campaign-proof-recipient-1",
|
|
@@ -10953,6 +11208,12 @@ var renderVoiceCampaignsHTML = (records, options = {}) => {
|
|
|
10953
11208
|
const summary = summarizeVoiceCampaigns(records);
|
|
10954
11209
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml18(title)}</title><style>body{background:#111827;color:#f9fafb;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1080px;padding:32px}.hero{background:linear-gradient(135deg,rgba(251,146,60,.18),rgba(45,212,191,.12));border:1px solid #334155;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#fdba74;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:18px 0}.grid article,table{background:#172033;border:1px solid #334155;border-radius:18px}.grid article{padding:16px}.grid span{color:#aab5c0}.grid strong{display:block;font-size:2rem;margin:.25rem 0}table{border-collapse:collapse;overflow:hidden;width:100%}td,th{border-bottom:1px solid #334155;padding:12px;text-align:left}</style></head><body><main><section class="hero"><p class="eyebrow">Self-hosted outbound</p><h1>${escapeHtml18(title)}</h1><p>Campaign orchestration, recipients, attempts, retries, and outcomes without a hosted dialer dashboard.</p><section class="grid"><article><span>Campaigns</span><strong>${String(summary.campaigns.total)}</strong></article><article><span>Recipients</span><strong>${String(summary.recipients.total)}</strong></article><article><span>Attempts</span><strong>${String(summary.attempts.total)}</strong></article><article><span>Running</span><strong>${String(summary.campaigns.running)}</strong></article></section></section><table><thead><tr><th>Name</th><th>Status</th><th>Recipients</th><th>Attempts</th><th>Updated</th></tr></thead><tbody>${rows || '<tr><td colspan="5">No campaigns yet.</td></tr>'}</tbody></table></main></body></html>`;
|
|
10955
11210
|
};
|
|
11211
|
+
var renderVoiceCampaignObservabilityHTML = (report, options = {}) => {
|
|
11212
|
+
const title = options.title ?? "Voice Campaign Observability";
|
|
11213
|
+
const campaignRows = report.campaigns.map((campaign) => `<tr><td>${escapeHtml18(campaign.name)}</td><td>${escapeHtml18(campaign.status)}</td><td>${String(campaign.queueDepth)}</td><td>${String(campaign.activeAttempts)}</td><td>${String(campaign.stuckRecipients + campaign.stuckAttempts)}</td><td>${campaign.lease ? escapeHtml18(campaign.lease.workerId) : "none"}</td></tr>`).join("");
|
|
11214
|
+
const failureRows = report.failureReasons.map((failure) => `<tr><td>${escapeHtml18(failure.reason)}</td><td>${String(failure.count)}</td></tr>`).join("");
|
|
11215
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml18(title)}</title><style>body{background:#0b1220;color:#e5edf7;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(20,184,166,.2),rgba(251,146,60,.14));border:1px solid #334155;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.2rem,5vw,4.6rem);line-height:.95;margin:.2rem 0 1rem}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));margin:18px 0}.card,table{background:#111c2f;border:1px solid #334155;border-radius:18px}.card{padding:16px}.card span{color:#9fb0c5}.card strong{display:block;font-size:2rem;margin:.25rem 0}table{border-collapse:collapse;margin-top:18px;overflow:hidden;width:100%}td,th{border-bottom:1px solid #334155;padding:12px;text-align:left}.warn{color:#fde68a}.bad{color:#fecaca}</style></head><body><main><section class="hero"><p class="eyebrow">Campaign ops</p><h1>${escapeHtml18(title)}</h1><p>Queue depth, active leases, attempt rates, failure reasons, and stuck work for self-hosted outbound voice.</p><section class="grid"><article class="card"><span>Queued recipients</span><strong>${String(report.queue.queuedRecipients)}</strong></article><article class="card"><span>Active attempts</span><strong>${String(report.queue.activeAttempts)}</strong></article><article class="card"><span>Running campaigns</span><strong>${String(report.queue.runningCampaigns)}</strong></article><article class="card"><span>Active leases</span><strong>${report.leases.known ? String(report.leases.active) : "n/a"}</strong></article><article class="card"><span>Attempts/window</span><strong>${String(report.attemptRate.started)}</strong></article><article class="card"><span>Stuck work</span><strong class="${report.stuck.attempts.length + report.stuck.recipients.length > 0 ? "bad" : ""}">${String(report.stuck.attempts.length + report.stuck.recipients.length)}</strong></article></section></section><h2>Campaigns</h2><table><thead><tr><th>Name</th><th>Status</th><th>Queued</th><th>Active</th><th>Stuck</th><th>Lease</th></tr></thead><tbody>${campaignRows || '<tr><td colspan="6">No campaigns yet.</td></tr>'}</tbody></table><h2>Failure Reasons</h2><table><thead><tr><th>Reason</th><th>Count</th></tr></thead><tbody>${failureRows || '<tr><td colspan="2">No failures recorded.</td></tr>'}</tbody></table></main></body></html>`;
|
|
11216
|
+
};
|
|
10956
11217
|
var readJsonBody = async (request) => {
|
|
10957
11218
|
const text = await request.text();
|
|
10958
11219
|
return text.trim() ? JSON.parse(text) : {};
|
|
@@ -10964,7 +11225,7 @@ var createVoiceCampaignRoutes = (options) => {
|
|
|
10964
11225
|
const app = new Elysia17({ name: options.name ?? "absolutejs-voice-campaigns" }).get(path, async () => ({
|
|
10965
11226
|
campaigns: await runtime.list(),
|
|
10966
11227
|
summary: await runtime.summarize()
|
|
10967
|
-
})).post(path, async ({ request }) => runtime.create(await readJsonBody(request))).get(`${path}/:campaignId`, ({ params }) => runtime.get(params.campaignId)).delete(`${path}/:campaignId`, async ({ params }) => {
|
|
11228
|
+
})).get(`${path}/observability`, async () => buildVoiceCampaignObservabilityReport(await runtime.list(), options.observability)).post(path, async ({ request }) => runtime.create(await readJsonBody(request))).get(`${path}/:campaignId`, ({ params }) => runtime.get(params.campaignId)).delete(`${path}/:campaignId`, async ({ params }) => {
|
|
10968
11229
|
await runtime.remove(params.campaignId);
|
|
10969
11230
|
return { ok: true };
|
|
10970
11231
|
}).post(`${path}/:campaignId/recipients`, async ({ params, request }) => {
|
|
@@ -10983,6 +11244,14 @@ var createVoiceCampaignRoutes = (options) => {
|
|
|
10983
11244
|
...options.headers
|
|
10984
11245
|
}
|
|
10985
11246
|
});
|
|
11247
|
+
}).get(`${htmlPath}/observability`, async () => {
|
|
11248
|
+
const report = await buildVoiceCampaignObservabilityReport(await runtime.list(), options.observability);
|
|
11249
|
+
return new Response(renderVoiceCampaignObservabilityHTML(report, options), {
|
|
11250
|
+
headers: {
|
|
11251
|
+
"content-type": "text/html; charset=utf-8",
|
|
11252
|
+
...options.headers
|
|
11253
|
+
}
|
|
11254
|
+
});
|
|
10986
11255
|
});
|
|
10987
11256
|
}
|
|
10988
11257
|
return app;
|
|
@@ -19033,6 +19302,7 @@ export {
|
|
|
19033
19302
|
renderVoiceEvalHTML,
|
|
19034
19303
|
renderVoiceEvalBaselineHTML,
|
|
19035
19304
|
renderVoiceCampaignsHTML,
|
|
19305
|
+
renderVoiceCampaignObservabilityHTML,
|
|
19036
19306
|
renderVoiceCallReviewMarkdown,
|
|
19037
19307
|
renderVoiceCallReviewHTML,
|
|
19038
19308
|
renderVoiceBargeInHTML,
|
|
@@ -19209,6 +19479,7 @@ export {
|
|
|
19209
19479
|
createVoiceDiagnosticsRoutes,
|
|
19210
19480
|
createVoiceCampaignWorkerLoop,
|
|
19211
19481
|
createVoiceCampaignWorker,
|
|
19482
|
+
createVoiceCampaignTelephonyOutcomeHandler,
|
|
19212
19483
|
createVoiceCampaignRoutes,
|
|
19213
19484
|
createVoiceCampaign,
|
|
19214
19485
|
createVoiceCallReviewRecorder,
|
|
@@ -19262,11 +19533,13 @@ export {
|
|
|
19262
19533
|
buildVoiceOpsTaskFromReview,
|
|
19263
19534
|
buildVoiceOpsConsoleReport,
|
|
19264
19535
|
buildVoiceDiagnosticsMarkdown,
|
|
19536
|
+
buildVoiceCampaignObservabilityReport,
|
|
19265
19537
|
assignVoiceOpsTask,
|
|
19266
19538
|
applyVoiceTelephonyOutcome,
|
|
19267
19539
|
applyVoiceOpsTaskPolicy,
|
|
19268
19540
|
applyVoiceOpsTaskAssignmentRule,
|
|
19269
19541
|
applyVoiceHandoffDeliveryResult,
|
|
19542
|
+
applyVoiceCampaignTelephonyOutcome,
|
|
19270
19543
|
applyRiskTieredPhraseHintCorrections,
|
|
19271
19544
|
applyPhraseHintCorrections,
|
|
19272
19545
|
TURN_PROFILE_DEFAULTS
|