@absolutejs/voice 0.0.22-beta.512 → 0.0.22-beta.513

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,40 @@
1
+ import type { VoiceScorecard } from "./callScorecard";
2
+ export type VoiceAgentPerformanceBucket = "day" | "week" | "month";
3
+ export type VoiceAgentPerformanceCriterionSummary = {
4
+ criterionId: string;
5
+ averageScore: number;
6
+ passRate: number;
7
+ trend: "up" | "down" | "flat";
8
+ delta: number;
9
+ };
10
+ export type VoiceAgentPerformanceBucketSummary = {
11
+ bucketKey: string;
12
+ callsScored: number;
13
+ averageWeightedScore: number;
14
+ passRate: number;
15
+ needsReviewRate: number;
16
+ failRate: number;
17
+ };
18
+ export type VoiceAgentPerformanceReport = {
19
+ agentId: string;
20
+ rubricId: string;
21
+ fromMs: number;
22
+ toMs: number;
23
+ bucket: VoiceAgentPerformanceBucket;
24
+ totalCalls: number;
25
+ buckets: VoiceAgentPerformanceBucketSummary[];
26
+ criteria: VoiceAgentPerformanceCriterionSummary[];
27
+ overallPassRate: number;
28
+ overallAverageScore: number;
29
+ worstCriterion: string | null;
30
+ bestCriterion: string | null;
31
+ };
32
+ export type BuildVoiceAgentPerformanceReportInput = {
33
+ agentId: string;
34
+ rubricId: string;
35
+ scorecards: VoiceScorecard[];
36
+ fromMs?: number;
37
+ toMs?: number;
38
+ bucket?: VoiceAgentPerformanceBucket;
39
+ };
40
+ export declare const buildVoiceAgentPerformanceReport: (input: BuildVoiceAgentPerformanceReportInput) => VoiceAgentPerformanceReport;
@@ -0,0 +1,32 @@
1
+ import { type VoiceScorecard, type VoiceScorecardRubric } from "./callScorecard";
2
+ export type VoiceAIScorecardCompletion = (input: {
3
+ prompt: string;
4
+ systemPrompt?: string;
5
+ }) => Promise<string>;
6
+ export type VoiceAIScorecardScoringResult = {
7
+ criterionId: string;
8
+ score: number;
9
+ rationale?: string;
10
+ };
11
+ export type VoiceAIScorecardParsedResponse = {
12
+ scores: VoiceAIScorecardScoringResult[];
13
+ comments?: string;
14
+ };
15
+ export type ScoreVoiceCallWithAIInput = {
16
+ rubric: VoiceScorecardRubric;
17
+ sessionId: string;
18
+ transcript: string;
19
+ agentId?: string;
20
+ reviewerId?: string;
21
+ metadata?: Record<string, unknown>;
22
+ now?: () => number;
23
+ };
24
+ export type CreateVoiceAIScorecardOptions = {
25
+ completion: VoiceAIScorecardCompletion;
26
+ systemPrompt?: string;
27
+ };
28
+ export declare const parseVoiceAIScorecardResponse: (raw: string, rubric: VoiceScorecardRubric) => VoiceAIScorecardParsedResponse;
29
+ export declare const createVoiceAIScorecard: (options: CreateVoiceAIScorecardOptions) => {
30
+ scoreCall(input: ScoreVoiceCallWithAIInput): Promise<VoiceScorecard>;
31
+ };
32
+ export type VoiceAIScorecard = ReturnType<typeof createVoiceAIScorecard>;
@@ -0,0 +1,53 @@
1
+ export type VoiceScorecardCriterion = {
2
+ id: string;
3
+ label: string;
4
+ weight: number;
5
+ section?: string;
6
+ passingScore?: number;
7
+ required?: boolean;
8
+ };
9
+ export type VoiceScorecardRubric = {
10
+ id: string;
11
+ label: string;
12
+ scaleMax?: number;
13
+ passingGrade?: number;
14
+ criteria: VoiceScorecardCriterion[];
15
+ };
16
+ export type VoiceScorecardCriterionResult = {
17
+ criterionId: string;
18
+ score: number;
19
+ weight: number;
20
+ rationale?: string;
21
+ passed: boolean;
22
+ };
23
+ export type VoiceScorecard = {
24
+ rubricId: string;
25
+ sessionId: string;
26
+ agentId?: string;
27
+ reviewer: "human" | "llm" | "hybrid";
28
+ reviewerId?: string;
29
+ createdAt: number;
30
+ scaleMax: number;
31
+ passingGrade: number;
32
+ results: VoiceScorecardCriterionResult[];
33
+ weightedScore: number;
34
+ grade: "pass" | "fail" | "needs-review";
35
+ sectionScores: Record<string, number>;
36
+ failedRequiredCriteria: string[];
37
+ comments?: string;
38
+ };
39
+ export type BuildVoiceCallScorecardInput = {
40
+ rubric: VoiceScorecardRubric;
41
+ sessionId: string;
42
+ agentId?: string;
43
+ reviewer: VoiceScorecard["reviewer"];
44
+ reviewerId?: string;
45
+ scores: Record<string, {
46
+ score: number;
47
+ rationale?: string;
48
+ }>;
49
+ comments?: string;
50
+ now?: () => number;
51
+ };
52
+ export declare const buildVoiceCallScorecard: (input: BuildVoiceCallScorecardInput) => VoiceScorecard;
53
+ export declare const DEFAULT_VOICE_SALES_RUBRIC: VoiceScorecardRubric;
package/dist/index.d.ts CHANGED
@@ -331,4 +331,14 @@ export { scoreVoiceNoShowRisk, summarizeVoiceNoShowVerdict, } from "./noShowPred
331
331
  export type { VoiceNoShowHistoricalRecord, VoiceNoShowScoreInput, VoiceNoShowSignal, VoiceNoShowVerdict, } from "./noShowPredictor";
332
332
  export { createVoiceReminderScheduler, DEFAULT_VOICE_REMINDER_TRIGGERS, } from "./reminderScheduler";
333
333
  export type { CreateVoiceReminderSchedulerOptions, ScheduleVoiceRemindersInput, VoiceReminderChannel, VoiceReminderJob, VoiceReminderScheduler, VoiceReminderTrigger, } from "./reminderScheduler";
334
+ export { buildVoiceCallScorecard, DEFAULT_VOICE_SALES_RUBRIC, } from "./callScorecard";
335
+ export type { BuildVoiceCallScorecardInput, VoiceScorecard, VoiceScorecardCriterion, VoiceScorecardCriterionResult, VoiceScorecardRubric, } from "./callScorecard";
336
+ export { createVoiceAIScorecard, parseVoiceAIScorecardResponse, } from "./aiScorecard";
337
+ export type { CreateVoiceAIScorecardOptions, ScoreVoiceCallWithAIInput, VoiceAIScorecard, VoiceAIScorecardCompletion, VoiceAIScorecardParsedResponse, VoiceAIScorecardScoringResult, } from "./aiScorecard";
338
+ export { buildVoiceAgentPerformanceReport } from "./agentPerformanceReport";
339
+ export type { BuildVoiceAgentPerformanceReportInput, VoiceAgentPerformanceBucket, VoiceAgentPerformanceBucketSummary, VoiceAgentPerformanceCriterionSummary, VoiceAgentPerformanceReport, } from "./agentPerformanceReport";
340
+ export { computeVoiceScorecardCalibration } from "./scorecardCalibration";
341
+ export type { VoiceScorecardCalibrationDivergence, VoiceScorecardCalibrationPair, VoiceScorecardCalibrationReport, } from "./scorecardCalibration";
342
+ export { detectVoiceQualityDrift } from "./qualityDriftDetector";
343
+ export type { DetectVoiceQualityDriftInput, VoiceQualityDriftCriterionAlert, VoiceQualityDriftReport, VoiceQualityDriftSeverity, } from "./qualityDriftDetector";
334
344
  export * from "./types";
package/dist/index.js CHANGED
@@ -50086,6 +50086,509 @@ var createVoiceReminderScheduler = (options = {}) => {
50086
50086
  }
50087
50087
  };
50088
50088
  };
50089
+ // src/callScorecard.ts
50090
+ var clampScore = (raw, max) => Math.max(0, Math.min(max, raw));
50091
+ var buildVoiceCallScorecard = (input) => {
50092
+ const now = input.now ?? (() => Date.now());
50093
+ const scaleMax = input.rubric.scaleMax ?? 5;
50094
+ const passingGrade = input.rubric.passingGrade ?? 0.7;
50095
+ const totalWeight = input.rubric.criteria.reduce((sum, c) => sum + c.weight, 0);
50096
+ if (totalWeight <= 0) {
50097
+ throw new Error("Rubric weights must sum to a positive number");
50098
+ }
50099
+ const results = [];
50100
+ const failedRequiredCriteria = [];
50101
+ const sectionAccum = new Map;
50102
+ for (const criterion of input.rubric.criteria) {
50103
+ const raw = input.scores[criterion.id];
50104
+ if (!raw) {
50105
+ throw new Error(`Missing score for criterion: ${criterion.id}`);
50106
+ }
50107
+ const score = clampScore(raw.score, scaleMax);
50108
+ const passingScore = criterion.passingScore ?? scaleMax * 0.6;
50109
+ const passed = score >= passingScore;
50110
+ const result = {
50111
+ criterionId: criterion.id,
50112
+ passed,
50113
+ score,
50114
+ weight: criterion.weight,
50115
+ ...raw.rationale !== undefined ? { rationale: raw.rationale } : {}
50116
+ };
50117
+ results.push(result);
50118
+ if (!passed && criterion.required) {
50119
+ failedRequiredCriteria.push(criterion.id);
50120
+ }
50121
+ const section = criterion.section ?? "default";
50122
+ const entry = sectionAccum.get(section) ?? { weight: 0, weighted: 0 };
50123
+ entry.weighted += score * criterion.weight;
50124
+ entry.weight += criterion.weight;
50125
+ sectionAccum.set(section, entry);
50126
+ }
50127
+ const weightedSum = results.reduce((sum, r) => sum + r.score * r.weight, 0);
50128
+ const weightedScore = weightedSum / (totalWeight * scaleMax);
50129
+ const sectionScores = {};
50130
+ for (const [section, accum] of sectionAccum) {
50131
+ sectionScores[section] = accum.weight === 0 ? 0 : accum.weighted / (accum.weight * scaleMax);
50132
+ }
50133
+ const grade = failedRequiredCriteria.length > 0 ? "fail" : weightedScore >= passingGrade ? "pass" : "needs-review";
50134
+ return {
50135
+ createdAt: now(),
50136
+ failedRequiredCriteria,
50137
+ grade,
50138
+ passingGrade,
50139
+ results,
50140
+ reviewer: input.reviewer,
50141
+ rubricId: input.rubric.id,
50142
+ scaleMax,
50143
+ sectionScores,
50144
+ sessionId: input.sessionId,
50145
+ weightedScore,
50146
+ ...input.agentId !== undefined ? { agentId: input.agentId } : {},
50147
+ ...input.reviewerId !== undefined ? { reviewerId: input.reviewerId } : {},
50148
+ ...input.comments !== undefined ? { comments: input.comments } : {}
50149
+ };
50150
+ };
50151
+ var DEFAULT_VOICE_SALES_RUBRIC = {
50152
+ criteria: [
50153
+ {
50154
+ id: "greeting",
50155
+ label: "Professional greeting",
50156
+ section: "opening",
50157
+ weight: 1
50158
+ },
50159
+ {
50160
+ id: "needs-discovery",
50161
+ label: "Discovers customer needs",
50162
+ required: true,
50163
+ section: "discovery",
50164
+ weight: 2
50165
+ },
50166
+ {
50167
+ id: "objection-handling",
50168
+ label: "Handles objections clearly",
50169
+ section: "objections",
50170
+ weight: 2
50171
+ },
50172
+ {
50173
+ id: "compliance-disclosure",
50174
+ label: "Made required compliance disclosure",
50175
+ required: true,
50176
+ section: "compliance",
50177
+ weight: 3
50178
+ },
50179
+ {
50180
+ id: "close-or-next-step",
50181
+ label: "Closes or sets a next step",
50182
+ section: "close",
50183
+ weight: 2
50184
+ }
50185
+ ],
50186
+ id: "default-sales",
50187
+ label: "Default sales QA rubric",
50188
+ passingGrade: 0.75,
50189
+ scaleMax: 5
50190
+ };
50191
+ // src/aiScorecard.ts
50192
+ var DEFAULT_SYSTEM_PROMPT3 = "You are an impartial quality reviewer scoring a voice-agent call transcript. " + "For each criterion, return a numeric score between 0 and the rubric's scaleMax, with a one-sentence rationale grounded in the transcript. " + 'Respond with strict JSON: {"scores":[{"criterionId":"\u2026","score":4,"rationale":"\u2026"}],"comments":"\u2026"}. ' + "Do not return prose outside the JSON.";
50193
+ var buildPrompt2 = (input) => {
50194
+ const { rubric } = input;
50195
+ const scaleMax = rubric.scaleMax ?? 5;
50196
+ const criteriaBlock = rubric.criteria.map((criterion) => `- ${criterion.id}${criterion.required ? " (required)" : ""}: ${criterion.label} (weight=${criterion.weight}${criterion.section ? `, section=${criterion.section}` : ""})`).join(`
50197
+ `);
50198
+ const metadataBlock = input.metadata ? `
50199
+ Metadata:
50200
+ ${JSON.stringify(input.metadata, null, 2)}
50201
+ ` : "";
50202
+ return `Rubric: ${rubric.label} (scaleMax=${scaleMax})
50203
+ Criteria:
50204
+ ${criteriaBlock}
50205
+ ${metadataBlock}
50206
+ Transcript:
50207
+ ${input.transcript}
50208
+
50209
+ Return JSON only.`;
50210
+ };
50211
+ var extractJson3 = (raw) => {
50212
+ const trimmed = raw.trim();
50213
+ if (!trimmed)
50214
+ throw new Error("AI scorecard returned an empty response");
50215
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/iu.exec(trimmed);
50216
+ const candidate = fenced ? fenced[1].trim() : trimmed;
50217
+ try {
50218
+ return JSON.parse(candidate);
50219
+ } catch {
50220
+ const start = candidate.indexOf("{");
50221
+ const end = candidate.lastIndexOf("}");
50222
+ if (start >= 0 && end > start) {
50223
+ return JSON.parse(candidate.slice(start, end + 1));
50224
+ }
50225
+ throw new Error(`AI scorecard response was not valid JSON: ${raw.slice(0, 200)}`);
50226
+ }
50227
+ };
50228
+ var parseVoiceAIScorecardResponse = (raw, rubric) => {
50229
+ const payload = extractJson3(raw);
50230
+ if (!payload || typeof payload !== "object") {
50231
+ throw new Error("AI scorecard response is not a JSON object");
50232
+ }
50233
+ const root = payload;
50234
+ const scoresRaw = root.scores;
50235
+ if (!Array.isArray(scoresRaw)) {
50236
+ throw new Error("AI scorecard response missing scores[] array");
50237
+ }
50238
+ const known = new Set(rubric.criteria.map((c) => c.id));
50239
+ const parsed = [];
50240
+ for (const entry of scoresRaw) {
50241
+ if (!entry || typeof entry !== "object")
50242
+ continue;
50243
+ const item = entry;
50244
+ const criterionId = String(item.criterionId ?? "").trim();
50245
+ if (!criterionId || !known.has(criterionId))
50246
+ continue;
50247
+ const scoreValue = Number(item.score);
50248
+ if (Number.isNaN(scoreValue))
50249
+ continue;
50250
+ parsed.push({
50251
+ criterionId,
50252
+ score: scoreValue,
50253
+ ...typeof item.rationale === "string" ? { rationale: item.rationale } : {}
50254
+ });
50255
+ }
50256
+ const comments = typeof root.comments === "string" ? root.comments : undefined;
50257
+ return {
50258
+ scores: parsed,
50259
+ ...comments !== undefined ? { comments } : {}
50260
+ };
50261
+ };
50262
+ var createVoiceAIScorecard = (options) => {
50263
+ const systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT3;
50264
+ return {
50265
+ async scoreCall(input) {
50266
+ const prompt = buildPrompt2(input);
50267
+ const raw = await options.completion({ prompt, systemPrompt });
50268
+ const parsed = parseVoiceAIScorecardResponse(raw, input.rubric);
50269
+ const scoreMap = {};
50270
+ for (const entry of parsed.scores) {
50271
+ scoreMap[entry.criterionId] = {
50272
+ score: entry.score,
50273
+ ...entry.rationale !== undefined ? { rationale: entry.rationale } : {}
50274
+ };
50275
+ }
50276
+ for (const criterion of input.rubric.criteria) {
50277
+ if (!scoreMap[criterion.id]) {
50278
+ scoreMap[criterion.id] = {
50279
+ rationale: "No rationale returned by AI scorer",
50280
+ score: 0
50281
+ };
50282
+ }
50283
+ }
50284
+ return buildVoiceCallScorecard({
50285
+ reviewer: "llm",
50286
+ rubric: input.rubric,
50287
+ scores: scoreMap,
50288
+ sessionId: input.sessionId,
50289
+ ...input.agentId !== undefined ? { agentId: input.agentId } : {},
50290
+ ...input.reviewerId !== undefined ? { reviewerId: input.reviewerId } : {},
50291
+ ...parsed.comments !== undefined ? { comments: parsed.comments } : {},
50292
+ ...input.now !== undefined ? { now: input.now } : {}
50293
+ });
50294
+ }
50295
+ };
50296
+ };
50297
+ // src/agentPerformanceReport.ts
50298
+ var bucketKeyFor = (ms, bucket) => {
50299
+ const date = new Date(ms);
50300
+ const year = date.getUTCFullYear();
50301
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
50302
+ const day = String(date.getUTCDate()).padStart(2, "0");
50303
+ if (bucket === "day")
50304
+ return `${year}-${month}-${day}`;
50305
+ if (bucket === "month")
50306
+ return `${year}-${month}`;
50307
+ const firstJan = Date.UTC(year, 0, 1);
50308
+ const week = Math.floor((ms - firstJan) / (7 * 24 * 60 * 60 * 1000)) + 1;
50309
+ return `${year}-W${String(week).padStart(2, "0")}`;
50310
+ };
50311
+ var buildVoiceAgentPerformanceReport = (input) => {
50312
+ const bucket = input.bucket ?? "week";
50313
+ const scorecards = input.scorecards.filter((card) => card.agentId === input.agentId && card.rubricId === input.rubricId).filter((card) => (input.fromMs === undefined || card.createdAt >= input.fromMs) && (input.toMs === undefined || card.createdAt <= input.toMs)).sort((a, b) => a.createdAt - b.createdAt);
50314
+ const bucketMap = new Map;
50315
+ for (const card of scorecards) {
50316
+ const key = bucketKeyFor(card.createdAt, bucket);
50317
+ const entry = bucketMap.get(key) ?? {
50318
+ fail: 0,
50319
+ needsReview: 0,
50320
+ pass: 0,
50321
+ sum: 0,
50322
+ total: 0
50323
+ };
50324
+ entry.total += 1;
50325
+ entry.sum += card.weightedScore;
50326
+ if (card.grade === "pass")
50327
+ entry.pass += 1;
50328
+ else if (card.grade === "needs-review")
50329
+ entry.needsReview += 1;
50330
+ else
50331
+ entry.fail += 1;
50332
+ bucketMap.set(key, entry);
50333
+ }
50334
+ const buckets = Array.from(bucketMap.entries()).sort((a, b) => a[0] < b[0] ? -1 : 1).map(([bucketKey2, e]) => ({
50335
+ averageWeightedScore: e.total > 0 ? e.sum / e.total : 0,
50336
+ bucketKey: bucketKey2,
50337
+ callsScored: e.total,
50338
+ failRate: e.total > 0 ? e.fail / e.total : 0,
50339
+ needsReviewRate: e.total > 0 ? e.needsReview / e.total : 0,
50340
+ passRate: e.total > 0 ? e.pass / e.total : 0
50341
+ }));
50342
+ const criterionMap = new Map;
50343
+ for (const card of scorecards) {
50344
+ for (const result of card.results) {
50345
+ const entry = criterionMap.get(result.criterionId) ?? {
50346
+ firstAvg: null,
50347
+ passes: 0,
50348
+ scoreSum: 0,
50349
+ total: 0
50350
+ };
50351
+ entry.scoreSum += result.score / card.scaleMax;
50352
+ entry.total += 1;
50353
+ if (result.passed)
50354
+ entry.passes += 1;
50355
+ criterionMap.set(result.criterionId, entry);
50356
+ }
50357
+ }
50358
+ const firstHalf = scorecards.slice(0, Math.max(1, Math.floor(scorecards.length / 2)));
50359
+ const secondHalf = scorecards.slice(Math.floor(scorecards.length / 2));
50360
+ const halfAverage = (cards, criterionId) => {
50361
+ const matches = cards.flatMap((c) => c.results.filter((r) => r.criterionId === criterionId).map((r) => r.score / c.scaleMax));
50362
+ if (matches.length === 0)
50363
+ return null;
50364
+ return matches.reduce((a, b) => a + b, 0) / matches.length;
50365
+ };
50366
+ const criteria = [];
50367
+ for (const [criterionId, e] of criterionMap) {
50368
+ const earlier = halfAverage(firstHalf, criterionId);
50369
+ const later = halfAverage(secondHalf, criterionId);
50370
+ let trend = "flat";
50371
+ let delta = 0;
50372
+ if (earlier !== null && later !== null) {
50373
+ delta = later - earlier;
50374
+ if (delta > 0.05)
50375
+ trend = "up";
50376
+ else if (delta < -0.05)
50377
+ trend = "down";
50378
+ }
50379
+ criteria.push({
50380
+ averageScore: e.total > 0 ? e.scoreSum / e.total : 0,
50381
+ criterionId,
50382
+ delta,
50383
+ passRate: e.total > 0 ? e.passes / e.total : 0,
50384
+ trend
50385
+ });
50386
+ }
50387
+ criteria.sort((a, b) => a.criterionId.localeCompare(b.criterionId));
50388
+ const overallTotal = scorecards.length;
50389
+ const overallSum = scorecards.reduce((s, c) => s + c.weightedScore, 0);
50390
+ const overallPasses = scorecards.filter((c) => c.grade === "pass").length;
50391
+ const ranked = [...criteria].sort((a, b) => a.averageScore - b.averageScore);
50392
+ return {
50393
+ agentId: input.agentId,
50394
+ bestCriterion: ranked.at(-1)?.criterionId ?? null,
50395
+ bucket,
50396
+ buckets,
50397
+ criteria,
50398
+ fromMs: input.fromMs ?? scorecards[0]?.createdAt ?? 0,
50399
+ overallAverageScore: overallTotal > 0 ? overallSum / overallTotal : 0,
50400
+ overallPassRate: overallTotal > 0 ? overallPasses / overallTotal : 0,
50401
+ rubricId: input.rubricId,
50402
+ toMs: input.toMs ?? scorecards.at(-1)?.createdAt ?? 0,
50403
+ totalCalls: overallTotal,
50404
+ worstCriterion: ranked[0]?.criterionId ?? null
50405
+ };
50406
+ };
50407
+ // src/scorecardCalibration.ts
50408
+ var normalize = (raw, scaleMax) => scaleMax === 0 ? 0 : raw / scaleMax;
50409
+ var correlation = (xs, ys) => {
50410
+ if (xs.length === 0 || xs.length !== ys.length)
50411
+ return 0;
50412
+ const meanX = xs.reduce((a, b) => a + b, 0) / xs.length;
50413
+ const meanY = ys.reduce((a, b) => a + b, 0) / ys.length;
50414
+ let num = 0;
50415
+ let denomX = 0;
50416
+ let denomY = 0;
50417
+ for (let i = 0;i < xs.length; i++) {
50418
+ const dx = (xs[i] ?? 0) - meanX;
50419
+ const dy = (ys[i] ?? 0) - meanY;
50420
+ num += dx * dy;
50421
+ denomX += dx * dx;
50422
+ denomY += dy * dy;
50423
+ }
50424
+ if (denomX === 0 || denomY === 0)
50425
+ return 0;
50426
+ return num / Math.sqrt(denomX * denomY);
50427
+ };
50428
+ var computeVoiceScorecardCalibration = (pairs, options = {}) => {
50429
+ if (pairs.length === 0) {
50430
+ return {
50431
+ gradeAgreementRate: 0,
50432
+ meanAbsoluteError: 0,
50433
+ pairsCompared: 0,
50434
+ perCriterion: [],
50435
+ rootMeanSquareError: 0,
50436
+ weightedScoreCorrelation: 0,
50437
+ worstDivergences: []
50438
+ };
50439
+ }
50440
+ const topN = options.topDivergences ?? 10;
50441
+ const gapsByCriterion = new Map;
50442
+ const allGaps = [];
50443
+ const divergences = [];
50444
+ const humanWeighted = [];
50445
+ const llmWeighted = [];
50446
+ let gradeAgreed = 0;
50447
+ let comparedPairs = 0;
50448
+ for (const pair of pairs) {
50449
+ if (pair.human.rubricId !== pair.llm.rubricId)
50450
+ continue;
50451
+ comparedPairs += 1;
50452
+ if (pair.human.grade === pair.llm.grade)
50453
+ gradeAgreed += 1;
50454
+ humanWeighted.push(pair.human.weightedScore);
50455
+ llmWeighted.push(pair.llm.weightedScore);
50456
+ const humanByCriterion = new Map(pair.human.results.map((r) => [r.criterionId, r]));
50457
+ const llmByCriterion = new Map(pair.llm.results.map((r) => [r.criterionId, r]));
50458
+ const criteriaIds = new Set([
50459
+ ...humanByCriterion.keys(),
50460
+ ...llmByCriterion.keys()
50461
+ ]);
50462
+ for (const criterionId of criteriaIds) {
50463
+ const h = humanByCriterion.get(criterionId);
50464
+ const l = llmByCriterion.get(criterionId);
50465
+ if (!h || !l)
50466
+ continue;
50467
+ const hn = normalize(h.score, pair.human.scaleMax);
50468
+ const ln = normalize(l.score, pair.llm.scaleMax);
50469
+ const gap = Math.abs(hn - ln);
50470
+ allGaps.push(gap);
50471
+ divergences.push({
50472
+ criterionId,
50473
+ humanScore: hn,
50474
+ llmScore: ln,
50475
+ normalizedGap: hn - ln,
50476
+ sessionId: pair.sessionId
50477
+ });
50478
+ const entry = gapsByCriterion.get(criterionId) ?? {
50479
+ absSum: 0,
50480
+ biasSum: 0,
50481
+ count: 0,
50482
+ humanSum: 0,
50483
+ llmSum: 0
50484
+ };
50485
+ entry.absSum += gap;
50486
+ entry.biasSum += ln - hn;
50487
+ entry.humanSum += hn;
50488
+ entry.llmSum += ln;
50489
+ entry.count += 1;
50490
+ gapsByCriterion.set(criterionId, entry);
50491
+ }
50492
+ }
50493
+ const mae = allGaps.length === 0 ? 0 : allGaps.reduce((a, b) => a + b, 0) / allGaps.length;
50494
+ const rmse = allGaps.length === 0 ? 0 : Math.sqrt(allGaps.reduce((a, b) => a + b * b, 0) / allGaps.length);
50495
+ const perCriterion = Array.from(gapsByCriterion.entries()).map(([criterionId, e]) => ({
50496
+ averageHumanScore: e.count === 0 ? 0 : e.humanSum / e.count,
50497
+ averageLLMScore: e.count === 0 ? 0 : e.llmSum / e.count,
50498
+ bias: e.count === 0 ? 0 : e.biasSum / e.count,
50499
+ criterionId,
50500
+ meanAbsoluteError: e.count === 0 ? 0 : e.absSum / e.count
50501
+ }));
50502
+ return {
50503
+ gradeAgreementRate: comparedPairs === 0 ? 0 : gradeAgreed / comparedPairs,
50504
+ meanAbsoluteError: mae,
50505
+ pairsCompared: comparedPairs,
50506
+ perCriterion,
50507
+ rootMeanSquareError: rmse,
50508
+ weightedScoreCorrelation: correlation(humanWeighted, llmWeighted),
50509
+ worstDivergences: divergences.sort((a, b) => Math.abs(b.normalizedGap) - Math.abs(a.normalizedGap)).slice(0, topN)
50510
+ };
50511
+ };
50512
+ // src/qualityDriftDetector.ts
50513
+ var severityFor = (delta, watch, regression) => {
50514
+ if (delta <= -regression)
50515
+ return "regression";
50516
+ if (delta <= -watch)
50517
+ return "watch";
50518
+ return "ok";
50519
+ };
50520
+ var averageScore = (cards) => cards.length === 0 ? 0 : cards.reduce((sum, c) => sum + c.weightedScore, 0) / cards.length;
50521
+ var averageCriterion = (cards, criterionId) => {
50522
+ const matches = [];
50523
+ for (const card of cards) {
50524
+ for (const result of card.results) {
50525
+ if (result.criterionId === criterionId) {
50526
+ matches.push(result.score / card.scaleMax);
50527
+ }
50528
+ }
50529
+ }
50530
+ return matches.length === 0 ? 0 : matches.reduce((a, b) => a + b, 0) / matches.length;
50531
+ };
50532
+ var detectVoiceQualityDrift = (input) => {
50533
+ const now = input.now ?? (() => Date.now());
50534
+ const currentWindow = input.currentWindowMs ?? 7 * 24 * 60 * 60 * 1000;
50535
+ const baselineWindow = input.baselineWindowMs ?? 30 * 24 * 60 * 60 * 1000;
50536
+ const watch = input.watchThreshold ?? 0.05;
50537
+ const regression = input.regressionThreshold ?? 0.1;
50538
+ const cutoff = now();
50539
+ const currentFrom = cutoff - currentWindow;
50540
+ const baselineFrom = cutoff - currentWindow - baselineWindow;
50541
+ const baselineTo = currentFrom;
50542
+ const relevant = input.scorecards.filter((card) => card.rubricId === input.rubricId);
50543
+ const baselineCards = relevant.filter((c) => c.createdAt >= baselineFrom && c.createdAt < baselineTo);
50544
+ const currentCards = relevant.filter((c) => c.createdAt >= currentFrom && c.createdAt <= cutoff);
50545
+ const baselineAvg = averageScore(baselineCards);
50546
+ const currentAvg = averageScore(currentCards);
50547
+ const overallDelta = currentAvg - baselineAvg;
50548
+ const criterionIds = new Set;
50549
+ for (const card of relevant) {
50550
+ for (const result of card.results)
50551
+ criterionIds.add(result.criterionId);
50552
+ }
50553
+ const criteria = [];
50554
+ for (const criterionId of criterionIds) {
50555
+ const baseline = averageCriterion(baselineCards, criterionId);
50556
+ const current = averageCriterion(currentCards, criterionId);
50557
+ const delta = current - baseline;
50558
+ const severity = severityFor(delta, watch, regression);
50559
+ criteria.push({
50560
+ baselineAverage: baseline,
50561
+ criterionId,
50562
+ currentAverage: current,
50563
+ delta,
50564
+ severity
50565
+ });
50566
+ }
50567
+ criteria.sort((a, b) => a.delta - b.delta);
50568
+ const alertCount = criteria.filter((c) => c.severity !== "ok").length;
50569
+ return {
50570
+ alertCount,
50571
+ baselineWindow: {
50572
+ from: baselineFrom,
50573
+ sampleSize: baselineCards.length,
50574
+ to: baselineTo
50575
+ },
50576
+ criteria,
50577
+ currentWindow: {
50578
+ from: currentFrom,
50579
+ sampleSize: currentCards.length,
50580
+ to: cutoff
50581
+ },
50582
+ overall: {
50583
+ baselineAverage: baselineAvg,
50584
+ currentAverage: currentAvg,
50585
+ delta: overallDelta,
50586
+ severity: severityFor(overallDelta, watch, regression)
50587
+ },
50588
+ rubricId: input.rubricId,
50589
+ scope: { from: baselineFrom, to: cutoff }
50590
+ };
50591
+ };
50089
50592
  export {
50090
50593
  writeVoiceProofPack,
50091
50594
  writeVoiceMediaPipelineArtifacts,
@@ -50338,6 +50841,7 @@ export {
50338
50841
  predictVoiceCallCost,
50339
50842
  parseVoiceTelephonyWebhookEvent,
50340
50843
  parseVoiceSessionSnapshot,
50844
+ parseVoiceAIScorecardResponse,
50341
50845
  normalizeVoiceProofTrendReport,
50342
50846
  normalizePhoneNumber,
50343
50847
  muteVoiceMonitorIssue,
@@ -50416,6 +50920,7 @@ export {
50416
50920
  encodeTwilioMulawBase64,
50417
50921
  encodeStereoWav,
50418
50922
  encodePcmAsWav,
50923
+ detectVoiceQualityDrift,
50419
50924
  describeVoiceIVRPlan,
50420
50925
  describeVoiceAssistantMode,
50421
50926
  describeVoiceAgentUIState,
@@ -50784,6 +51289,7 @@ export {
50784
51289
  createVoiceAgentTool,
50785
51290
  createVoiceAgentSquad,
50786
51291
  createVoiceAgent,
51292
+ createVoiceAIScorecard,
50787
51293
  createVoiceAIJudgeCompletion,
50788
51294
  createTwilioVoiceRoutes,
50789
51295
  createTwilioVoiceResponse,
@@ -50823,6 +51329,7 @@ export {
50823
51329
  createAnthropicVoiceAssistantModel,
50824
51330
  createAIVoiceModel,
50825
51331
  conditionAudioChunk,
51332
+ computeVoiceScorecardCalibration,
50826
51333
  computePcmDurationMs,
50827
51334
  completeVoiceOpsTask,
50828
51335
  compareVoiceEvalBaseline,
@@ -50916,11 +51423,13 @@ export {
50916
51423
  buildVoiceCompetitiveCoverageReport,
50917
51424
  buildVoiceCampaignObservabilityReport,
50918
51425
  buildVoiceCallerMemoryNamespace,
51426
+ buildVoiceCallScorecard,
50919
51427
  buildVoiceCallDebuggerReport,
50920
51428
  buildVoiceBrowserCallProfileReport,
50921
51429
  buildVoiceAuditTrailReport,
50922
51430
  buildVoiceAuditExport,
50923
51431
  buildVoiceAuditDeliveryReport,
51432
+ buildVoiceAgentPerformanceReport,
50924
51433
  buildReplayTimelineReport,
50925
51434
  buildOTELTraceId,
50926
51435
  buildOTELSpanId,
@@ -50985,6 +51494,7 @@ export {
50985
51494
  VOICE_DTMF_DIGITS,
50986
51495
  VOICE_CALLER_MEMORY_KEY,
50987
51496
  TURN_PROFILE_DEFAULTS,
51497
+ DEFAULT_VOICE_SALES_RUBRIC,
50988
51498
  DEFAULT_VOICE_REMINDER_TRIGGERS,
50989
51499
  DEFAULT_VOICE_REDACTION_PATTERNS,
50990
51500
  DEFAULT_VOICE_PROOF_TREND_PROFILE_DEFINITIONS,
@@ -0,0 +1,44 @@
1
+ import type { VoiceScorecard } from "./callScorecard";
2
+ export type VoiceQualityDriftSeverity = "ok" | "watch" | "regression";
3
+ export type VoiceQualityDriftCriterionAlert = {
4
+ criterionId: string;
5
+ baselineAverage: number;
6
+ currentAverage: number;
7
+ delta: number;
8
+ severity: VoiceQualityDriftSeverity;
9
+ };
10
+ export type VoiceQualityDriftReport = {
11
+ rubricId: string;
12
+ scope: {
13
+ from: number;
14
+ to: number;
15
+ };
16
+ baselineWindow: {
17
+ from: number;
18
+ to: number;
19
+ sampleSize: number;
20
+ };
21
+ currentWindow: {
22
+ from: number;
23
+ to: number;
24
+ sampleSize: number;
25
+ };
26
+ overall: {
27
+ baselineAverage: number;
28
+ currentAverage: number;
29
+ delta: number;
30
+ severity: VoiceQualityDriftSeverity;
31
+ };
32
+ criteria: VoiceQualityDriftCriterionAlert[];
33
+ alertCount: number;
34
+ };
35
+ export type DetectVoiceQualityDriftInput = {
36
+ rubricId: string;
37
+ scorecards: VoiceScorecard[];
38
+ baselineWindowMs?: number;
39
+ currentWindowMs?: number;
40
+ now?: () => number;
41
+ watchThreshold?: number;
42
+ regressionThreshold?: number;
43
+ };
44
+ export declare const detectVoiceQualityDrift: (input: DetectVoiceQualityDriftInput) => VoiceQualityDriftReport;
@@ -0,0 +1,31 @@
1
+ import type { VoiceScorecard } from "./callScorecard";
2
+ export type VoiceScorecardCalibrationPair = {
3
+ sessionId: string;
4
+ human: VoiceScorecard;
5
+ llm: VoiceScorecard;
6
+ };
7
+ export type VoiceScorecardCalibrationDivergence = {
8
+ sessionId: string;
9
+ criterionId: string;
10
+ humanScore: number;
11
+ llmScore: number;
12
+ normalizedGap: number;
13
+ };
14
+ export type VoiceScorecardCalibrationReport = {
15
+ pairsCompared: number;
16
+ meanAbsoluteError: number;
17
+ rootMeanSquareError: number;
18
+ weightedScoreCorrelation: number;
19
+ gradeAgreementRate: number;
20
+ perCriterion: {
21
+ criterionId: string;
22
+ meanAbsoluteError: number;
23
+ averageHumanScore: number;
24
+ averageLLMScore: number;
25
+ bias: number;
26
+ }[];
27
+ worstDivergences: VoiceScorecardCalibrationDivergence[];
28
+ };
29
+ export declare const computeVoiceScorecardCalibration: (pairs: VoiceScorecardCalibrationPair[], options?: {
30
+ topDivergences?: number;
31
+ }) => VoiceScorecardCalibrationReport;
@@ -53,5 +53,5 @@ export declare const VoiceCostDashboard: import("vue").DefineComponent<import("v
53
53
  title: string;
54
54
  currency: string;
55
55
  emptyMessage: string;
56
- bucketBy: "day" | "hour" | "month" | undefined;
56
+ bucketBy: "day" | "month" | "hour" | undefined;
57
57
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.512",
3
+ "version": "0.0.22-beta.513",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",