@absolutejs/voice 0.0.22-beta.511 → 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.
- package/dist/agentPerformanceReport.d.ts +40 -0
- package/dist/aiScorecard.d.ts +32 -0
- package/dist/angular/index.js +13 -0
- package/dist/bookingFlow.d.ts +43 -0
- package/dist/calendarAdapter.d.ts +47 -0
- package/dist/calendarSlots.d.ts +35 -0
- package/dist/callScorecard.d.ts +53 -0
- package/dist/client/index.js +13 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +1002 -0
- package/dist/noShowPredictor.d.ts +46 -0
- package/dist/qualityDriftDetector.d.ts +44 -0
- package/dist/react/index.js +13 -0
- package/dist/reminderScheduler.d.ts +43 -0
- package/dist/scorecardCalibration.d.ts +31 -0
- package/dist/svelte/index.js +13 -0
- package/dist/testing/index.js +13 -0
- package/dist/vue/VoiceCostDashboard.d.ts +1 -1
- package/dist/vue/index.js +13 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,19 @@ var __name = (target, name) => {
|
|
|
10
10
|
});
|
|
11
11
|
return target;
|
|
12
12
|
};
|
|
13
|
+
var __returnValue = (v) => v;
|
|
14
|
+
function __exportSetter(name, newValue) {
|
|
15
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
16
|
+
}
|
|
17
|
+
var __export = (target, all) => {
|
|
18
|
+
for (var name in all)
|
|
19
|
+
__defProp(target, name, {
|
|
20
|
+
get: all[name],
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
set: __exportSetter.bind(all, name)
|
|
24
|
+
});
|
|
25
|
+
};
|
|
13
26
|
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
|
|
14
27
|
var __typeError = (msg) => {
|
|
15
28
|
throw TypeError(msg);
|
|
@@ -70,6 +83,115 @@ var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
|
70
83
|
};
|
|
71
84
|
var __require = import.meta.require;
|
|
72
85
|
|
|
86
|
+
// src/calendarSlots.ts
|
|
87
|
+
var exports_calendarSlots = {};
|
|
88
|
+
__export(exports_calendarSlots, {
|
|
89
|
+
summarizeVoiceCalendarSlot: () => summarizeVoiceCalendarSlot,
|
|
90
|
+
generateVoiceCalendarSlots: () => generateVoiceCalendarSlots
|
|
91
|
+
});
|
|
92
|
+
var parseHHMM = (value) => {
|
|
93
|
+
const match = /^([0-9]{1,2}):([0-9]{2})$/u.exec(value);
|
|
94
|
+
if (!match)
|
|
95
|
+
throw new Error(`Invalid time string (expected HH:MM): ${value}`);
|
|
96
|
+
return Number(match[1]) * 60 + Number(match[2]);
|
|
97
|
+
}, partsAt = (ms, timezone) => {
|
|
98
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
99
|
+
day: "2-digit",
|
|
100
|
+
hour: "2-digit",
|
|
101
|
+
hour12: false,
|
|
102
|
+
minute: "2-digit",
|
|
103
|
+
month: "2-digit",
|
|
104
|
+
timeZone: timezone,
|
|
105
|
+
weekday: "short",
|
|
106
|
+
year: "numeric"
|
|
107
|
+
});
|
|
108
|
+
const map = {};
|
|
109
|
+
for (const part of formatter.formatToParts(new Date(ms))) {
|
|
110
|
+
if (part.type !== "literal")
|
|
111
|
+
map[part.type] = part.value;
|
|
112
|
+
}
|
|
113
|
+
const weekdayMap = {
|
|
114
|
+
Fri: 5,
|
|
115
|
+
Mon: 1,
|
|
116
|
+
Sat: 6,
|
|
117
|
+
Sun: 0,
|
|
118
|
+
Thu: 4,
|
|
119
|
+
Tue: 2,
|
|
120
|
+
Wed: 3
|
|
121
|
+
};
|
|
122
|
+
const hourValue = map.hour === "24" ? "00" : map.hour ?? "0";
|
|
123
|
+
return {
|
|
124
|
+
date: `${map.year}-${map.month}-${map.day}`,
|
|
125
|
+
minutes: Number(hourValue) * 60 + Number(map.minute ?? "0"),
|
|
126
|
+
weekday: weekdayMap[map.weekday ?? ""] ?? 0
|
|
127
|
+
};
|
|
128
|
+
}, overlaps = (aStart, aEnd, bStart, bEnd) => aStart < bEnd && bStart < aEnd, generateVoiceCalendarSlots = (input) => {
|
|
129
|
+
if (input.durationMinutes <= 0) {
|
|
130
|
+
throw new Error("durationMinutes must be positive");
|
|
131
|
+
}
|
|
132
|
+
if (input.toMs <= input.fromMs)
|
|
133
|
+
return [];
|
|
134
|
+
const granularity = input.granularityMinutes ?? 15;
|
|
135
|
+
const buffer = input.bufferMinutes ?? 0;
|
|
136
|
+
const max = input.maxSlots ?? Infinity;
|
|
137
|
+
const hoursByDay = new Map;
|
|
138
|
+
for (const block of input.businessHours) {
|
|
139
|
+
const list = hoursByDay.get(block.weekday) ?? [];
|
|
140
|
+
list.push(block);
|
|
141
|
+
hoursByDay.set(block.weekday, list);
|
|
142
|
+
}
|
|
143
|
+
const blackoutDates = new Set((input.blackoutDates ?? []).map((b) => b.date));
|
|
144
|
+
const slots = [];
|
|
145
|
+
const stepMs = granularity * 60000;
|
|
146
|
+
const durationMs = input.durationMinutes * 60000;
|
|
147
|
+
const bufferMs = buffer * 60000;
|
|
148
|
+
let cursor = input.fromMs;
|
|
149
|
+
while (cursor + durationMs <= input.toMs && slots.length < max) {
|
|
150
|
+
const slotEnd = cursor + durationMs;
|
|
151
|
+
const startParts = partsAt(cursor, input.timezone);
|
|
152
|
+
if (blackoutDates.has(startParts.date)) {
|
|
153
|
+
cursor += stepMs;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const dayHours = hoursByDay.get(startParts.weekday);
|
|
157
|
+
if (!dayHours || dayHours.length === 0) {
|
|
158
|
+
cursor += stepMs;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const endParts = partsAt(slotEnd - 1, input.timezone);
|
|
162
|
+
const fitsHours = dayHours.some((block) => {
|
|
163
|
+
const startMin = parseHHMM(block.start);
|
|
164
|
+
const endMin = parseHHMM(block.end);
|
|
165
|
+
return startParts.minutes >= startMin && endParts.minutes < endMin && startParts.date === endParts.date;
|
|
166
|
+
});
|
|
167
|
+
if (!fitsHours) {
|
|
168
|
+
cursor += stepMs;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const collides = (input.bookedRanges ?? []).some((booked) => overlaps(cursor - bufferMs, slotEnd + bufferMs, booked.startMs, booked.endMs));
|
|
172
|
+
if (!collides) {
|
|
173
|
+
slots.push({
|
|
174
|
+
durationMinutes: input.durationMinutes,
|
|
175
|
+
endMs: slotEnd,
|
|
176
|
+
startMs: cursor
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
cursor += stepMs;
|
|
180
|
+
}
|
|
181
|
+
return slots;
|
|
182
|
+
}, summarizeVoiceCalendarSlot = (slot, options = {}) => {
|
|
183
|
+
const formatter = new Intl.DateTimeFormat(options.locale ?? "en-US", {
|
|
184
|
+
day: "numeric",
|
|
185
|
+
hour: "numeric",
|
|
186
|
+
hour12: true,
|
|
187
|
+
minute: "2-digit",
|
|
188
|
+
month: "long",
|
|
189
|
+
timeZone: options.timezone,
|
|
190
|
+
weekday: "long"
|
|
191
|
+
});
|
|
192
|
+
return formatter.format(new Date(slot.startMs));
|
|
193
|
+
};
|
|
194
|
+
|
|
73
195
|
// src/audioConditioning.ts
|
|
74
196
|
var DEFAULT_TARGET_LEVEL = 0.08;
|
|
75
197
|
var DEFAULT_MAX_GAIN = 3;
|
|
@@ -49602,6 +49724,871 @@ var createVoiceSupervisorPermissions = (options = {}) => {
|
|
|
49602
49724
|
};
|
|
49603
49725
|
};
|
|
49604
49726
|
var VOICE_SUPERVISOR_TIER_CAPABILITIES = TIER_CAPABILITIES;
|
|
49727
|
+
// src/calendarAdapter.ts
|
|
49728
|
+
var createVoiceInMemoryCalendarAdapter = (options) => {
|
|
49729
|
+
const now = options.now ?? (() => Date.now());
|
|
49730
|
+
const generateId = options.generateId ?? (() => `appt_${Math.random().toString(36).slice(2, 10)}`);
|
|
49731
|
+
const appointments = new Map;
|
|
49732
|
+
for (const range of options.bookedRanges ?? []) {
|
|
49733
|
+
const id = generateId();
|
|
49734
|
+
appointments.set(id, {
|
|
49735
|
+
calendarId: "default",
|
|
49736
|
+
createdAt: now(),
|
|
49737
|
+
endMs: range.endMs,
|
|
49738
|
+
id,
|
|
49739
|
+
startMs: range.startMs,
|
|
49740
|
+
status: "scheduled"
|
|
49741
|
+
});
|
|
49742
|
+
}
|
|
49743
|
+
const liveRanges = () => Array.from(appointments.values()).filter((a) => a.status === "scheduled").map((a) => ({ endMs: a.endMs, startMs: a.startMs }));
|
|
49744
|
+
return {
|
|
49745
|
+
async book(input) {
|
|
49746
|
+
const clash = liveRanges().some((r) => input.startMs < r.endMs && r.startMs < input.endMs);
|
|
49747
|
+
if (clash)
|
|
49748
|
+
throw new Error("Slot is already booked");
|
|
49749
|
+
const id = generateId();
|
|
49750
|
+
const appointment = {
|
|
49751
|
+
calendarId: input.calendarId,
|
|
49752
|
+
createdAt: now(),
|
|
49753
|
+
endMs: input.endMs,
|
|
49754
|
+
id,
|
|
49755
|
+
startMs: input.startMs,
|
|
49756
|
+
status: "scheduled",
|
|
49757
|
+
...input.title !== undefined ? { title: input.title } : {},
|
|
49758
|
+
...input.attendees !== undefined ? { attendees: input.attendees } : {},
|
|
49759
|
+
...input.notes !== undefined ? { notes: input.notes } : {},
|
|
49760
|
+
...input.metadata !== undefined ? { metadata: input.metadata } : {}
|
|
49761
|
+
};
|
|
49762
|
+
appointments.set(id, appointment);
|
|
49763
|
+
return appointment;
|
|
49764
|
+
},
|
|
49765
|
+
async cancel(id) {
|
|
49766
|
+
const existing = appointments.get(id);
|
|
49767
|
+
if (!existing)
|
|
49768
|
+
return null;
|
|
49769
|
+
const cancelled = {
|
|
49770
|
+
...existing,
|
|
49771
|
+
status: "cancelled"
|
|
49772
|
+
};
|
|
49773
|
+
appointments.set(id, cancelled);
|
|
49774
|
+
return cancelled;
|
|
49775
|
+
},
|
|
49776
|
+
async get(id) {
|
|
49777
|
+
return appointments.get(id) ?? null;
|
|
49778
|
+
},
|
|
49779
|
+
async listAvailability(query) {
|
|
49780
|
+
const { generateVoiceCalendarSlots: generateVoiceCalendarSlots2 } = await Promise.resolve().then(() => exports_calendarSlots);
|
|
49781
|
+
return generateVoiceCalendarSlots2({
|
|
49782
|
+
bookedRanges: liveRanges(),
|
|
49783
|
+
...query.bufferMinutes !== undefined ? { bufferMinutes: query.bufferMinutes } : {},
|
|
49784
|
+
businessHours: options.businessHours,
|
|
49785
|
+
durationMinutes: query.durationMinutes,
|
|
49786
|
+
fromMs: query.fromMs,
|
|
49787
|
+
...query.granularityMinutes !== undefined ? { granularityMinutes: query.granularityMinutes } : {},
|
|
49788
|
+
...query.maxSlots !== undefined ? { maxSlots: query.maxSlots } : {},
|
|
49789
|
+
...options.timezone !== undefined ? { timezone: options.timezone } : {},
|
|
49790
|
+
toMs: query.toMs
|
|
49791
|
+
});
|
|
49792
|
+
},
|
|
49793
|
+
providerName: "in-memory",
|
|
49794
|
+
async reschedule(id, nextStartMs, nextEndMs) {
|
|
49795
|
+
const existing = appointments.get(id);
|
|
49796
|
+
if (!existing)
|
|
49797
|
+
return null;
|
|
49798
|
+
const others = liveRanges().filter((r) => r.startMs !== existing.startMs || r.endMs !== existing.endMs);
|
|
49799
|
+
const clash = others.some((r) => nextStartMs < r.endMs && r.startMs < nextEndMs);
|
|
49800
|
+
if (clash)
|
|
49801
|
+
throw new Error("Cannot reschedule onto a booked slot");
|
|
49802
|
+
const updated = {
|
|
49803
|
+
...existing,
|
|
49804
|
+
endMs: nextEndMs,
|
|
49805
|
+
startMs: nextStartMs
|
|
49806
|
+
};
|
|
49807
|
+
appointments.set(id, updated);
|
|
49808
|
+
return updated;
|
|
49809
|
+
}
|
|
49810
|
+
};
|
|
49811
|
+
};
|
|
49812
|
+
// src/bookingFlow.ts
|
|
49813
|
+
var createVoiceBookingFlow = (options) => {
|
|
49814
|
+
const initial = {
|
|
49815
|
+
proposedSlots: [],
|
|
49816
|
+
step: options.initialStep ?? (options.services ? "ask-service" : "ask-date")
|
|
49817
|
+
};
|
|
49818
|
+
let state = initial;
|
|
49819
|
+
const listeners = new Set;
|
|
49820
|
+
const setState = (next) => {
|
|
49821
|
+
state = { ...state, ...next };
|
|
49822
|
+
for (const listener of listeners)
|
|
49823
|
+
listener(state);
|
|
49824
|
+
};
|
|
49825
|
+
const chooseService = (serviceId) => {
|
|
49826
|
+
const services = options.services ?? [];
|
|
49827
|
+
const service = services.find((s) => s.id === serviceId);
|
|
49828
|
+
if (!service) {
|
|
49829
|
+
setState({ error: `Unknown service: ${serviceId}`, step: "failed" });
|
|
49830
|
+
return;
|
|
49831
|
+
}
|
|
49832
|
+
setState({
|
|
49833
|
+
serviceDurationMinutes: service.durationMinutes,
|
|
49834
|
+
serviceId: service.id,
|
|
49835
|
+
step: "ask-date"
|
|
49836
|
+
});
|
|
49837
|
+
};
|
|
49838
|
+
const proposeSlotsForDay = async (input) => {
|
|
49839
|
+
const duration = state.serviceDurationMinutes ?? options.defaultDurationMinutes ?? 30;
|
|
49840
|
+
const slots = await options.adapter.listAvailability({
|
|
49841
|
+
calendarId: options.calendarId,
|
|
49842
|
+
durationMinutes: duration,
|
|
49843
|
+
fromMs: input.fromMs,
|
|
49844
|
+
...options.maxSlotsPerDay !== undefined ? { maxSlots: options.maxSlotsPerDay } : {},
|
|
49845
|
+
toMs: input.toMs
|
|
49846
|
+
});
|
|
49847
|
+
setState({ proposedSlots: slots, step: slots.length > 0 ? "ask-time" : "ask-date" });
|
|
49848
|
+
return slots;
|
|
49849
|
+
};
|
|
49850
|
+
const chooseSlot = (slotIndex) => {
|
|
49851
|
+
const slot = state.proposedSlots[slotIndex];
|
|
49852
|
+
if (!slot) {
|
|
49853
|
+
setState({ error: "Invalid slot selection", step: "failed" });
|
|
49854
|
+
return;
|
|
49855
|
+
}
|
|
49856
|
+
setState({ selectedSlot: slot, step: "confirm" });
|
|
49857
|
+
};
|
|
49858
|
+
const confirm = async (input = {}) => {
|
|
49859
|
+
if (state.step !== "confirm" || !state.selectedSlot) {
|
|
49860
|
+
setState({ error: "Nothing to confirm", step: "failed" });
|
|
49861
|
+
return null;
|
|
49862
|
+
}
|
|
49863
|
+
setState({ step: "booking" });
|
|
49864
|
+
try {
|
|
49865
|
+
const appt = await options.adapter.book({
|
|
49866
|
+
calendarId: options.calendarId,
|
|
49867
|
+
endMs: state.selectedSlot.endMs,
|
|
49868
|
+
startMs: state.selectedSlot.startMs,
|
|
49869
|
+
...input.attendees !== undefined ? { attendees: input.attendees } : {},
|
|
49870
|
+
...input.title !== undefined ? { title: input.title } : {},
|
|
49871
|
+
...input.notes !== undefined ? { notes: input.notes } : {}
|
|
49872
|
+
});
|
|
49873
|
+
setState({ appointment: appt, step: "booked" });
|
|
49874
|
+
return appt;
|
|
49875
|
+
} catch (error) {
|
|
49876
|
+
setState({
|
|
49877
|
+
error: error instanceof Error ? error.message : String(error),
|
|
49878
|
+
step: "failed"
|
|
49879
|
+
});
|
|
49880
|
+
return null;
|
|
49881
|
+
}
|
|
49882
|
+
};
|
|
49883
|
+
const reset = () => {
|
|
49884
|
+
state = {
|
|
49885
|
+
proposedSlots: [],
|
|
49886
|
+
step: options.initialStep ?? (options.services ? "ask-service" : "ask-date")
|
|
49887
|
+
};
|
|
49888
|
+
for (const listener of listeners)
|
|
49889
|
+
listener(state);
|
|
49890
|
+
};
|
|
49891
|
+
return {
|
|
49892
|
+
chooseService,
|
|
49893
|
+
chooseSlot,
|
|
49894
|
+
confirm,
|
|
49895
|
+
getState: () => state,
|
|
49896
|
+
proposeSlotsForDay,
|
|
49897
|
+
reset,
|
|
49898
|
+
subscribe(listener) {
|
|
49899
|
+
listeners.add(listener);
|
|
49900
|
+
listener(state);
|
|
49901
|
+
return () => {
|
|
49902
|
+
listeners.delete(listener);
|
|
49903
|
+
};
|
|
49904
|
+
}
|
|
49905
|
+
};
|
|
49906
|
+
};
|
|
49907
|
+
// src/noShowPredictor.ts
|
|
49908
|
+
var clamp = (value, min = 0, max = 1) => Math.max(min, Math.min(max, value));
|
|
49909
|
+
var scoreVoiceNoShowRisk = (input) => {
|
|
49910
|
+
const now = input.now ?? (() => Date.now());
|
|
49911
|
+
const leadHours = (input.appointmentStartMs - input.bookedAtMs) / 3600000;
|
|
49912
|
+
const startDate = new Date(input.appointmentStartMs);
|
|
49913
|
+
const weekday = startDate.getUTCDay();
|
|
49914
|
+
const hour = startDate.getUTCHours();
|
|
49915
|
+
const history = input.history ?? [];
|
|
49916
|
+
const past = history.filter((r) => r.scheduledStartMs < input.appointmentStartMs);
|
|
49917
|
+
const priorNoShows = past.filter((r) => r.outcome === "no-show").length;
|
|
49918
|
+
const priorKept = past.filter((r) => r.outcome === "kept").length;
|
|
49919
|
+
let score = 0.15;
|
|
49920
|
+
const drivers = [];
|
|
49921
|
+
if (leadHours > 72) {
|
|
49922
|
+
score += 0.1;
|
|
49923
|
+
drivers.push({ kind: "lead-time-hours", value: leadHours });
|
|
49924
|
+
} else if (leadHours < 12) {
|
|
49925
|
+
score -= 0.05;
|
|
49926
|
+
drivers.push({ kind: "lead-time-hours", value: leadHours });
|
|
49927
|
+
}
|
|
49928
|
+
if (weekday === 1) {
|
|
49929
|
+
score += 0.04;
|
|
49930
|
+
drivers.push({ kind: "weekday", value: weekday });
|
|
49931
|
+
}
|
|
49932
|
+
if (weekday === 5) {
|
|
49933
|
+
score += 0.03;
|
|
49934
|
+
drivers.push({ kind: "weekday", value: weekday });
|
|
49935
|
+
}
|
|
49936
|
+
if (hour < 9 || hour >= 17) {
|
|
49937
|
+
score += 0.04;
|
|
49938
|
+
drivers.push({ kind: "hour-of-day", value: hour });
|
|
49939
|
+
}
|
|
49940
|
+
if (priorNoShows > 0) {
|
|
49941
|
+
const delta = Math.min(0.5, priorNoShows * 0.2);
|
|
49942
|
+
score += delta;
|
|
49943
|
+
drivers.push({ kind: "prior-no-show-count", value: priorNoShows });
|
|
49944
|
+
}
|
|
49945
|
+
if (priorKept > 2 && priorNoShows === 0) {
|
|
49946
|
+
score -= 0.08;
|
|
49947
|
+
drivers.push({ kind: "prior-kept-count", value: priorKept });
|
|
49948
|
+
}
|
|
49949
|
+
if (input.reminderConfirmed === true) {
|
|
49950
|
+
score -= 0.15;
|
|
49951
|
+
drivers.push({ kind: "reminder-confirmed", value: true });
|
|
49952
|
+
} else if (input.reminderConfirmed === false) {
|
|
49953
|
+
score += 0.1;
|
|
49954
|
+
drivers.push({ kind: "reminder-confirmed", value: false });
|
|
49955
|
+
}
|
|
49956
|
+
if (input.callbackDistanceHours !== undefined && input.callbackDistanceHours > 24) {
|
|
49957
|
+
score += 0.06;
|
|
49958
|
+
drivers.push({
|
|
49959
|
+
kind: "callback-distance-hours",
|
|
49960
|
+
value: input.callbackDistanceHours
|
|
49961
|
+
});
|
|
49962
|
+
}
|
|
49963
|
+
if (input.weatherDisruption) {
|
|
49964
|
+
score += 0.18;
|
|
49965
|
+
drivers.push({ kind: "weather-disruption", value: true });
|
|
49966
|
+
}
|
|
49967
|
+
const finalScore = clamp(score);
|
|
49968
|
+
const band = finalScore >= 0.55 ? "high" : finalScore >= 0.3 ? "moderate" : "low";
|
|
49969
|
+
return { band, drivers, score: finalScore };
|
|
49970
|
+
};
|
|
49971
|
+
var summarizeVoiceNoShowVerdict = (verdict) => {
|
|
49972
|
+
const pct = Math.round(verdict.score * 100);
|
|
49973
|
+
const top = verdict.drivers.slice(0, 2).map((d) => d.kind).join(", ");
|
|
49974
|
+
return `${verdict.band} risk (${pct}%)${top ? ` \u2014 driven by ${top}` : ""}`;
|
|
49975
|
+
};
|
|
49976
|
+
// src/reminderScheduler.ts
|
|
49977
|
+
var DEFAULT_VOICE_REMINDER_TRIGGERS = [
|
|
49978
|
+
{
|
|
49979
|
+
channel: "sms",
|
|
49980
|
+
id: "remind-24h",
|
|
49981
|
+
offsetMinutesBeforeStart: 24 * 60
|
|
49982
|
+
},
|
|
49983
|
+
{
|
|
49984
|
+
channel: "sms",
|
|
49985
|
+
id: "remind-2h",
|
|
49986
|
+
offsetMinutesBeforeStart: 120
|
|
49987
|
+
},
|
|
49988
|
+
{
|
|
49989
|
+
channel: "call",
|
|
49990
|
+
id: "remind-call-30m",
|
|
49991
|
+
offsetMinutesBeforeStart: 30,
|
|
49992
|
+
retryOnFailure: true
|
|
49993
|
+
}
|
|
49994
|
+
];
|
|
49995
|
+
var createVoiceReminderScheduler = (options = {}) => {
|
|
49996
|
+
const now = options.now ?? (() => Date.now());
|
|
49997
|
+
const generateId = options.generateJobId ?? (() => `rem_${Math.random().toString(36).slice(2, 10)}`);
|
|
49998
|
+
const defaultTriggers = options.defaultTriggers ?? DEFAULT_VOICE_REMINDER_TRIGGERS;
|
|
49999
|
+
const maxAttempts = options.maxAttempts ?? 2;
|
|
50000
|
+
const jobs = new Map;
|
|
50001
|
+
const listeners = new Set;
|
|
50002
|
+
const broadcast = (job) => {
|
|
50003
|
+
for (const listener of listeners)
|
|
50004
|
+
listener(job);
|
|
50005
|
+
};
|
|
50006
|
+
const schedule = (input) => {
|
|
50007
|
+
const triggers = input.triggers.length > 0 ? input.triggers : defaultTriggers;
|
|
50008
|
+
const at = now();
|
|
50009
|
+
const created = [];
|
|
50010
|
+
for (const trigger of triggers) {
|
|
50011
|
+
const fireAt = input.appointmentStartMs - trigger.offsetMinutesBeforeStart * 60000;
|
|
50012
|
+
if (fireAt <= at)
|
|
50013
|
+
continue;
|
|
50014
|
+
const job = {
|
|
50015
|
+
appointmentId: input.appointmentId,
|
|
50016
|
+
attempts: 0,
|
|
50017
|
+
channel: trigger.channel,
|
|
50018
|
+
id: generateId(),
|
|
50019
|
+
scheduledAtMs: fireAt,
|
|
50020
|
+
status: "pending",
|
|
50021
|
+
triggerId: trigger.id,
|
|
50022
|
+
...input.metadata !== undefined ? { metadata: input.metadata } : {}
|
|
50023
|
+
};
|
|
50024
|
+
jobs.set(job.id, job);
|
|
50025
|
+
created.push(job);
|
|
50026
|
+
broadcast(job);
|
|
50027
|
+
}
|
|
50028
|
+
return created;
|
|
50029
|
+
};
|
|
50030
|
+
const due = (at = now()) => Array.from(jobs.values()).filter((j) => j.status === "pending" && j.scheduledAtMs <= at);
|
|
50031
|
+
const markInFlight = (jobId) => {
|
|
50032
|
+
const job = jobs.get(jobId);
|
|
50033
|
+
if (!job || job.status !== "pending")
|
|
50034
|
+
return false;
|
|
50035
|
+
job.status = "in-flight";
|
|
50036
|
+
job.attempts += 1;
|
|
50037
|
+
broadcast(job);
|
|
50038
|
+
return true;
|
|
50039
|
+
};
|
|
50040
|
+
const markSent = (jobId) => {
|
|
50041
|
+
const job = jobs.get(jobId);
|
|
50042
|
+
if (!job)
|
|
50043
|
+
return false;
|
|
50044
|
+
job.status = "sent";
|
|
50045
|
+
broadcast(job);
|
|
50046
|
+
return true;
|
|
50047
|
+
};
|
|
50048
|
+
const markFailed = (jobId, error) => {
|
|
50049
|
+
const job = jobs.get(jobId);
|
|
50050
|
+
if (!job)
|
|
50051
|
+
return false;
|
|
50052
|
+
job.lastError = error;
|
|
50053
|
+
if (job.attempts < maxAttempts) {
|
|
50054
|
+
job.status = "pending";
|
|
50055
|
+
job.scheduledAtMs = now() + 5 * 60000;
|
|
50056
|
+
} else {
|
|
50057
|
+
job.status = "failed";
|
|
50058
|
+
}
|
|
50059
|
+
broadcast(job);
|
|
50060
|
+
return true;
|
|
50061
|
+
};
|
|
50062
|
+
const cancelForAppointment = (appointmentId) => {
|
|
50063
|
+
let count = 0;
|
|
50064
|
+
for (const job of jobs.values()) {
|
|
50065
|
+
if (job.appointmentId === appointmentId && (job.status === "pending" || job.status === "in-flight")) {
|
|
50066
|
+
job.status = "cancelled";
|
|
50067
|
+
broadcast(job);
|
|
50068
|
+
count += 1;
|
|
50069
|
+
}
|
|
50070
|
+
}
|
|
50071
|
+
return count;
|
|
50072
|
+
};
|
|
50073
|
+
return {
|
|
50074
|
+
cancelForAppointment,
|
|
50075
|
+
due,
|
|
50076
|
+
list: (appointmentId) => Array.from(jobs.values()).filter((j) => !appointmentId || j.appointmentId === appointmentId),
|
|
50077
|
+
markFailed,
|
|
50078
|
+
markInFlight,
|
|
50079
|
+
markSent,
|
|
50080
|
+
schedule,
|
|
50081
|
+
subscribe(listener) {
|
|
50082
|
+
listeners.add(listener);
|
|
50083
|
+
return () => {
|
|
50084
|
+
listeners.delete(listener);
|
|
50085
|
+
};
|
|
50086
|
+
}
|
|
50087
|
+
};
|
|
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
|
+
};
|
|
49605
50592
|
export {
|
|
49606
50593
|
writeVoiceProofPack,
|
|
49607
50594
|
writeVoiceMediaPipelineArtifacts,
|
|
@@ -49649,6 +50636,7 @@ export {
|
|
|
49649
50636
|
summarizeVoiceOpsTaskQueue,
|
|
49650
50637
|
summarizeVoiceOpsTaskAnalytics,
|
|
49651
50638
|
summarizeVoiceOpsStatus,
|
|
50639
|
+
summarizeVoiceNoShowVerdict,
|
|
49652
50640
|
summarizeVoiceMediaPipelineReport,
|
|
49653
50641
|
summarizeVoiceLiveLatency,
|
|
49654
50642
|
summarizeVoiceIntegrationEvents,
|
|
@@ -49657,6 +50645,7 @@ export {
|
|
|
49657
50645
|
summarizeVoiceCampaigns,
|
|
49658
50646
|
summarizeVoiceCampaignDispositions,
|
|
49659
50647
|
summarizeVoiceCallerTranscript,
|
|
50648
|
+
summarizeVoiceCalendarSlot,
|
|
49660
50649
|
summarizeVoiceBrowserMedia,
|
|
49661
50650
|
summarizeVoiceBargeIn,
|
|
49662
50651
|
summarizeVoiceAuditTrail,
|
|
@@ -49670,6 +50659,7 @@ export {
|
|
|
49670
50659
|
shouldRetryCampaignAttempt,
|
|
49671
50660
|
shapeTelephonyAssistantText,
|
|
49672
50661
|
selectVoiceTraceEventsForPrune,
|
|
50662
|
+
scoreVoiceNoShowRisk,
|
|
49673
50663
|
saveVoiceIncidentBundleArtifact,
|
|
49674
50664
|
runVoiceToolContractSuite,
|
|
49675
50665
|
runVoiceToolContract,
|
|
@@ -49851,6 +50841,7 @@ export {
|
|
|
49851
50841
|
predictVoiceCallCost,
|
|
49852
50842
|
parseVoiceTelephonyWebhookEvent,
|
|
49853
50843
|
parseVoiceSessionSnapshot,
|
|
50844
|
+
parseVoiceAIScorecardResponse,
|
|
49854
50845
|
normalizeVoiceProofTrendReport,
|
|
49855
50846
|
normalizePhoneNumber,
|
|
49856
50847
|
muteVoiceMonitorIssue,
|
|
@@ -49877,6 +50868,7 @@ export {
|
|
|
49877
50868
|
getLatestVoiceTelephonyMediaReport,
|
|
49878
50869
|
getLatestVoiceBrowserMediaReport,
|
|
49879
50870
|
getDefaultVoiceTelephonyBenchmarkScenarios,
|
|
50871
|
+
generateVoiceCalendarSlots,
|
|
49880
50872
|
fromVapiAssistantConfig,
|
|
49881
50873
|
formatVoiceProofTrendAge,
|
|
49882
50874
|
formatVoiceCallPlayerTimestamp,
|
|
@@ -49928,6 +50920,7 @@ export {
|
|
|
49928
50920
|
encodeTwilioMulawBase64,
|
|
49929
50921
|
encodeStereoWav,
|
|
49930
50922
|
encodePcmAsWav,
|
|
50923
|
+
detectVoiceQualityDrift,
|
|
49931
50924
|
describeVoiceIVRPlan,
|
|
49932
50925
|
describeVoiceAssistantMode,
|
|
49933
50926
|
describeVoiceAgentUIState,
|
|
@@ -50053,6 +51046,7 @@ export {
|
|
|
50053
51046
|
createVoiceRetentionScheduler,
|
|
50054
51047
|
createVoiceResilienceRoutes,
|
|
50055
51048
|
createVoiceReplayTimelineHTMXRoute,
|
|
51049
|
+
createVoiceReminderScheduler,
|
|
50056
51050
|
createVoiceRedisTelnyxWebhookEventStore,
|
|
50057
51051
|
createVoiceRedisTelephonyWebhookIdempotencyStore,
|
|
50058
51052
|
createVoiceRedisTaskLeaseCoordinator,
|
|
@@ -50193,6 +51187,7 @@ export {
|
|
|
50193
51187
|
createVoiceIncidentBundleRoutes,
|
|
50194
51188
|
createVoiceInMemoryRealCallProfileRecoveryJobStore,
|
|
50195
51189
|
createVoiceInMemoryMonitorRegistry,
|
|
51190
|
+
createVoiceInMemoryCalendarAdapter,
|
|
50196
51191
|
createVoiceIVRSession,
|
|
50197
51192
|
createVoiceHubSpotTaskUpdateSink,
|
|
50198
51193
|
createVoiceHubSpotTaskSyncSinks,
|
|
@@ -50267,6 +51262,7 @@ export {
|
|
|
50267
51262
|
createVoiceCRMActivitySink,
|
|
50268
51263
|
createVoiceBrowserMediaRoutes,
|
|
50269
51264
|
createVoiceBrowserCallProfileRoutes,
|
|
51265
|
+
createVoiceBookingFlow,
|
|
50270
51266
|
createVoiceBearerAuthVerifier,
|
|
50271
51267
|
createVoiceBargeInRoutes,
|
|
50272
51268
|
createVoiceBackchannelDriver,
|
|
@@ -50293,6 +51289,7 @@ export {
|
|
|
50293
51289
|
createVoiceAgentTool,
|
|
50294
51290
|
createVoiceAgentSquad,
|
|
50295
51291
|
createVoiceAgent,
|
|
51292
|
+
createVoiceAIScorecard,
|
|
50296
51293
|
createVoiceAIJudgeCompletion,
|
|
50297
51294
|
createTwilioVoiceRoutes,
|
|
50298
51295
|
createTwilioVoiceResponse,
|
|
@@ -50332,6 +51329,7 @@ export {
|
|
|
50332
51329
|
createAnthropicVoiceAssistantModel,
|
|
50333
51330
|
createAIVoiceModel,
|
|
50334
51331
|
conditionAudioChunk,
|
|
51332
|
+
computeVoiceScorecardCalibration,
|
|
50335
51333
|
computePcmDurationMs,
|
|
50336
51334
|
completeVoiceOpsTask,
|
|
50337
51335
|
compareVoiceEvalBaseline,
|
|
@@ -50425,11 +51423,13 @@ export {
|
|
|
50425
51423
|
buildVoiceCompetitiveCoverageReport,
|
|
50426
51424
|
buildVoiceCampaignObservabilityReport,
|
|
50427
51425
|
buildVoiceCallerMemoryNamespace,
|
|
51426
|
+
buildVoiceCallScorecard,
|
|
50428
51427
|
buildVoiceCallDebuggerReport,
|
|
50429
51428
|
buildVoiceBrowserCallProfileReport,
|
|
50430
51429
|
buildVoiceAuditTrailReport,
|
|
50431
51430
|
buildVoiceAuditExport,
|
|
50432
51431
|
buildVoiceAuditDeliveryReport,
|
|
51432
|
+
buildVoiceAgentPerformanceReport,
|
|
50433
51433
|
buildReplayTimelineReport,
|
|
50434
51434
|
buildOTELTraceId,
|
|
50435
51435
|
buildOTELSpanId,
|
|
@@ -50494,6 +51494,8 @@ export {
|
|
|
50494
51494
|
VOICE_DTMF_DIGITS,
|
|
50495
51495
|
VOICE_CALLER_MEMORY_KEY,
|
|
50496
51496
|
TURN_PROFILE_DEFAULTS,
|
|
51497
|
+
DEFAULT_VOICE_SALES_RUBRIC,
|
|
51498
|
+
DEFAULT_VOICE_REMINDER_TRIGGERS,
|
|
50497
51499
|
DEFAULT_VOICE_REDACTION_PATTERNS,
|
|
50498
51500
|
DEFAULT_VOICE_PROOF_TREND_PROFILE_DEFINITIONS,
|
|
50499
51501
|
DEFAULT_VOICE_PROOF_TRENDS_MAX_AGE_MS,
|