@absolutejs/voice 0.0.22-beta.512 → 0.0.22-beta.514
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/agentPerformanceReport.d.ts +40 -0
- package/dist/aiScorecard.d.ts +32 -0
- package/dist/callScorecard.d.ts +53 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +1291 -0
- package/dist/pathway.d.ts +94 -0
- package/dist/pathwayCompiler.d.ts +31 -0
- package/dist/pathwayRuntime.d.ts +57 -0
- package/dist/pathwaySlotCollector.d.ts +29 -0
- package/dist/pathwayVisualizer.d.ts +8 -0
- package/dist/qualityDriftDetector.d.ts +44 -0
- package/dist/scorecardCalibration.d.ts +31 -0
- package/dist/vue/VoiceCostDashboard.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -50086,6 +50086,1280 @@ 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
|
+
};
|
|
50592
|
+
// src/pathway.ts
|
|
50593
|
+
var slotRefsInActions = (actions) => {
|
|
50594
|
+
if (!actions)
|
|
50595
|
+
return [];
|
|
50596
|
+
const refs = [];
|
|
50597
|
+
for (const action of actions) {
|
|
50598
|
+
if (action.kind === "collect-slot")
|
|
50599
|
+
refs.push(action.slotId);
|
|
50600
|
+
if (action.kind === "set-slot")
|
|
50601
|
+
refs.push(action.slotId);
|
|
50602
|
+
if (action.kind === "call-tool" && action.argsFromSlots) {
|
|
50603
|
+
refs.push(...action.argsFromSlots);
|
|
50604
|
+
}
|
|
50605
|
+
}
|
|
50606
|
+
return refs;
|
|
50607
|
+
};
|
|
50608
|
+
var slotRefsInCondition = (condition) => {
|
|
50609
|
+
if (condition.kind === "always" || condition.kind === "fallback")
|
|
50610
|
+
return [];
|
|
50611
|
+
return [condition.slotId];
|
|
50612
|
+
};
|
|
50613
|
+
var validateVoicePathway = (pathway) => {
|
|
50614
|
+
const issues = [];
|
|
50615
|
+
const stateIds = new Set;
|
|
50616
|
+
for (const state of pathway.states) {
|
|
50617
|
+
if (stateIds.has(state.id)) {
|
|
50618
|
+
issues.push({
|
|
50619
|
+
code: "duplicate-state",
|
|
50620
|
+
message: `Duplicate state id: ${state.id}`,
|
|
50621
|
+
severity: "error",
|
|
50622
|
+
stateId: state.id
|
|
50623
|
+
});
|
|
50624
|
+
}
|
|
50625
|
+
stateIds.add(state.id);
|
|
50626
|
+
}
|
|
50627
|
+
const slotIds = new Set;
|
|
50628
|
+
for (const slot of pathway.slots) {
|
|
50629
|
+
if (slotIds.has(slot.id)) {
|
|
50630
|
+
issues.push({
|
|
50631
|
+
code: "duplicate-slot",
|
|
50632
|
+
message: `Duplicate slot id: ${slot.id}`,
|
|
50633
|
+
severity: "error",
|
|
50634
|
+
slotId: slot.id
|
|
50635
|
+
});
|
|
50636
|
+
}
|
|
50637
|
+
slotIds.add(slot.id);
|
|
50638
|
+
}
|
|
50639
|
+
if (!stateIds.has(pathway.entryStateId)) {
|
|
50640
|
+
issues.push({
|
|
50641
|
+
code: "unknown-entry",
|
|
50642
|
+
message: `Entry state ${pathway.entryStateId} is not defined`,
|
|
50643
|
+
severity: "error"
|
|
50644
|
+
});
|
|
50645
|
+
}
|
|
50646
|
+
for (const state of pathway.states) {
|
|
50647
|
+
const seenTransitionKeys = new Set;
|
|
50648
|
+
state.transitions.forEach((transition, index) => {
|
|
50649
|
+
if (!stateIds.has(transition.to)) {
|
|
50650
|
+
issues.push({
|
|
50651
|
+
code: "missing-transition-target",
|
|
50652
|
+
message: `State ${state.id} transitions to unknown state ${transition.to}`,
|
|
50653
|
+
severity: "error",
|
|
50654
|
+
stateId: state.id
|
|
50655
|
+
});
|
|
50656
|
+
}
|
|
50657
|
+
const key = `${transition.to}::${transition.condition.kind}::${"slotId" in transition.condition ? transition.condition.slotId : ""}`;
|
|
50658
|
+
if (seenTransitionKeys.has(key)) {
|
|
50659
|
+
issues.push({
|
|
50660
|
+
code: "duplicate-transition",
|
|
50661
|
+
message: `State ${state.id} has duplicate transition to ${transition.to}`,
|
|
50662
|
+
severity: "warning",
|
|
50663
|
+
stateId: state.id
|
|
50664
|
+
});
|
|
50665
|
+
}
|
|
50666
|
+
seenTransitionKeys.add(key);
|
|
50667
|
+
if (transition.condition.kind === "fallback" && index !== state.transitions.length - 1) {
|
|
50668
|
+
issues.push({
|
|
50669
|
+
code: "fallback-not-last",
|
|
50670
|
+
message: `Fallback transition in ${state.id} must be the last transition`,
|
|
50671
|
+
severity: "error",
|
|
50672
|
+
stateId: state.id
|
|
50673
|
+
});
|
|
50674
|
+
}
|
|
50675
|
+
for (const ref of slotRefsInCondition(transition.condition)) {
|
|
50676
|
+
if (!slotIds.has(ref)) {
|
|
50677
|
+
issues.push({
|
|
50678
|
+
code: "missing-slot-ref",
|
|
50679
|
+
message: `Transition condition references unknown slot ${ref}`,
|
|
50680
|
+
severity: "error",
|
|
50681
|
+
slotId: ref,
|
|
50682
|
+
stateId: state.id
|
|
50683
|
+
});
|
|
50684
|
+
}
|
|
50685
|
+
}
|
|
50686
|
+
});
|
|
50687
|
+
for (const ref of slotRefsInActions(state.actions)) {
|
|
50688
|
+
if (!slotIds.has(ref)) {
|
|
50689
|
+
issues.push({
|
|
50690
|
+
code: "missing-slot-ref",
|
|
50691
|
+
message: `Action in ${state.id} references unknown slot ${ref}`,
|
|
50692
|
+
severity: "error",
|
|
50693
|
+
slotId: ref,
|
|
50694
|
+
stateId: state.id
|
|
50695
|
+
});
|
|
50696
|
+
}
|
|
50697
|
+
}
|
|
50698
|
+
}
|
|
50699
|
+
const reachable = new Set;
|
|
50700
|
+
const queue = stateIds.has(pathway.entryStateId) ? [pathway.entryStateId] : [];
|
|
50701
|
+
const stateById = new Map(pathway.states.map((s) => [s.id, s]));
|
|
50702
|
+
while (queue.length > 0) {
|
|
50703
|
+
const id = queue.shift();
|
|
50704
|
+
if (reachable.has(id))
|
|
50705
|
+
continue;
|
|
50706
|
+
reachable.add(id);
|
|
50707
|
+
const state = stateById.get(id);
|
|
50708
|
+
if (!state)
|
|
50709
|
+
continue;
|
|
50710
|
+
for (const transition of state.transitions) {
|
|
50711
|
+
if (stateIds.has(transition.to) && !reachable.has(transition.to)) {
|
|
50712
|
+
queue.push(transition.to);
|
|
50713
|
+
}
|
|
50714
|
+
}
|
|
50715
|
+
}
|
|
50716
|
+
for (const state of pathway.states) {
|
|
50717
|
+
if (!reachable.has(state.id)) {
|
|
50718
|
+
issues.push({
|
|
50719
|
+
code: "unreachable-state",
|
|
50720
|
+
message: `State ${state.id} is not reachable from entry ${pathway.entryStateId}`,
|
|
50721
|
+
severity: "warning",
|
|
50722
|
+
stateId: state.id
|
|
50723
|
+
});
|
|
50724
|
+
}
|
|
50725
|
+
}
|
|
50726
|
+
const hasReachableTerminal = pathway.states.some((state) => reachable.has(state.id) && (state.kind === "terminal" || state.transitions.length === 0));
|
|
50727
|
+
if (!hasReachableTerminal) {
|
|
50728
|
+
issues.push({
|
|
50729
|
+
code: "no-terminal-reachable",
|
|
50730
|
+
message: "No terminal state is reachable; pathway has no exit condition",
|
|
50731
|
+
severity: "error"
|
|
50732
|
+
});
|
|
50733
|
+
}
|
|
50734
|
+
const fatal = issues.some((i) => i.severity === "error");
|
|
50735
|
+
return {
|
|
50736
|
+
issues,
|
|
50737
|
+
reachableStates: Array.from(reachable),
|
|
50738
|
+
valid: !fatal
|
|
50739
|
+
};
|
|
50740
|
+
};
|
|
50741
|
+
var findVoicePathwayState = (pathway, id) => pathway.states.find((s) => s.id === id) ?? null;
|
|
50742
|
+
var findVoicePathwaySlot = (pathway, id) => pathway.slots.find((s) => s.id === id) ?? null;
|
|
50743
|
+
// src/pathwayRuntime.ts
|
|
50744
|
+
var evaluateCondition = (condition, slots) => {
|
|
50745
|
+
switch (condition.kind) {
|
|
50746
|
+
case "always":
|
|
50747
|
+
return true;
|
|
50748
|
+
case "fallback":
|
|
50749
|
+
return true;
|
|
50750
|
+
case "slot-filled":
|
|
50751
|
+
return slots[condition.slotId] !== undefined && slots[condition.slotId] !== null && slots[condition.slotId] !== "";
|
|
50752
|
+
case "slot-equals":
|
|
50753
|
+
return slots[condition.slotId] === condition.value;
|
|
50754
|
+
case "slot-matches": {
|
|
50755
|
+
const raw = slots[condition.slotId];
|
|
50756
|
+
if (typeof raw !== "string")
|
|
50757
|
+
return false;
|
|
50758
|
+
try {
|
|
50759
|
+
return new RegExp(condition.pattern, "u").test(raw);
|
|
50760
|
+
} catch {
|
|
50761
|
+
return false;
|
|
50762
|
+
}
|
|
50763
|
+
}
|
|
50764
|
+
}
|
|
50765
|
+
};
|
|
50766
|
+
var pickTransition = (state, slots) => {
|
|
50767
|
+
for (const transition of state.transitions) {
|
|
50768
|
+
if (transition.condition.kind === "fallback")
|
|
50769
|
+
continue;
|
|
50770
|
+
if (evaluateCondition(transition.condition, slots))
|
|
50771
|
+
return transition;
|
|
50772
|
+
}
|
|
50773
|
+
const fallback = state.transitions.find((t) => t.condition.kind === "fallback");
|
|
50774
|
+
return fallback ?? null;
|
|
50775
|
+
};
|
|
50776
|
+
var buildPendingActions = (state, slots, pathway, emit2) => {
|
|
50777
|
+
const pending = [];
|
|
50778
|
+
let awaitingSlotId = null;
|
|
50779
|
+
for (const action of state.actions ?? []) {
|
|
50780
|
+
if (action.kind === "say") {
|
|
50781
|
+
emit2({ text: action.text, type: "say" });
|
|
50782
|
+
continue;
|
|
50783
|
+
}
|
|
50784
|
+
if (action.kind === "collect-slot") {
|
|
50785
|
+
if (slots[action.slotId] === undefined || slots[action.slotId] === null) {
|
|
50786
|
+
const slot = findVoicePathwaySlot(pathway, action.slotId);
|
|
50787
|
+
emit2({
|
|
50788
|
+
prompt: slot?.prompt ?? `Please provide ${action.slotId}.`,
|
|
50789
|
+
slotId: action.slotId,
|
|
50790
|
+
type: "ask-slot"
|
|
50791
|
+
});
|
|
50792
|
+
awaitingSlotId = action.slotId;
|
|
50793
|
+
break;
|
|
50794
|
+
}
|
|
50795
|
+
continue;
|
|
50796
|
+
}
|
|
50797
|
+
if (action.kind === "call-tool") {
|
|
50798
|
+
const args = {};
|
|
50799
|
+
for (const id of action.argsFromSlots ?? []) {
|
|
50800
|
+
args[id] = slots[id] ?? null;
|
|
50801
|
+
}
|
|
50802
|
+
emit2({ call: { args, toolId: action.toolId }, type: "tool-call" });
|
|
50803
|
+
continue;
|
|
50804
|
+
}
|
|
50805
|
+
if (action.kind === "set-slot") {
|
|
50806
|
+
pending.push(action);
|
|
50807
|
+
continue;
|
|
50808
|
+
}
|
|
50809
|
+
if (action.kind === "transfer") {
|
|
50810
|
+
emit2({ destination: action.destination, type: "transfer" });
|
|
50811
|
+
return { actions: pending, awaitingSlotId: null, ended: true, reason: `transfer:${action.destination}` };
|
|
50812
|
+
}
|
|
50813
|
+
if (action.kind === "end-call") {
|
|
50814
|
+
emit2({ ...action.reason !== undefined ? { reason: action.reason } : {}, type: "end-call" });
|
|
50815
|
+
return { actions: pending, awaitingSlotId: null, ended: true, ...action.reason !== undefined ? { reason: action.reason } : {} };
|
|
50816
|
+
}
|
|
50817
|
+
}
|
|
50818
|
+
return { actions: pending, awaitingSlotId, ended: false };
|
|
50819
|
+
};
|
|
50820
|
+
var createVoicePathwayRuntime = (options) => {
|
|
50821
|
+
if (!options.skipValidation) {
|
|
50822
|
+
const report = validateVoicePathway(options.pathway);
|
|
50823
|
+
if (!report.valid) {
|
|
50824
|
+
throw new Error(`Invalid pathway: ${report.issues.filter((i) => i.severity === "error").map((i) => i.message).join("; ")}`);
|
|
50825
|
+
}
|
|
50826
|
+
}
|
|
50827
|
+
const now = options.now ?? (() => Date.now());
|
|
50828
|
+
const listeners = new Set;
|
|
50829
|
+
const emit2 = (event) => {
|
|
50830
|
+
for (const l of listeners)
|
|
50831
|
+
l(event);
|
|
50832
|
+
};
|
|
50833
|
+
let state = {
|
|
50834
|
+
awaitingSlotId: null,
|
|
50835
|
+
currentStateId: options.pathway.entryStateId,
|
|
50836
|
+
history: [],
|
|
50837
|
+
pendingActions: [],
|
|
50838
|
+
slots: { ...options.initialSlots ?? {} },
|
|
50839
|
+
status: "ready"
|
|
50840
|
+
};
|
|
50841
|
+
const enter = (stateId) => {
|
|
50842
|
+
const target = findVoicePathwayState(options.pathway, stateId);
|
|
50843
|
+
if (!target) {
|
|
50844
|
+
state = { ...state, lastError: `Unknown state ${stateId}`, status: "errored" };
|
|
50845
|
+
emit2({ message: state.lastError, type: "errored" });
|
|
50846
|
+
return;
|
|
50847
|
+
}
|
|
50848
|
+
state = {
|
|
50849
|
+
...state,
|
|
50850
|
+
currentStateId: stateId,
|
|
50851
|
+
history: [...state.history, { at: now(), stateId }],
|
|
50852
|
+
pendingActions: []
|
|
50853
|
+
};
|
|
50854
|
+
emit2({ stateId, type: "state-entered" });
|
|
50855
|
+
const result = buildPendingActions(target, state.slots, options.pathway, emit2);
|
|
50856
|
+
state = {
|
|
50857
|
+
...state,
|
|
50858
|
+
awaitingSlotId: result.awaitingSlotId,
|
|
50859
|
+
pendingActions: result.actions,
|
|
50860
|
+
status: result.ended ? "ended" : result.awaitingSlotId ? "awaiting-slot" : "branching",
|
|
50861
|
+
...result.reason !== undefined ? { endedReason: result.reason } : {}
|
|
50862
|
+
};
|
|
50863
|
+
if (state.status === "branching")
|
|
50864
|
+
tryTransition();
|
|
50865
|
+
};
|
|
50866
|
+
const tryTransition = () => {
|
|
50867
|
+
const current = findVoicePathwayState(options.pathway, state.currentStateId);
|
|
50868
|
+
if (!current)
|
|
50869
|
+
return;
|
|
50870
|
+
if (current.transitions.length === 0) {
|
|
50871
|
+
state = { ...state, status: "ended" };
|
|
50872
|
+
return;
|
|
50873
|
+
}
|
|
50874
|
+
const transition = pickTransition(current, state.slots);
|
|
50875
|
+
if (!transition) {
|
|
50876
|
+
state = { ...state, status: "awaiting-slot" };
|
|
50877
|
+
return;
|
|
50878
|
+
}
|
|
50879
|
+
enter(transition.to);
|
|
50880
|
+
};
|
|
50881
|
+
const start = () => {
|
|
50882
|
+
state.history = [];
|
|
50883
|
+
enter(options.pathway.entryStateId);
|
|
50884
|
+
};
|
|
50885
|
+
const fillSlot = (slotId, value) => {
|
|
50886
|
+
state = { ...state, slots: { ...state.slots, [slotId]: value } };
|
|
50887
|
+
if (state.awaitingSlotId === slotId) {
|
|
50888
|
+
state = { ...state, awaitingSlotId: null, status: "branching" };
|
|
50889
|
+
const current = findVoicePathwayState(options.pathway, state.currentStateId);
|
|
50890
|
+
if (current) {
|
|
50891
|
+
const result = buildPendingActions(current, state.slots, options.pathway, emit2);
|
|
50892
|
+
state = {
|
|
50893
|
+
...state,
|
|
50894
|
+
awaitingSlotId: result.awaitingSlotId,
|
|
50895
|
+
pendingActions: result.actions,
|
|
50896
|
+
status: result.ended ? "ended" : result.awaitingSlotId ? "awaiting-slot" : "branching",
|
|
50897
|
+
...result.reason !== undefined ? { endedReason: result.reason } : {}
|
|
50898
|
+
};
|
|
50899
|
+
if (state.status === "branching")
|
|
50900
|
+
tryTransition();
|
|
50901
|
+
}
|
|
50902
|
+
}
|
|
50903
|
+
};
|
|
50904
|
+
return {
|
|
50905
|
+
fillSlot,
|
|
50906
|
+
getState: () => state,
|
|
50907
|
+
start,
|
|
50908
|
+
subscribe(listener) {
|
|
50909
|
+
listeners.add(listener);
|
|
50910
|
+
return () => {
|
|
50911
|
+
listeners.delete(listener);
|
|
50912
|
+
};
|
|
50913
|
+
},
|
|
50914
|
+
tryTransition
|
|
50915
|
+
};
|
|
50916
|
+
};
|
|
50917
|
+
// src/pathwaySlotCollector.ts
|
|
50918
|
+
var numberWords = {
|
|
50919
|
+
eight: 8,
|
|
50920
|
+
five: 5,
|
|
50921
|
+
four: 4,
|
|
50922
|
+
nine: 9,
|
|
50923
|
+
one: 1,
|
|
50924
|
+
seven: 7,
|
|
50925
|
+
six: 6,
|
|
50926
|
+
ten: 10,
|
|
50927
|
+
three: 3,
|
|
50928
|
+
two: 2,
|
|
50929
|
+
zero: 0
|
|
50930
|
+
};
|
|
50931
|
+
var parseString = (raw, slot) => {
|
|
50932
|
+
const trimmed = raw.trim();
|
|
50933
|
+
if (!trimmed)
|
|
50934
|
+
return { ok: false, reason: "empty" };
|
|
50935
|
+
const min = slot.validation?.minLength ?? 1;
|
|
50936
|
+
const max = slot.validation?.maxLength ?? Number.MAX_SAFE_INTEGER;
|
|
50937
|
+
if (trimmed.length < min || trimmed.length > max) {
|
|
50938
|
+
return {
|
|
50939
|
+
hint: `Expected ${min}\u2013${max} characters`,
|
|
50940
|
+
ok: false,
|
|
50941
|
+
reason: "out-of-range"
|
|
50942
|
+
};
|
|
50943
|
+
}
|
|
50944
|
+
if (slot.validation?.pattern) {
|
|
50945
|
+
try {
|
|
50946
|
+
if (!new RegExp(slot.validation.pattern, "u").test(trimmed)) {
|
|
50947
|
+
return { ok: false, reason: "no-match" };
|
|
50948
|
+
}
|
|
50949
|
+
} catch {
|
|
50950
|
+
return { ok: false, reason: "no-match" };
|
|
50951
|
+
}
|
|
50952
|
+
}
|
|
50953
|
+
return { normalized: trimmed, ok: true, value: trimmed };
|
|
50954
|
+
};
|
|
50955
|
+
var parseNumber = (raw, slot) => {
|
|
50956
|
+
const trimmed = raw.trim().toLowerCase();
|
|
50957
|
+
if (!trimmed)
|
|
50958
|
+
return { ok: false, reason: "empty" };
|
|
50959
|
+
let value;
|
|
50960
|
+
if (numberWords[trimmed] !== undefined) {
|
|
50961
|
+
value = numberWords[trimmed];
|
|
50962
|
+
} else {
|
|
50963
|
+
const cleaned = trimmed.replace(/[$,]/gu, "");
|
|
50964
|
+
value = Number(cleaned);
|
|
50965
|
+
if (Number.isNaN(value))
|
|
50966
|
+
return { ok: false, reason: "type-mismatch" };
|
|
50967
|
+
}
|
|
50968
|
+
const min = slot.validation?.min ?? Number.NEGATIVE_INFINITY;
|
|
50969
|
+
const max = slot.validation?.max ?? Number.POSITIVE_INFINITY;
|
|
50970
|
+
if (value < min || value > max) {
|
|
50971
|
+
return { hint: `Expected ${min}\u2013${max}`, ok: false, reason: "out-of-range" };
|
|
50972
|
+
}
|
|
50973
|
+
return { normalized: String(value), ok: true, value };
|
|
50974
|
+
};
|
|
50975
|
+
var parseBoolean2 = (raw) => {
|
|
50976
|
+
const trimmed = raw.trim().toLowerCase();
|
|
50977
|
+
if (!trimmed)
|
|
50978
|
+
return { ok: false, reason: "empty" };
|
|
50979
|
+
if (["yes", "yeah", "yep", "correct", "true", "sure", "ok"].includes(trimmed)) {
|
|
50980
|
+
return { normalized: "true", ok: true, value: true };
|
|
50981
|
+
}
|
|
50982
|
+
if (["no", "nope", "nah", "false", "incorrect"].includes(trimmed)) {
|
|
50983
|
+
return { normalized: "false", ok: true, value: false };
|
|
50984
|
+
}
|
|
50985
|
+
return { ok: false, reason: "type-mismatch" };
|
|
50986
|
+
};
|
|
50987
|
+
var parseDate = (raw) => {
|
|
50988
|
+
const trimmed = raw.trim();
|
|
50989
|
+
if (!trimmed)
|
|
50990
|
+
return { ok: false, reason: "empty" };
|
|
50991
|
+
const date = new Date(trimmed);
|
|
50992
|
+
if (Number.isNaN(date.getTime()))
|
|
50993
|
+
return { ok: false, reason: "type-mismatch" };
|
|
50994
|
+
const iso = date.toISOString().slice(0, 10);
|
|
50995
|
+
return { normalized: iso, ok: true, value: iso };
|
|
50996
|
+
};
|
|
50997
|
+
var parseTime2 = (raw) => {
|
|
50998
|
+
const trimmed = raw.trim();
|
|
50999
|
+
const match = /^([0-9]{1,2}):?([0-9]{2})?\s*(am|pm)?$/iu.exec(trimmed);
|
|
51000
|
+
if (!match)
|
|
51001
|
+
return { ok: false, reason: "type-mismatch" };
|
|
51002
|
+
let hour = Number(match[1]);
|
|
51003
|
+
const minute = Number(match[2] ?? "0");
|
|
51004
|
+
const meridiem = match[3]?.toLowerCase();
|
|
51005
|
+
if (meridiem === "pm" && hour < 12)
|
|
51006
|
+
hour += 12;
|
|
51007
|
+
if (meridiem === "am" && hour === 12)
|
|
51008
|
+
hour = 0;
|
|
51009
|
+
if (hour > 23 || minute > 59)
|
|
51010
|
+
return { ok: false, reason: "out-of-range" };
|
|
51011
|
+
const formatted = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
|
51012
|
+
return { normalized: formatted, ok: true, value: formatted };
|
|
51013
|
+
};
|
|
51014
|
+
var parsePhone = (raw) => {
|
|
51015
|
+
const trimmed = raw.trim();
|
|
51016
|
+
if (!trimmed)
|
|
51017
|
+
return { ok: false, reason: "empty" };
|
|
51018
|
+
const digits = trimmed.replace(/\D/gu, "");
|
|
51019
|
+
if (digits.length < 10 || digits.length > 15) {
|
|
51020
|
+
return { ok: false, reason: "type-mismatch" };
|
|
51021
|
+
}
|
|
51022
|
+
const normalized = digits.length === 10 ? `+1${digits}` : `+${digits}`;
|
|
51023
|
+
return { normalized, ok: true, value: normalized };
|
|
51024
|
+
};
|
|
51025
|
+
var parseEmail = (raw) => {
|
|
51026
|
+
const trimmed = raw.trim();
|
|
51027
|
+
if (!trimmed)
|
|
51028
|
+
return { ok: false, reason: "empty" };
|
|
51029
|
+
const collapsed = trimmed.replace(/\s+at\s+/gu, "@").replace(/\s+dot\s+/gu, ".");
|
|
51030
|
+
const ok = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/iu.test(collapsed);
|
|
51031
|
+
if (!ok)
|
|
51032
|
+
return { ok: false, reason: "type-mismatch" };
|
|
51033
|
+
return { normalized: collapsed.toLowerCase(), ok: true, value: collapsed.toLowerCase() };
|
|
51034
|
+
};
|
|
51035
|
+
var parseCurrency = (raw, slot) => {
|
|
51036
|
+
const trimmed = raw.trim().toLowerCase();
|
|
51037
|
+
if (!trimmed)
|
|
51038
|
+
return { ok: false, reason: "empty" };
|
|
51039
|
+
const cleaned = trimmed.replace(/[$,]/gu, "").replace(/\s*(dollars?|usd)$/u, "");
|
|
51040
|
+
const value = Number(cleaned);
|
|
51041
|
+
if (Number.isNaN(value))
|
|
51042
|
+
return { ok: false, reason: "type-mismatch" };
|
|
51043
|
+
const min = slot.validation?.min ?? 0;
|
|
51044
|
+
const max = slot.validation?.max ?? Number.POSITIVE_INFINITY;
|
|
51045
|
+
if (value < min || value > max) {
|
|
51046
|
+
return { hint: `Expected ${min}\u2013${max}`, ok: false, reason: "out-of-range" };
|
|
51047
|
+
}
|
|
51048
|
+
return { normalized: value.toFixed(2), ok: true, value };
|
|
51049
|
+
};
|
|
51050
|
+
var parseChoice = (raw, slot) => {
|
|
51051
|
+
const trimmed = raw.trim().toLowerCase();
|
|
51052
|
+
if (!trimmed)
|
|
51053
|
+
return { ok: false, reason: "empty" };
|
|
51054
|
+
const match = (slot.choices ?? []).find((choice) => choice.toLowerCase() === trimmed);
|
|
51055
|
+
if (!match)
|
|
51056
|
+
return { ok: false, reason: "no-match" };
|
|
51057
|
+
return { normalized: match, ok: true, value: match };
|
|
51058
|
+
};
|
|
51059
|
+
var DEFAULT_VOICE_PATHWAY_SLOT_PARSERS = {
|
|
51060
|
+
boolean: parseBoolean2,
|
|
51061
|
+
choice: parseChoice,
|
|
51062
|
+
currency: parseCurrency,
|
|
51063
|
+
date: parseDate,
|
|
51064
|
+
email: parseEmail,
|
|
51065
|
+
number: parseNumber,
|
|
51066
|
+
phone: parsePhone,
|
|
51067
|
+
string: parseString,
|
|
51068
|
+
time: parseTime2
|
|
51069
|
+
};
|
|
51070
|
+
var createVoicePathwaySlotCollector = (options = {}) => {
|
|
51071
|
+
const parsers = {
|
|
51072
|
+
...DEFAULT_VOICE_PATHWAY_SLOT_PARSERS,
|
|
51073
|
+
...options.parsers ?? {}
|
|
51074
|
+
};
|
|
51075
|
+
const maxAttempts = options.maxAttemptsPerSlot ?? 3;
|
|
51076
|
+
const attemptCounts = new Map;
|
|
51077
|
+
const interpret = (slot, raw) => {
|
|
51078
|
+
const parser = parsers[slot.type];
|
|
51079
|
+
if (!parser) {
|
|
51080
|
+
return {
|
|
51081
|
+
attempt: (attemptCounts.get(slot.id) ?? 0) + 1,
|
|
51082
|
+
raw,
|
|
51083
|
+
result: {
|
|
51084
|
+
hint: `No parser for ${slot.type}`,
|
|
51085
|
+
ok: false,
|
|
51086
|
+
reason: "no-match"
|
|
51087
|
+
},
|
|
51088
|
+
slotId: slot.id
|
|
51089
|
+
};
|
|
51090
|
+
}
|
|
51091
|
+
const next = (attemptCounts.get(slot.id) ?? 0) + 1;
|
|
51092
|
+
attemptCounts.set(slot.id, next);
|
|
51093
|
+
return {
|
|
51094
|
+
attempt: next,
|
|
51095
|
+
raw,
|
|
51096
|
+
result: parser(raw, slot),
|
|
51097
|
+
slotId: slot.id
|
|
51098
|
+
};
|
|
51099
|
+
};
|
|
51100
|
+
const attemptsExceeded = (slotId) => (attemptCounts.get(slotId) ?? 0) >= maxAttempts;
|
|
51101
|
+
return {
|
|
51102
|
+
attemptsExceeded,
|
|
51103
|
+
interpret,
|
|
51104
|
+
reset: (slotId) => {
|
|
51105
|
+
if (slotId)
|
|
51106
|
+
attemptCounts.delete(slotId);
|
|
51107
|
+
else
|
|
51108
|
+
attemptCounts.clear();
|
|
51109
|
+
}
|
|
51110
|
+
};
|
|
51111
|
+
};
|
|
51112
|
+
// src/pathwayCompiler.ts
|
|
51113
|
+
var describeAction = (action) => {
|
|
51114
|
+
switch (action.kind) {
|
|
51115
|
+
case "say":
|
|
51116
|
+
return `Say to the caller: "${action.text}"`;
|
|
51117
|
+
case "collect-slot":
|
|
51118
|
+
return `Collect slot \`${action.slotId}\` from the caller.`;
|
|
51119
|
+
case "call-tool":
|
|
51120
|
+
return `Call tool \`${action.toolId}\`${action.argsFromSlots ? ` with slots: ${action.argsFromSlots.join(", ")}` : ""}.`;
|
|
51121
|
+
case "set-slot":
|
|
51122
|
+
return `Set slot \`${action.slotId}\` to expression \`${action.valueExpression}\`.`;
|
|
51123
|
+
case "transfer":
|
|
51124
|
+
return `Transfer the call to \`${action.destination}\`.`;
|
|
51125
|
+
case "end-call":
|
|
51126
|
+
return `End the call${action.reason ? ` (reason: ${action.reason})` : ""}.`;
|
|
51127
|
+
}
|
|
51128
|
+
};
|
|
51129
|
+
var describeCondition = (condition) => {
|
|
51130
|
+
switch (condition.kind) {
|
|
51131
|
+
case "always":
|
|
51132
|
+
return "always";
|
|
51133
|
+
case "fallback":
|
|
51134
|
+
return "if no other condition matches";
|
|
51135
|
+
case "slot-filled":
|
|
51136
|
+
return `when slot \`${condition.slotId}\` is filled`;
|
|
51137
|
+
case "slot-equals":
|
|
51138
|
+
return `when slot \`${condition.slotId}\` equals ${JSON.stringify(condition.value)}`;
|
|
51139
|
+
case "slot-matches":
|
|
51140
|
+
return `when slot \`${condition.slotId}\` matches /${condition.pattern}/`;
|
|
51141
|
+
}
|
|
51142
|
+
};
|
|
51143
|
+
var describeTransition = (transition) => `\u2192 ${transition.to} (${describeCondition(transition.condition)})`;
|
|
51144
|
+
var describeState = (state) => {
|
|
51145
|
+
const header = `## State: ${state.id} \u2014 ${state.label}${state.kind ? ` (${state.kind})` : ""}`;
|
|
51146
|
+
const description = state.description ? `
|
|
51147
|
+
${state.description}` : "";
|
|
51148
|
+
const note = state.systemNote ? `
|
|
51149
|
+
Note: ${state.systemNote}` : "";
|
|
51150
|
+
const actions = (state.actions ?? []).map((a) => `- ${describeAction(a)}`).join(`
|
|
51151
|
+
`);
|
|
51152
|
+
const transitions = state.transitions.map((t) => `- ${describeTransition(t)}`).join(`
|
|
51153
|
+
`);
|
|
51154
|
+
return [
|
|
51155
|
+
header,
|
|
51156
|
+
description,
|
|
51157
|
+
note,
|
|
51158
|
+
actions ? `
|
|
51159
|
+
Actions:
|
|
51160
|
+
${actions}` : "",
|
|
51161
|
+
transitions ? `
|
|
51162
|
+
Transitions:
|
|
51163
|
+
${transitions}` : ""
|
|
51164
|
+
].filter((s) => s.length > 0).join(`
|
|
51165
|
+
`);
|
|
51166
|
+
};
|
|
51167
|
+
var buildTools = (pathway, prefix) => {
|
|
51168
|
+
const advanceTool = {
|
|
51169
|
+
description: `Advance the ${pathway.label} pathway by transitioning to the next state. Always call when a state's conditions are met.`,
|
|
51170
|
+
name: `${prefix}_advance`,
|
|
51171
|
+
parameters: {
|
|
51172
|
+
properties: {
|
|
51173
|
+
rationale: {
|
|
51174
|
+
description: "Why this transition is being taken now.",
|
|
51175
|
+
type: "string"
|
|
51176
|
+
},
|
|
51177
|
+
toStateId: {
|
|
51178
|
+
description: "ID of the target state.",
|
|
51179
|
+
enum: pathway.states.map((s) => s.id),
|
|
51180
|
+
type: "string"
|
|
51181
|
+
}
|
|
51182
|
+
},
|
|
51183
|
+
required: ["toStateId"],
|
|
51184
|
+
type: "object"
|
|
51185
|
+
}
|
|
51186
|
+
};
|
|
51187
|
+
const fillSlotTool = {
|
|
51188
|
+
description: `Record a slot value collected from the caller within the ${pathway.label} pathway.`,
|
|
51189
|
+
name: `${prefix}_fill_slot`,
|
|
51190
|
+
parameters: {
|
|
51191
|
+
properties: {
|
|
51192
|
+
slotId: {
|
|
51193
|
+
description: "ID of the slot being filled.",
|
|
51194
|
+
enum: pathway.slots.map((s) => s.id),
|
|
51195
|
+
type: "string"
|
|
51196
|
+
},
|
|
51197
|
+
value: {
|
|
51198
|
+
description: "The value the caller provided.",
|
|
51199
|
+
type: "string"
|
|
51200
|
+
}
|
|
51201
|
+
},
|
|
51202
|
+
required: ["slotId", "value"],
|
|
51203
|
+
type: "object"
|
|
51204
|
+
}
|
|
51205
|
+
};
|
|
51206
|
+
const endCallTool = {
|
|
51207
|
+
description: `End the ${pathway.label} call.`,
|
|
51208
|
+
name: `${prefix}_end_call`,
|
|
51209
|
+
parameters: {
|
|
51210
|
+
properties: {
|
|
51211
|
+
reason: { description: "Reason for ending.", type: "string" }
|
|
51212
|
+
},
|
|
51213
|
+
required: [],
|
|
51214
|
+
type: "object"
|
|
51215
|
+
}
|
|
51216
|
+
};
|
|
51217
|
+
const userTools = (pathway.tools ?? []).map((tool) => ({
|
|
51218
|
+
description: tool.description,
|
|
51219
|
+
name: `${prefix}_tool_${tool.id}`,
|
|
51220
|
+
parameters: {
|
|
51221
|
+
properties: {
|
|
51222
|
+
arguments: { description: "JSON-encoded arguments.", type: "string" }
|
|
51223
|
+
},
|
|
51224
|
+
required: [],
|
|
51225
|
+
type: "object"
|
|
51226
|
+
}
|
|
51227
|
+
}));
|
|
51228
|
+
return [advanceTool, fillSlotTool, endCallTool, ...userTools];
|
|
51229
|
+
};
|
|
51230
|
+
var compileVoicePathwayToAssistant = (options) => {
|
|
51231
|
+
const report = validateVoicePathway(options.pathway);
|
|
51232
|
+
if (!report.valid) {
|
|
51233
|
+
throw new Error(`Cannot compile invalid pathway: ${report.issues.filter((i) => i.severity === "error").map((i) => i.message).join("; ")}`);
|
|
51234
|
+
}
|
|
51235
|
+
const prefix = options.toolNamePrefix ?? `pathway_${options.pathway.id}`;
|
|
51236
|
+
const tools = buildTools(options.pathway, prefix);
|
|
51237
|
+
const fallback = options.fallbackBehavior === "end-call" ? "If no transition condition is met, end the call politely." : options.fallbackBehavior === "transfer" ? "If no transition condition is met, transfer to a human agent." : "If no transition condition is met, ask a clarifying question and stay in the current state.";
|
|
51238
|
+
const slotBlock = options.pathway.slots.map((slot) => `- \`${slot.id}\` (${slot.type}${slot.required ? ", required" : ""}): ${slot.description ?? slot.prompt}`).join(`
|
|
51239
|
+
`);
|
|
51240
|
+
const stateBlock = options.pathway.states.map(describeState).join(`
|
|
51241
|
+
|
|
51242
|
+
`);
|
|
51243
|
+
const systemPrompt = [
|
|
51244
|
+
`You are operating the "${options.pathway.label}" pathway as a voice agent.`,
|
|
51245
|
+
`Follow the state machine exactly. Use the provided tools to advance states, fill slots, and end the call.`,
|
|
51246
|
+
`Start in state \`${options.pathway.entryStateId}\`. Track which state you are in. Do not invent states or transitions.`,
|
|
51247
|
+
fallback,
|
|
51248
|
+
"",
|
|
51249
|
+
`Slots:
|
|
51250
|
+
${slotBlock || "(none)"}`,
|
|
51251
|
+
"",
|
|
51252
|
+
`States:
|
|
51253
|
+
${stateBlock}`
|
|
51254
|
+
].join(`
|
|
51255
|
+
`);
|
|
51256
|
+
return {
|
|
51257
|
+
metadata: {
|
|
51258
|
+
entryStateId: options.pathway.entryStateId,
|
|
51259
|
+
pathwayId: options.pathway.id,
|
|
51260
|
+
pathwayLabel: options.pathway.label
|
|
51261
|
+
},
|
|
51262
|
+
systemPrompt,
|
|
51263
|
+
tools,
|
|
51264
|
+
...options.introduction !== undefined ? { initialPrompt: options.introduction } : {}
|
|
51265
|
+
};
|
|
51266
|
+
};
|
|
51267
|
+
// src/pathwayVisualizer.ts
|
|
51268
|
+
var escapeMermaidLabel = (text) => text.replace(/[<>"]/gu, "").replace(/\|/gu, "/");
|
|
51269
|
+
var conditionLabel = (condition) => {
|
|
51270
|
+
switch (condition.kind) {
|
|
51271
|
+
case "always":
|
|
51272
|
+
return "always";
|
|
51273
|
+
case "fallback":
|
|
51274
|
+
return "else";
|
|
51275
|
+
case "slot-filled":
|
|
51276
|
+
return `${condition.slotId} \u2713`;
|
|
51277
|
+
case "slot-equals":
|
|
51278
|
+
return `${condition.slotId}=${condition.value}`;
|
|
51279
|
+
case "slot-matches":
|
|
51280
|
+
return `${condition.slotId}~/${condition.pattern}/`;
|
|
51281
|
+
}
|
|
51282
|
+
};
|
|
51283
|
+
var mermaidShape = (state) => {
|
|
51284
|
+
switch (state.kind) {
|
|
51285
|
+
case "entry":
|
|
51286
|
+
return { close: ")", open: "((" };
|
|
51287
|
+
case "terminal":
|
|
51288
|
+
return { close: "))", open: "((" };
|
|
51289
|
+
case "branch":
|
|
51290
|
+
return { close: "}", open: "{" };
|
|
51291
|
+
case "action":
|
|
51292
|
+
return { close: "]", open: "[" };
|
|
51293
|
+
case "collect":
|
|
51294
|
+
return { close: "]", open: "[" };
|
|
51295
|
+
default:
|
|
51296
|
+
return { close: "]", open: "[" };
|
|
51297
|
+
}
|
|
51298
|
+
};
|
|
51299
|
+
var renderVoicePathwayMermaid = (pathway) => {
|
|
51300
|
+
const lines = ["flowchart TD"];
|
|
51301
|
+
for (const state of pathway.states) {
|
|
51302
|
+
const shape = mermaidShape(state);
|
|
51303
|
+
const label = escapeMermaidLabel(`${state.id}: ${state.label}`);
|
|
51304
|
+
lines.push(` ${state.id}${shape.open}"${label}"${shape.close}`);
|
|
51305
|
+
}
|
|
51306
|
+
for (const state of pathway.states) {
|
|
51307
|
+
for (const transition of state.transitions) {
|
|
51308
|
+
const label = escapeMermaidLabel(conditionLabel(transition.condition));
|
|
51309
|
+
lines.push(` ${state.id} -- "${label}" --> ${transition.to}`);
|
|
51310
|
+
}
|
|
51311
|
+
}
|
|
51312
|
+
return lines.join(`
|
|
51313
|
+
`);
|
|
51314
|
+
};
|
|
51315
|
+
var renderVoicePathwayText = (pathway) => {
|
|
51316
|
+
const stateById = new Map(pathway.states.map((s) => [s.id, s]));
|
|
51317
|
+
const visited = new Set;
|
|
51318
|
+
const lines = [
|
|
51319
|
+
`Pathway: ${pathway.label} (${pathway.id})`,
|
|
51320
|
+
`Entry: ${pathway.entryStateId}`,
|
|
51321
|
+
""
|
|
51322
|
+
];
|
|
51323
|
+
if (pathway.slots.length > 0) {
|
|
51324
|
+
lines.push("Slots:");
|
|
51325
|
+
for (const slot of pathway.slots) {
|
|
51326
|
+
lines.push(` - ${slot.id} (${slot.type}${slot.required ? ", required" : ""}): ${slot.prompt}`);
|
|
51327
|
+
}
|
|
51328
|
+
lines.push("");
|
|
51329
|
+
}
|
|
51330
|
+
const walk = (stateId, depth) => {
|
|
51331
|
+
if (visited.has(stateId)) {
|
|
51332
|
+
lines.push(`${" ".repeat(depth)}\u2192 ${stateId} (already shown)`);
|
|
51333
|
+
return;
|
|
51334
|
+
}
|
|
51335
|
+
visited.add(stateId);
|
|
51336
|
+
const state = stateById.get(stateId);
|
|
51337
|
+
if (!state) {
|
|
51338
|
+
lines.push(`${" ".repeat(depth)}\u2192 ${stateId} (missing)`);
|
|
51339
|
+
return;
|
|
51340
|
+
}
|
|
51341
|
+
const indent = " ".repeat(depth);
|
|
51342
|
+
const kind = state.kind ? ` [${state.kind}]` : "";
|
|
51343
|
+
lines.push(`${indent}- ${state.id}: ${state.label}${kind}`);
|
|
51344
|
+
if (state.actions && state.actions.length > 0) {
|
|
51345
|
+
for (const action of state.actions) {
|
|
51346
|
+
lines.push(`${indent} \xB7 ${action.kind}`);
|
|
51347
|
+
}
|
|
51348
|
+
}
|
|
51349
|
+
for (const transition of state.transitions) {
|
|
51350
|
+
lines.push(`${indent} \u2192 ${transition.to} (${conditionLabel(transition.condition)})`);
|
|
51351
|
+
walk(transition.to, depth + 1);
|
|
51352
|
+
}
|
|
51353
|
+
};
|
|
51354
|
+
lines.push("States:");
|
|
51355
|
+
walk(pathway.entryStateId, 1);
|
|
51356
|
+
return lines.join(`
|
|
51357
|
+
`);
|
|
51358
|
+
};
|
|
51359
|
+
var visualizeVoicePathway = (pathway) => ({
|
|
51360
|
+
mermaid: renderVoicePathwayMermaid(pathway),
|
|
51361
|
+
text: renderVoicePathwayText(pathway)
|
|
51362
|
+
});
|
|
50089
51363
|
export {
|
|
50090
51364
|
writeVoiceProofPack,
|
|
50091
51365
|
writeVoiceMediaPipelineArtifacts,
|
|
@@ -50100,12 +51374,14 @@ export {
|
|
|
50100
51374
|
voiceComplianceRedactionDefaults,
|
|
50101
51375
|
voiceAgentUIStateOrder,
|
|
50102
51376
|
voice,
|
|
51377
|
+
visualizeVoicePathway,
|
|
50103
51378
|
verifyVoiceWebhookSignature,
|
|
50104
51379
|
verifyVoiceTwilioWebhookSignature,
|
|
50105
51380
|
verifyVoiceTelnyxWebhookSignature,
|
|
50106
51381
|
verifyVoicePlivoWebhookSignature,
|
|
50107
51382
|
verifyVoiceOpsWebhookSignature,
|
|
50108
51383
|
validateVoiceWorkflowRouteResult,
|
|
51384
|
+
validateVoicePathway,
|
|
50109
51385
|
validateVoiceObservabilityExportRecord,
|
|
50110
51386
|
validateVoiceDTMFLuhn,
|
|
50111
51387
|
ttsAdapterSessionCanCancel,
|
|
@@ -50256,6 +51532,8 @@ export {
|
|
|
50256
51532
|
renderVoiceProductionReadinessHTML,
|
|
50257
51533
|
renderVoicePostCallAnalysisMarkdown,
|
|
50258
51534
|
renderVoicePhoneAgentProductionSmokeHTML,
|
|
51535
|
+
renderVoicePathwayText,
|
|
51536
|
+
renderVoicePathwayMermaid,
|
|
50259
51537
|
renderVoiceOutcomeContractHTML,
|
|
50260
51538
|
renderVoiceOpsStatusHTML,
|
|
50261
51539
|
renderVoiceOpsRecoveryMarkdown,
|
|
@@ -50338,6 +51616,7 @@ export {
|
|
|
50338
51616
|
predictVoiceCallCost,
|
|
50339
51617
|
parseVoiceTelephonyWebhookEvent,
|
|
50340
51618
|
parseVoiceSessionSnapshot,
|
|
51619
|
+
parseVoiceAIScorecardResponse,
|
|
50341
51620
|
normalizeVoiceProofTrendReport,
|
|
50342
51621
|
normalizePhoneNumber,
|
|
50343
51622
|
muteVoiceMonitorIssue,
|
|
@@ -50368,6 +51647,8 @@ export {
|
|
|
50368
51647
|
fromVapiAssistantConfig,
|
|
50369
51648
|
formatVoiceProofTrendAge,
|
|
50370
51649
|
formatVoiceCallPlayerTimestamp,
|
|
51650
|
+
findVoicePathwayState,
|
|
51651
|
+
findVoicePathwaySlot,
|
|
50371
51652
|
filterVoiceTraceEvents,
|
|
50372
51653
|
filterVoiceAuditEvents,
|
|
50373
51654
|
fetchVoiceProofTarget,
|
|
@@ -50416,6 +51697,7 @@ export {
|
|
|
50416
51697
|
encodeTwilioMulawBase64,
|
|
50417
51698
|
encodeStereoWav,
|
|
50418
51699
|
encodePcmAsWav,
|
|
51700
|
+
detectVoiceQualityDrift,
|
|
50419
51701
|
describeVoiceIVRPlan,
|
|
50420
51702
|
describeVoiceAssistantMode,
|
|
50421
51703
|
describeVoiceAgentUIState,
|
|
@@ -50622,6 +51904,8 @@ export {
|
|
|
50622
51904
|
createVoicePhoneAgentProductionSmokeJSONHandler,
|
|
50623
51905
|
createVoicePhoneAgentProductionSmokeHTMLHandler,
|
|
50624
51906
|
createVoicePhoneAgent,
|
|
51907
|
+
createVoicePathwaySlotCollector,
|
|
51908
|
+
createVoicePathwayRuntime,
|
|
50625
51909
|
createVoiceOutcomeContractRoutes,
|
|
50626
51910
|
createVoiceOutcomeContractJSONHandler,
|
|
50627
51911
|
createVoiceOutcomeContractHTMLHandler,
|
|
@@ -50784,6 +52068,7 @@ export {
|
|
|
50784
52068
|
createVoiceAgentTool,
|
|
50785
52069
|
createVoiceAgentSquad,
|
|
50786
52070
|
createVoiceAgent,
|
|
52071
|
+
createVoiceAIScorecard,
|
|
50787
52072
|
createVoiceAIJudgeCompletion,
|
|
50788
52073
|
createTwilioVoiceRoutes,
|
|
50789
52074
|
createTwilioVoiceResponse,
|
|
@@ -50823,8 +52108,10 @@ export {
|
|
|
50823
52108
|
createAnthropicVoiceAssistantModel,
|
|
50824
52109
|
createAIVoiceModel,
|
|
50825
52110
|
conditionAudioChunk,
|
|
52111
|
+
computeVoiceScorecardCalibration,
|
|
50826
52112
|
computePcmDurationMs,
|
|
50827
52113
|
completeVoiceOpsTask,
|
|
52114
|
+
compileVoicePathwayToAssistant,
|
|
50828
52115
|
compareVoiceEvalBaseline,
|
|
50829
52116
|
compareVoiceCostScenarios,
|
|
50830
52117
|
collectVoiceDTMFInput,
|
|
@@ -50916,11 +52203,13 @@ export {
|
|
|
50916
52203
|
buildVoiceCompetitiveCoverageReport,
|
|
50917
52204
|
buildVoiceCampaignObservabilityReport,
|
|
50918
52205
|
buildVoiceCallerMemoryNamespace,
|
|
52206
|
+
buildVoiceCallScorecard,
|
|
50919
52207
|
buildVoiceCallDebuggerReport,
|
|
50920
52208
|
buildVoiceBrowserCallProfileReport,
|
|
50921
52209
|
buildVoiceAuditTrailReport,
|
|
50922
52210
|
buildVoiceAuditExport,
|
|
50923
52211
|
buildVoiceAuditDeliveryReport,
|
|
52212
|
+
buildVoiceAgentPerformanceReport,
|
|
50924
52213
|
buildReplayTimelineReport,
|
|
50925
52214
|
buildOTELTraceId,
|
|
50926
52215
|
buildOTELSpanId,
|
|
@@ -50985,6 +52274,7 @@ export {
|
|
|
50985
52274
|
VOICE_DTMF_DIGITS,
|
|
50986
52275
|
VOICE_CALLER_MEMORY_KEY,
|
|
50987
52276
|
TURN_PROFILE_DEFAULTS,
|
|
52277
|
+
DEFAULT_VOICE_SALES_RUBRIC,
|
|
50988
52278
|
DEFAULT_VOICE_REMINDER_TRIGGERS,
|
|
50989
52279
|
DEFAULT_VOICE_REDACTION_PATTERNS,
|
|
50990
52280
|
DEFAULT_VOICE_PROOF_TREND_PROFILE_DEFINITIONS,
|
|
@@ -50992,6 +52282,7 @@ export {
|
|
|
50992
52282
|
DEFAULT_VOICE_PROMPT_INJECTION_RULES,
|
|
50993
52283
|
DEFAULT_VOICE_PRICE_BOOK,
|
|
50994
52284
|
DEFAULT_VOICE_POST_CALL_SURVEY_QUESTIONS,
|
|
52285
|
+
DEFAULT_VOICE_PATHWAY_SLOT_PARSERS,
|
|
50995
52286
|
DEFAULT_VOICE_CAMPAIGN_TEMPLATE_FILTERS,
|
|
50996
52287
|
DEFAULT_VOICE_CALL_DISPOSITIONS,
|
|
50997
52288
|
DEFAULT_VOICE_ANNOTATION_KIND_SEVERITY,
|