@absolutejs/voice 0.0.22-beta.507 → 0.0.22-beta.509
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/angular/index.d.ts +2 -0
- package/dist/angular/index.js +374 -272
- package/dist/angular/voice-live-agent-console.service.d.ts +16 -0
- package/dist/dtmfCollector.d.ts +37 -0
- package/dist/holdAudio.d.ts +23 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +563 -0
- package/dist/postCallSurvey.d.ts +41 -0
- package/dist/promptInjectionGuard.d.ts +30 -0
- package/dist/react/VoiceLiveAgentConsole.d.ts +11 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +285 -29
- package/dist/svelte/createVoiceLiveAgentConsole.d.ts +23 -0
- package/dist/svelte/index.d.ts +2 -0
- package/dist/svelte/index.js +291 -180
- package/dist/vue/VoiceLiveAgentConsole.d.ts +50 -0
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.js +250 -32
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -48011,6 +48011,556 @@ var createVoiceLiveMonitorRoutes = (options) => {
|
|
|
48011
48011
|
}
|
|
48012
48012
|
return app;
|
|
48013
48013
|
};
|
|
48014
|
+
// src/costPredictor.ts
|
|
48015
|
+
var lookupRates2 = (priceBook, provider, model) => {
|
|
48016
|
+
if (!provider)
|
|
48017
|
+
return;
|
|
48018
|
+
const namespacedKey = model ? `${provider.toLowerCase()}:${model.toLowerCase()}` : undefined;
|
|
48019
|
+
if (namespacedKey && priceBook[namespacedKey]) {
|
|
48020
|
+
return priceBook[namespacedKey];
|
|
48021
|
+
}
|
|
48022
|
+
const bare = provider.toLowerCase();
|
|
48023
|
+
return priceBook[bare];
|
|
48024
|
+
};
|
|
48025
|
+
var round = (value, places = 6) => {
|
|
48026
|
+
const factor = 10 ** places;
|
|
48027
|
+
return Math.round(value * factor) / factor;
|
|
48028
|
+
};
|
|
48029
|
+
var predictVoiceCallCost = (input) => {
|
|
48030
|
+
const priceBook = input.priceBook ?? DEFAULT_VOICE_PRICE_BOOK;
|
|
48031
|
+
const p = input.profile;
|
|
48032
|
+
const llm = lookupRates2(priceBook, p.llmProvider, p.llmModel)?.llm;
|
|
48033
|
+
const tts = p.ttsProvider ? lookupRates2(priceBook, p.ttsProvider, p.ttsModel)?.tts : undefined;
|
|
48034
|
+
const stt = p.sttProvider ? lookupRates2(priceBook, p.sttProvider, p.sttModel)?.stt : undefined;
|
|
48035
|
+
const telephony = p.telephonyProvider ? lookupRates2(priceBook, p.telephonyProvider, undefined)?.telephony : undefined;
|
|
48036
|
+
const totalInputTokens = p.inputTokensPerTurn * p.turnsPerCall;
|
|
48037
|
+
const totalOutputTokens = p.outputTokensPerTurn * p.turnsPerCall;
|
|
48038
|
+
const llmUsd = llm ? totalInputTokens * llm.inputPerMillionTokensUsd / 1e6 + totalOutputTokens * llm.outputPerMillionTokensUsd / 1e6 : 0;
|
|
48039
|
+
const totalChars = p.ttsCharsPerTurn * p.turnsPerCall;
|
|
48040
|
+
let ttsUsd = 0;
|
|
48041
|
+
if (tts?.perMillionCharactersUsd !== undefined && totalChars > 0) {
|
|
48042
|
+
ttsUsd = totalChars * tts.perMillionCharactersUsd / 1e6;
|
|
48043
|
+
} else if (tts?.perSecondUsd !== undefined) {
|
|
48044
|
+
const audioSec = totalChars / 13.75;
|
|
48045
|
+
ttsUsd = audioSec * tts.perSecondUsd;
|
|
48046
|
+
}
|
|
48047
|
+
const sttSeconds = p.minutesPerCall * 60;
|
|
48048
|
+
const sttUsd = stt ? sttSeconds * stt.perSecondUsd : 0;
|
|
48049
|
+
const telephonyUsd = telephony ? p.minutesPerCall * telephony.perMinuteUsd : 0;
|
|
48050
|
+
const totalPerCall = llmUsd + ttsUsd + sttUsd + telephonyUsd;
|
|
48051
|
+
const callsPerDay = (p.inboundPerDay ?? 0) + (p.outboundPerDay ?? 0);
|
|
48052
|
+
const monthlyCalls = callsPerDay * 30;
|
|
48053
|
+
return {
|
|
48054
|
+
callsPerDay,
|
|
48055
|
+
monthly: {
|
|
48056
|
+
llmUsd: round(llmUsd * monthlyCalls),
|
|
48057
|
+
sttUsd: round(sttUsd * monthlyCalls),
|
|
48058
|
+
telephonyUsd: round(telephonyUsd * monthlyCalls),
|
|
48059
|
+
totalUsd: round(totalPerCall * monthlyCalls),
|
|
48060
|
+
ttsUsd: round(ttsUsd * monthlyCalls)
|
|
48061
|
+
},
|
|
48062
|
+
perCall: {
|
|
48063
|
+
llmUsd: round(llmUsd),
|
|
48064
|
+
sttUsd: round(sttUsd),
|
|
48065
|
+
telephonyUsd: round(telephonyUsd),
|
|
48066
|
+
totalUsd: round(totalPerCall),
|
|
48067
|
+
ttsUsd: round(ttsUsd)
|
|
48068
|
+
}
|
|
48069
|
+
};
|
|
48070
|
+
};
|
|
48071
|
+
var compareVoiceCostScenarios = (input) => {
|
|
48072
|
+
const baselineDef = input.scenarios.find((s) => s.id === input.baselineId);
|
|
48073
|
+
if (!baselineDef) {
|
|
48074
|
+
throw new Error(`Baseline scenario '${input.baselineId}' not found in scenarios list`);
|
|
48075
|
+
}
|
|
48076
|
+
const baseline = predictVoiceCallCost({
|
|
48077
|
+
priceBook: input.priceBook,
|
|
48078
|
+
profile: baselineDef.profile
|
|
48079
|
+
});
|
|
48080
|
+
return {
|
|
48081
|
+
baseline,
|
|
48082
|
+
scenarios: input.scenarios.map((scenario) => {
|
|
48083
|
+
const prediction = predictVoiceCallCost({
|
|
48084
|
+
priceBook: input.priceBook,
|
|
48085
|
+
profile: scenario.profile
|
|
48086
|
+
});
|
|
48087
|
+
return {
|
|
48088
|
+
delta: {
|
|
48089
|
+
monthlyUsd: round(prediction.monthly.totalUsd - baseline.monthly.totalUsd),
|
|
48090
|
+
perCallUsd: round(prediction.perCall.totalUsd - baseline.perCall.totalUsd)
|
|
48091
|
+
},
|
|
48092
|
+
prediction,
|
|
48093
|
+
scenarioId: scenario.id
|
|
48094
|
+
};
|
|
48095
|
+
})
|
|
48096
|
+
};
|
|
48097
|
+
};
|
|
48098
|
+
// src/iceServers.ts
|
|
48099
|
+
var toBase643 = (bytes) => {
|
|
48100
|
+
if (typeof Buffer !== "undefined") {
|
|
48101
|
+
return Buffer.from(bytes).toString("base64");
|
|
48102
|
+
}
|
|
48103
|
+
let bin = "";
|
|
48104
|
+
for (const byte of bytes)
|
|
48105
|
+
bin += String.fromCharCode(byte);
|
|
48106
|
+
return btoa(bin);
|
|
48107
|
+
};
|
|
48108
|
+
var defaultHmacSha1Base64 = async (key, message) => {
|
|
48109
|
+
const encoder2 = new TextEncoder;
|
|
48110
|
+
const cryptoKey = await crypto.subtle.importKey("raw", encoder2.encode(key), { hash: "SHA-1", name: "HMAC" }, false, ["sign"]);
|
|
48111
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, encoder2.encode(message));
|
|
48112
|
+
return toBase643(new Uint8Array(signature));
|
|
48113
|
+
};
|
|
48114
|
+
var createCoturnIceServers = async (input) => {
|
|
48115
|
+
const ttlSec = input.ttlSec ?? 3600;
|
|
48116
|
+
const now = input.now ?? Math.floor(Date.now() / 1000);
|
|
48117
|
+
const expires = now + ttlSec;
|
|
48118
|
+
const ephemeralUsername = `${expires}:${input.username}`;
|
|
48119
|
+
const credential = await (input.hmacSha1Base64 ?? defaultHmacSha1Base64)(input.sharedSecret, ephemeralUsername);
|
|
48120
|
+
const stun = input.stunUrls ?? ["stun:stun.l.google.com:19302"];
|
|
48121
|
+
return [
|
|
48122
|
+
{ urls: stun },
|
|
48123
|
+
{
|
|
48124
|
+
credential,
|
|
48125
|
+
urls: [
|
|
48126
|
+
`turn:${input.turnHost}?transport=udp`,
|
|
48127
|
+
`turn:${input.turnHost}?transport=tcp`,
|
|
48128
|
+
`turns:${input.turnHost}?transport=tcp`
|
|
48129
|
+
],
|
|
48130
|
+
username: ephemeralUsername
|
|
48131
|
+
}
|
|
48132
|
+
];
|
|
48133
|
+
};
|
|
48134
|
+
var toBasicAuth2 = (sid, token) => `Basic ${btoa(`${sid}:${token}`)}`;
|
|
48135
|
+
var createTwilioNTSIceServers = async (input) => {
|
|
48136
|
+
if (!input.accountSid || !input.authToken) {
|
|
48137
|
+
throw new Error("Twilio NTS requires accountSid + authToken");
|
|
48138
|
+
}
|
|
48139
|
+
const fetchImpl = input.fetch ?? globalThis.fetch.bind(globalThis);
|
|
48140
|
+
const response = await fetchImpl(`https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(input.accountSid)}/Tokens.json`, {
|
|
48141
|
+
headers: {
|
|
48142
|
+
accept: "application/json",
|
|
48143
|
+
authorization: toBasicAuth2(input.accountSid, input.authToken)
|
|
48144
|
+
},
|
|
48145
|
+
method: "POST"
|
|
48146
|
+
});
|
|
48147
|
+
if (!response.ok) {
|
|
48148
|
+
const text = await response.text().catch(() => "");
|
|
48149
|
+
throw new Error(`Twilio NTS Tokens failed: ${response.status} ${response.statusText} ${text.slice(0, 200)}`);
|
|
48150
|
+
}
|
|
48151
|
+
const payload = await response.json();
|
|
48152
|
+
const entries = payload.ice_servers ?? [];
|
|
48153
|
+
return entries.map((entry) => ({
|
|
48154
|
+
credential: entry.credential,
|
|
48155
|
+
urls: entry.urls ?? entry.url ?? [],
|
|
48156
|
+
username: entry.username
|
|
48157
|
+
}));
|
|
48158
|
+
};
|
|
48159
|
+
// src/holdAudio.ts
|
|
48160
|
+
var DEFAULT_CUES2 = [
|
|
48161
|
+
{ text: "Let me look that up for you." },
|
|
48162
|
+
{ text: "One moment while I check on that." },
|
|
48163
|
+
{ text: "Still looking into this." }
|
|
48164
|
+
];
|
|
48165
|
+
var createVoiceHoldAudioDriver = (options) => {
|
|
48166
|
+
const cues = options.cues ?? DEFAULT_CUES2;
|
|
48167
|
+
const cooldownMs = Math.max(0, options.cooldownMs ?? 4000);
|
|
48168
|
+
const thinkingThresholdMs = Math.max(0, options.thinkingThresholdMs ?? 1500);
|
|
48169
|
+
let thinkingSince;
|
|
48170
|
+
let lastCueAt;
|
|
48171
|
+
let cueIndex = 0;
|
|
48172
|
+
let timer;
|
|
48173
|
+
let firing = false;
|
|
48174
|
+
const tryFire = async (now) => {
|
|
48175
|
+
if (firing || cues.length === 0)
|
|
48176
|
+
return;
|
|
48177
|
+
if (thinkingSince === undefined)
|
|
48178
|
+
return;
|
|
48179
|
+
if (now - thinkingSince < thinkingThresholdMs)
|
|
48180
|
+
return;
|
|
48181
|
+
if (lastCueAt !== undefined && now - lastCueAt < cooldownMs)
|
|
48182
|
+
return;
|
|
48183
|
+
const cue = cues[cueIndex % cues.length];
|
|
48184
|
+
if (!cue)
|
|
48185
|
+
return;
|
|
48186
|
+
firing = true;
|
|
48187
|
+
try {
|
|
48188
|
+
await options.onCue(cue);
|
|
48189
|
+
} finally {
|
|
48190
|
+
firing = false;
|
|
48191
|
+
lastCueAt = now;
|
|
48192
|
+
cueIndex += 1;
|
|
48193
|
+
}
|
|
48194
|
+
};
|
|
48195
|
+
const scheduleNext = (now) => {
|
|
48196
|
+
if (thinkingSince === undefined)
|
|
48197
|
+
return;
|
|
48198
|
+
if (timer)
|
|
48199
|
+
clearTimeout(timer);
|
|
48200
|
+
const elapsed = now - thinkingSince;
|
|
48201
|
+
const delay = Math.max(0, lastCueAt === undefined ? thinkingThresholdMs - elapsed : cooldownMs - (now - lastCueAt));
|
|
48202
|
+
timer = setTimeout(() => {
|
|
48203
|
+
timer = undefined;
|
|
48204
|
+
const nextNow = Date.now();
|
|
48205
|
+
tryFire(nextNow).then(() => {
|
|
48206
|
+
if (thinkingSince !== undefined)
|
|
48207
|
+
scheduleNext(Date.now());
|
|
48208
|
+
});
|
|
48209
|
+
}, delay);
|
|
48210
|
+
};
|
|
48211
|
+
const clearTimer = () => {
|
|
48212
|
+
if (timer)
|
|
48213
|
+
clearTimeout(timer);
|
|
48214
|
+
timer = undefined;
|
|
48215
|
+
};
|
|
48216
|
+
return {
|
|
48217
|
+
noteResponse: () => {
|
|
48218
|
+
clearTimer();
|
|
48219
|
+
thinkingSince = undefined;
|
|
48220
|
+
},
|
|
48221
|
+
noteThinking: (timestampMs) => {
|
|
48222
|
+
const now = timestampMs ?? Date.now();
|
|
48223
|
+
if (thinkingSince === undefined) {
|
|
48224
|
+
thinkingSince = now;
|
|
48225
|
+
}
|
|
48226
|
+
scheduleNext(now);
|
|
48227
|
+
},
|
|
48228
|
+
reset: () => {
|
|
48229
|
+
clearTimer();
|
|
48230
|
+
thinkingSince = undefined;
|
|
48231
|
+
lastCueAt = undefined;
|
|
48232
|
+
cueIndex = 0;
|
|
48233
|
+
}
|
|
48234
|
+
};
|
|
48235
|
+
};
|
|
48236
|
+
// src/promptInjectionGuard.ts
|
|
48237
|
+
var DEFAULT_VOICE_PROMPT_INJECTION_RULES = [
|
|
48238
|
+
{
|
|
48239
|
+
label: "ignore-prior-instructions",
|
|
48240
|
+
pattern: /\bignore (?:all |the |any )?(?:previous|prior|above|earlier) (?:instructions|rules|prompts?)\b/iu,
|
|
48241
|
+
severity: "high"
|
|
48242
|
+
},
|
|
48243
|
+
{
|
|
48244
|
+
label: "role-override",
|
|
48245
|
+
pattern: /\byou are (?:now )?(?:a |an )?(?:different|new) (?:assistant|model|role)\b/iu,
|
|
48246
|
+
severity: "high"
|
|
48247
|
+
},
|
|
48248
|
+
{
|
|
48249
|
+
label: "system-prompt-leak",
|
|
48250
|
+
pattern: /\b(?:reveal|show|print|repeat|tell (?:me|us)|share) (?:your |the )?(?:system|hidden|secret) (?:prompt|instructions|message)\b/iu,
|
|
48251
|
+
severity: "high"
|
|
48252
|
+
},
|
|
48253
|
+
{
|
|
48254
|
+
label: "developer-impersonation",
|
|
48255
|
+
pattern: /\b(?:as your |i am the )?(?:developer|engineer|owner|admin)(?: of)?\b[^.]{0,40}\b(?:override|disable|bypass)/iu,
|
|
48256
|
+
severity: "medium"
|
|
48257
|
+
},
|
|
48258
|
+
{
|
|
48259
|
+
label: "jailbreak-persona",
|
|
48260
|
+
pattern: /\b(?:DAN|do anything now|jailbreak|developer mode|god mode)\b/iu,
|
|
48261
|
+
severity: "high"
|
|
48262
|
+
},
|
|
48263
|
+
{
|
|
48264
|
+
label: "tool-misuse-request",
|
|
48265
|
+
pattern: /\b(?:call|invoke|use) (?:the )?(?:transfer|hangup|end[_ ]call)(?:[_ ]?(?:tool|function))?\b/iu,
|
|
48266
|
+
severity: "low"
|
|
48267
|
+
}
|
|
48268
|
+
];
|
|
48269
|
+
var extractText2 = (input) => typeof input === "string" ? input : input.text;
|
|
48270
|
+
var createVoicePromptInjectionGuard = (options = {}) => {
|
|
48271
|
+
const rules = options.rules ?? DEFAULT_VOICE_PROMPT_INJECTION_RULES;
|
|
48272
|
+
const replacement = options.sanitizedReplacement ?? "[REDACTED:INJECTION]";
|
|
48273
|
+
return {
|
|
48274
|
+
evaluate: (input) => {
|
|
48275
|
+
const text = extractText2(input);
|
|
48276
|
+
const matches = [];
|
|
48277
|
+
for (const rule of rules) {
|
|
48278
|
+
rule.pattern.lastIndex = 0;
|
|
48279
|
+
const match = rule.pattern.exec(text);
|
|
48280
|
+
if (match) {
|
|
48281
|
+
matches.push({
|
|
48282
|
+
label: rule.label,
|
|
48283
|
+
matchedText: match[0],
|
|
48284
|
+
severity: rule.severity
|
|
48285
|
+
});
|
|
48286
|
+
}
|
|
48287
|
+
}
|
|
48288
|
+
return { matches, ok: matches.length === 0 };
|
|
48289
|
+
},
|
|
48290
|
+
rules,
|
|
48291
|
+
sanitize: (text) => {
|
|
48292
|
+
let result = text;
|
|
48293
|
+
for (const rule of rules) {
|
|
48294
|
+
result = result.replace(new RegExp(rule.pattern, "gi"), replacement);
|
|
48295
|
+
}
|
|
48296
|
+
return result;
|
|
48297
|
+
}
|
|
48298
|
+
};
|
|
48299
|
+
};
|
|
48300
|
+
// src/postCallSurvey.ts
|
|
48301
|
+
var DEFAULT_VOICE_POST_CALL_SURVEY_QUESTIONS = [
|
|
48302
|
+
{
|
|
48303
|
+
id: "nps",
|
|
48304
|
+
max: 10,
|
|
48305
|
+
min: 0,
|
|
48306
|
+
prompt: "On a scale of zero to ten, how likely are you to recommend this service?",
|
|
48307
|
+
required: true,
|
|
48308
|
+
type: "rating"
|
|
48309
|
+
},
|
|
48310
|
+
{
|
|
48311
|
+
id: "resolved",
|
|
48312
|
+
prompt: "Did we resolve the reason for your call today?",
|
|
48313
|
+
required: true,
|
|
48314
|
+
type: "boolean"
|
|
48315
|
+
},
|
|
48316
|
+
{
|
|
48317
|
+
id: "comment",
|
|
48318
|
+
prompt: "Anything else you'd like to share before we wrap up?",
|
|
48319
|
+
type: "comment"
|
|
48320
|
+
}
|
|
48321
|
+
];
|
|
48322
|
+
var bucketize = (rating) => {
|
|
48323
|
+
if (rating === null)
|
|
48324
|
+
return null;
|
|
48325
|
+
if (rating >= 9)
|
|
48326
|
+
return "promoter";
|
|
48327
|
+
if (rating >= 7)
|
|
48328
|
+
return "passive";
|
|
48329
|
+
return "detractor";
|
|
48330
|
+
};
|
|
48331
|
+
var validateRating = (question, value) => {
|
|
48332
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
48333
|
+
throw new TypeError(`Question ${question.id} requires a numeric rating`);
|
|
48334
|
+
}
|
|
48335
|
+
const min = question.min ?? 0;
|
|
48336
|
+
const max = question.max ?? 10;
|
|
48337
|
+
if (value < min || value > max) {
|
|
48338
|
+
throw new RangeError(`Question ${question.id} expects a rating between ${min} and ${max}`);
|
|
48339
|
+
}
|
|
48340
|
+
return value;
|
|
48341
|
+
};
|
|
48342
|
+
var createVoicePostCallSurvey = (options) => {
|
|
48343
|
+
const now = options.now ?? (() => Date.now());
|
|
48344
|
+
const questions = options.questions ?? DEFAULT_VOICE_POST_CALL_SURVEY_QUESTIONS;
|
|
48345
|
+
const response = {
|
|
48346
|
+
answers: [],
|
|
48347
|
+
completedAt: null,
|
|
48348
|
+
npsBucket: null,
|
|
48349
|
+
sessionId: options.sessionId,
|
|
48350
|
+
startedAt: now()
|
|
48351
|
+
};
|
|
48352
|
+
const indexById = new Map(questions.map((q) => [q.id, q]));
|
|
48353
|
+
let cursor = 0;
|
|
48354
|
+
const next = () => cursor < questions.length ? questions[cursor] ?? null : null;
|
|
48355
|
+
const record = (questionId, value) => {
|
|
48356
|
+
const question = indexById.get(questionId);
|
|
48357
|
+
if (!question)
|
|
48358
|
+
throw new Error(`Unknown survey question: ${questionId}`);
|
|
48359
|
+
let stored = value;
|
|
48360
|
+
if (question.type === "rating")
|
|
48361
|
+
stored = validateRating(question, value);
|
|
48362
|
+
if (question.type === "boolean") {
|
|
48363
|
+
if (typeof value !== "boolean") {
|
|
48364
|
+
throw new TypeError(`Question ${questionId} requires a boolean answer`);
|
|
48365
|
+
}
|
|
48366
|
+
stored = value;
|
|
48367
|
+
}
|
|
48368
|
+
if (question.type === "comment" && value !== null && typeof value !== "string") {
|
|
48369
|
+
throw new TypeError(`Question ${questionId} requires a string answer`);
|
|
48370
|
+
}
|
|
48371
|
+
if (question.required && (stored === null || stored === "")) {
|
|
48372
|
+
throw new Error(`Question ${questionId} is required`);
|
|
48373
|
+
}
|
|
48374
|
+
const answer = { questionId, value: stored };
|
|
48375
|
+
response.answers.push(answer);
|
|
48376
|
+
if (questionId === "nps" && typeof stored === "number") {
|
|
48377
|
+
response.npsBucket = bucketize(stored);
|
|
48378
|
+
}
|
|
48379
|
+
const idx = questions.findIndex((q) => q.id === questionId);
|
|
48380
|
+
if (idx >= 0)
|
|
48381
|
+
cursor = Math.max(cursor, idx + 1);
|
|
48382
|
+
return answer;
|
|
48383
|
+
};
|
|
48384
|
+
const skip = () => {
|
|
48385
|
+
const question = next();
|
|
48386
|
+
if (!question)
|
|
48387
|
+
return null;
|
|
48388
|
+
if (question.required) {
|
|
48389
|
+
throw new Error(`Cannot skip required question: ${question.id}`);
|
|
48390
|
+
}
|
|
48391
|
+
response.answers.push({ questionId: question.id, value: null });
|
|
48392
|
+
cursor += 1;
|
|
48393
|
+
return question;
|
|
48394
|
+
};
|
|
48395
|
+
const complete = () => {
|
|
48396
|
+
for (const question of questions) {
|
|
48397
|
+
if (question.required && !response.answers.some((a) => a.questionId === question.id && a.value !== null)) {
|
|
48398
|
+
throw new Error(`Survey is missing required answer: ${question.id}`);
|
|
48399
|
+
}
|
|
48400
|
+
}
|
|
48401
|
+
response.completedAt = now();
|
|
48402
|
+
return response;
|
|
48403
|
+
};
|
|
48404
|
+
return {
|
|
48405
|
+
complete,
|
|
48406
|
+
getResponse: () => response,
|
|
48407
|
+
next,
|
|
48408
|
+
questions,
|
|
48409
|
+
record,
|
|
48410
|
+
skip
|
|
48411
|
+
};
|
|
48412
|
+
};
|
|
48413
|
+
var summarizeVoicePostCallSurveys = (responses) => {
|
|
48414
|
+
const completed = responses.filter((r) => r.completedAt !== null);
|
|
48415
|
+
const ratings = completed.flatMap((r) => r.answers).filter((a) => a.questionId === "nps" && typeof a.value === "number");
|
|
48416
|
+
const promoters = ratings.filter((a) => a.value >= 9).length;
|
|
48417
|
+
const detractors = ratings.filter((a) => a.value <= 6).length;
|
|
48418
|
+
const denom = ratings.length || 1;
|
|
48419
|
+
return {
|
|
48420
|
+
completion: responses.length === 0 ? 0 : completed.length / responses.length,
|
|
48421
|
+
detractors,
|
|
48422
|
+
nps: ratings.length === 0 ? null : (promoters - detractors) / denom * 100,
|
|
48423
|
+
promoters,
|
|
48424
|
+
sampleSize: ratings.length
|
|
48425
|
+
};
|
|
48426
|
+
};
|
|
48427
|
+
// src/dtmfCollector.ts
|
|
48428
|
+
var VOICE_DTMF_DIGITS = [
|
|
48429
|
+
"0",
|
|
48430
|
+
"1",
|
|
48431
|
+
"2",
|
|
48432
|
+
"3",
|
|
48433
|
+
"4",
|
|
48434
|
+
"5",
|
|
48435
|
+
"6",
|
|
48436
|
+
"7",
|
|
48437
|
+
"8",
|
|
48438
|
+
"9",
|
|
48439
|
+
"*",
|
|
48440
|
+
"#"
|
|
48441
|
+
];
|
|
48442
|
+
var isDigit = (value) => VOICE_DTMF_DIGITS.includes(value);
|
|
48443
|
+
var collectVoiceDTMFInput = (options) => {
|
|
48444
|
+
const now = options.now ?? (() => Date.now());
|
|
48445
|
+
const minLength = options.minLength ?? 1;
|
|
48446
|
+
const maxLength = options.maxLength ?? minLength;
|
|
48447
|
+
if (maxLength < minLength) {
|
|
48448
|
+
throw new RangeError(`maxLength (${maxLength}) cannot be less than minLength (${minLength})`);
|
|
48449
|
+
}
|
|
48450
|
+
const terminator = options.terminator === undefined ? "#" : options.terminator;
|
|
48451
|
+
const timeoutMs = options.timeoutMs ?? 8000;
|
|
48452
|
+
const interDigitTimeoutMs = options.interDigitTimeoutMs ?? 3000;
|
|
48453
|
+
const startedAt = now();
|
|
48454
|
+
let lastDigitAt = startedAt;
|
|
48455
|
+
let state = { digits: "", status: "collecting" };
|
|
48456
|
+
const listeners = new Set;
|
|
48457
|
+
const notify = () => {
|
|
48458
|
+
for (const listener of listeners)
|
|
48459
|
+
listener(state);
|
|
48460
|
+
};
|
|
48461
|
+
const checkTimeouts = (at) => {
|
|
48462
|
+
if (state.status !== "collecting")
|
|
48463
|
+
return false;
|
|
48464
|
+
if (at - startedAt > timeoutMs) {
|
|
48465
|
+
state = { digits: state.digits, reason: "timeout", status: "rejected" };
|
|
48466
|
+
notify();
|
|
48467
|
+
return true;
|
|
48468
|
+
}
|
|
48469
|
+
if (state.digits.length > 0 && at - lastDigitAt > interDigitTimeoutMs) {
|
|
48470
|
+
state = { digits: state.digits, reason: "timeout", status: "rejected" };
|
|
48471
|
+
notify();
|
|
48472
|
+
return true;
|
|
48473
|
+
}
|
|
48474
|
+
return false;
|
|
48475
|
+
};
|
|
48476
|
+
const finish = (reason) => {
|
|
48477
|
+
if (state.status !== "collecting")
|
|
48478
|
+
return;
|
|
48479
|
+
const digits = state.digits;
|
|
48480
|
+
if (digits.length < minLength) {
|
|
48481
|
+
state = { digits, reason: "too-short", status: "rejected" };
|
|
48482
|
+
notify();
|
|
48483
|
+
return;
|
|
48484
|
+
}
|
|
48485
|
+
if (options.validator) {
|
|
48486
|
+
const verdict = options.validator(digits);
|
|
48487
|
+
if (verdict !== true) {
|
|
48488
|
+
state = { digits, reason: "invalid", status: "rejected" };
|
|
48489
|
+
notify();
|
|
48490
|
+
return;
|
|
48491
|
+
}
|
|
48492
|
+
}
|
|
48493
|
+
state = { digits, reason, status: "completed" };
|
|
48494
|
+
notify();
|
|
48495
|
+
};
|
|
48496
|
+
return {
|
|
48497
|
+
cancel() {
|
|
48498
|
+
if (state.status === "collecting") {
|
|
48499
|
+
state = { digits: state.digits, status: "cancelled" };
|
|
48500
|
+
notify();
|
|
48501
|
+
}
|
|
48502
|
+
return state;
|
|
48503
|
+
},
|
|
48504
|
+
feed(digit, at = now()) {
|
|
48505
|
+
if (state.status !== "collecting")
|
|
48506
|
+
return state;
|
|
48507
|
+
if (checkTimeouts(at))
|
|
48508
|
+
return state;
|
|
48509
|
+
if (!isDigit(digit)) {
|
|
48510
|
+
state = {
|
|
48511
|
+
digits: state.digits,
|
|
48512
|
+
reason: "invalid",
|
|
48513
|
+
status: "rejected"
|
|
48514
|
+
};
|
|
48515
|
+
notify();
|
|
48516
|
+
return state;
|
|
48517
|
+
}
|
|
48518
|
+
lastDigitAt = at;
|
|
48519
|
+
if (terminator && digit === terminator) {
|
|
48520
|
+
finish("terminator");
|
|
48521
|
+
return state;
|
|
48522
|
+
}
|
|
48523
|
+
const digits = state.digits + digit;
|
|
48524
|
+
state = { digits, status: "collecting" };
|
|
48525
|
+
if (digits.length >= maxLength) {
|
|
48526
|
+
finish("length");
|
|
48527
|
+
} else {
|
|
48528
|
+
notify();
|
|
48529
|
+
}
|
|
48530
|
+
return state;
|
|
48531
|
+
},
|
|
48532
|
+
getState: () => state,
|
|
48533
|
+
prompt: options.prompt,
|
|
48534
|
+
subscribe(listener) {
|
|
48535
|
+
listeners.add(listener);
|
|
48536
|
+
listener(state);
|
|
48537
|
+
return () => {
|
|
48538
|
+
listeners.delete(listener);
|
|
48539
|
+
};
|
|
48540
|
+
},
|
|
48541
|
+
tick(at = now()) {
|
|
48542
|
+
checkTimeouts(at);
|
|
48543
|
+
return state;
|
|
48544
|
+
}
|
|
48545
|
+
};
|
|
48546
|
+
};
|
|
48547
|
+
var validateVoiceDTMFLuhn = (digits) => {
|
|
48548
|
+
if (!/^\d+$/u.test(digits))
|
|
48549
|
+
return false;
|
|
48550
|
+
let sum = 0;
|
|
48551
|
+
let alt = false;
|
|
48552
|
+
for (let i = digits.length - 1;i >= 0; i--) {
|
|
48553
|
+
let n = Number(digits[i]);
|
|
48554
|
+
if (alt) {
|
|
48555
|
+
n *= 2;
|
|
48556
|
+
if (n > 9)
|
|
48557
|
+
n -= 9;
|
|
48558
|
+
}
|
|
48559
|
+
sum += n;
|
|
48560
|
+
alt = !alt;
|
|
48561
|
+
}
|
|
48562
|
+
return sum % 10 === 0;
|
|
48563
|
+
};
|
|
48014
48564
|
export {
|
|
48015
48565
|
writeVoiceProofPack,
|
|
48016
48566
|
writeVoiceMediaPipelineArtifacts,
|
|
@@ -48032,6 +48582,7 @@ export {
|
|
|
48032
48582
|
verifyVoiceOpsWebhookSignature,
|
|
48033
48583
|
validateVoiceWorkflowRouteResult,
|
|
48034
48584
|
validateVoiceObservabilityExportRecord,
|
|
48585
|
+
validateVoiceDTMFLuhn,
|
|
48035
48586
|
ttsAdapterSessionCanCancel,
|
|
48036
48587
|
transcodeTwilioInboundPayloadToPCM16,
|
|
48037
48588
|
transcodePCMToTwilioOutboundPayload,
|
|
@@ -48052,6 +48603,7 @@ export {
|
|
|
48052
48603
|
summarizeVoiceProviderCapabilities,
|
|
48053
48604
|
summarizeVoiceProofAssertions,
|
|
48054
48605
|
summarizeVoiceProductionReadinessGate,
|
|
48606
|
+
summarizeVoicePostCallSurveys,
|
|
48055
48607
|
summarizeVoiceOpsTasks,
|
|
48056
48608
|
summarizeVoiceOpsTaskQueue,
|
|
48057
48609
|
summarizeVoiceOpsTaskAnalytics,
|
|
@@ -48254,6 +48806,7 @@ export {
|
|
|
48254
48806
|
pruneVoiceIncidentBundleArtifacts,
|
|
48255
48807
|
provisionTwilioPhoneNumber,
|
|
48256
48808
|
provisionTelnyxPhoneNumber,
|
|
48809
|
+
predictVoiceCallCost,
|
|
48257
48810
|
parseVoiceTelephonyWebhookEvent,
|
|
48258
48811
|
parseVoiceSessionSnapshot,
|
|
48259
48812
|
normalizeVoiceProofTrendReport,
|
|
@@ -48501,6 +49054,7 @@ export {
|
|
|
48501
49054
|
createVoiceProofPackBuildContext,
|
|
48502
49055
|
createVoiceProofPackArtifacts,
|
|
48503
49056
|
createVoiceProofAssertion,
|
|
49057
|
+
createVoicePromptInjectionGuard,
|
|
48504
49058
|
createVoiceProfileTraceTagger,
|
|
48505
49059
|
createVoiceProfileSwitchReadinessRoutes,
|
|
48506
49060
|
createVoiceProfileSwitchPolicyProofRoutes,
|
|
@@ -48522,6 +49076,7 @@ export {
|
|
|
48522
49076
|
createVoicePostgresCampaignStore,
|
|
48523
49077
|
createVoicePostgresAuditSinkDeliveryStore,
|
|
48524
49078
|
createVoicePostgresAuditEventStore,
|
|
49079
|
+
createVoicePostCallSurvey,
|
|
48525
49080
|
createVoicePostCallAnalysisRoutes,
|
|
48526
49081
|
createVoicePlivoWebhookVerifier,
|
|
48527
49082
|
createVoicePlivoCampaignDialer,
|
|
@@ -48593,6 +49148,7 @@ export {
|
|
|
48593
49148
|
createVoiceHubSpotTaskUpdateSink,
|
|
48594
49149
|
createVoiceHubSpotTaskSyncSinks,
|
|
48595
49150
|
createVoiceHubSpotTaskSink,
|
|
49151
|
+
createVoiceHoldAudioDriver,
|
|
48596
49152
|
createVoiceHelpdeskTicketSink,
|
|
48597
49153
|
createVoiceHandoffHealthRoutes,
|
|
48598
49154
|
createVoiceHandoffHealthJSONHandler,
|
|
@@ -48688,6 +49244,7 @@ export {
|
|
|
48688
49244
|
createVoiceAIJudgeCompletion,
|
|
48689
49245
|
createTwilioVoiceRoutes,
|
|
48690
49246
|
createTwilioVoiceResponse,
|
|
49247
|
+
createTwilioNTSIceServers,
|
|
48691
49248
|
createTwilioMediaStreamBridge,
|
|
48692
49249
|
createTelnyxVoiceRoutes,
|
|
48693
49250
|
createTelnyxVoiceResponse,
|
|
@@ -48719,12 +49276,15 @@ export {
|
|
|
48719
49276
|
createGeminiVoiceAssistantModel,
|
|
48720
49277
|
createDomainPhraseHints,
|
|
48721
49278
|
createDomainLexicon,
|
|
49279
|
+
createCoturnIceServers,
|
|
48722
49280
|
createAnthropicVoiceAssistantModel,
|
|
48723
49281
|
createAIVoiceModel,
|
|
48724
49282
|
conditionAudioChunk,
|
|
48725
49283
|
computePcmDurationMs,
|
|
48726
49284
|
completeVoiceOpsTask,
|
|
48727
49285
|
compareVoiceEvalBaseline,
|
|
49286
|
+
compareVoiceCostScenarios,
|
|
49287
|
+
collectVoiceDTMFInput,
|
|
48728
49288
|
claimVoiceOpsTask,
|
|
48729
49289
|
buildVoiceTraceReplay,
|
|
48730
49290
|
buildVoiceTraceDeliveryReport,
|
|
@@ -48876,11 +49436,14 @@ export {
|
|
|
48876
49436
|
VOICE_WEBHOOK_TIMESTAMP_HEADER,
|
|
48877
49437
|
VOICE_WEBHOOK_SIGNATURE_HEADER,
|
|
48878
49438
|
VOICE_LIVE_OPS_ACTIONS,
|
|
49439
|
+
VOICE_DTMF_DIGITS,
|
|
48879
49440
|
VOICE_CALLER_MEMORY_KEY,
|
|
48880
49441
|
TURN_PROFILE_DEFAULTS,
|
|
48881
49442
|
DEFAULT_VOICE_REDACTION_PATTERNS,
|
|
48882
49443
|
DEFAULT_VOICE_PROOF_TREND_PROFILE_DEFINITIONS,
|
|
48883
49444
|
DEFAULT_VOICE_PROOF_TRENDS_MAX_AGE_MS,
|
|
49445
|
+
DEFAULT_VOICE_PROMPT_INJECTION_RULES,
|
|
48884
49446
|
DEFAULT_VOICE_PRICE_BOOK,
|
|
49447
|
+
DEFAULT_VOICE_POST_CALL_SURVEY_QUESTIONS,
|
|
48885
49448
|
BROWSER_NOISE_SUPPRESSOR_PRESETS
|
|
48886
49449
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type VoicePostCallSurveyQuestion = {
|
|
2
|
+
id: string;
|
|
3
|
+
prompt: string;
|
|
4
|
+
type: "rating" | "boolean" | "comment";
|
|
5
|
+
min?: number;
|
|
6
|
+
max?: number;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type VoicePostCallSurveyAnswer = {
|
|
10
|
+
questionId: string;
|
|
11
|
+
value: number | boolean | string | null;
|
|
12
|
+
};
|
|
13
|
+
export type VoicePostCallSurveyResponse = {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
startedAt: number;
|
|
16
|
+
completedAt: number | null;
|
|
17
|
+
answers: VoicePostCallSurveyAnswer[];
|
|
18
|
+
npsBucket: "promoter" | "passive" | "detractor" | null;
|
|
19
|
+
};
|
|
20
|
+
export type CreateVoicePostCallSurveyOptions = {
|
|
21
|
+
sessionId: string;
|
|
22
|
+
questions?: VoicePostCallSurveyQuestion[];
|
|
23
|
+
now?: () => number;
|
|
24
|
+
};
|
|
25
|
+
export declare const DEFAULT_VOICE_POST_CALL_SURVEY_QUESTIONS: VoicePostCallSurveyQuestion[];
|
|
26
|
+
export declare const createVoicePostCallSurvey: (options: CreateVoicePostCallSurveyOptions) => {
|
|
27
|
+
complete: () => VoicePostCallSurveyResponse;
|
|
28
|
+
getResponse: () => VoicePostCallSurveyResponse;
|
|
29
|
+
next: () => VoicePostCallSurveyQuestion | null;
|
|
30
|
+
questions: VoicePostCallSurveyQuestion[];
|
|
31
|
+
record: (questionId: string, value: number | boolean | string | null) => VoicePostCallSurveyAnswer;
|
|
32
|
+
skip: () => VoicePostCallSurveyQuestion | null;
|
|
33
|
+
};
|
|
34
|
+
export type VoicePostCallSurvey = ReturnType<typeof createVoicePostCallSurvey>;
|
|
35
|
+
export declare const summarizeVoicePostCallSurveys: (responses: VoicePostCallSurveyResponse[]) => {
|
|
36
|
+
completion: number;
|
|
37
|
+
detractors: number;
|
|
38
|
+
nps: number | null;
|
|
39
|
+
promoters: number;
|
|
40
|
+
sampleSize: number;
|
|
41
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Transcript } from "./types";
|
|
2
|
+
export type VoicePromptInjectionRule = {
|
|
3
|
+
/** Reason emitted in the verdict when this rule matches. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** Regex applied to the transcript text (case-insensitive by convention). */
|
|
6
|
+
pattern: RegExp;
|
|
7
|
+
/** Severity tag for downstream routing. */
|
|
8
|
+
severity?: "high" | "low" | "medium";
|
|
9
|
+
};
|
|
10
|
+
export type VoicePromptInjectionVerdict = {
|
|
11
|
+
matches: Array<{
|
|
12
|
+
label: string;
|
|
13
|
+
matchedText: string;
|
|
14
|
+
severity?: VoicePromptInjectionRule["severity"];
|
|
15
|
+
}>;
|
|
16
|
+
ok: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare const DEFAULT_VOICE_PROMPT_INJECTION_RULES: VoicePromptInjectionRule[];
|
|
19
|
+
export type CreateVoicePromptInjectionGuardOptions = {
|
|
20
|
+
/** Custom rule set. Defaults to DEFAULT_VOICE_PROMPT_INJECTION_RULES. */
|
|
21
|
+
rules?: ReadonlyArray<VoicePromptInjectionRule>;
|
|
22
|
+
/** Replacement string for sanitized text. Default '[REDACTED:INJECTION]'. */
|
|
23
|
+
sanitizedReplacement?: string;
|
|
24
|
+
};
|
|
25
|
+
export type VoicePromptInjectionGuard = {
|
|
26
|
+
evaluate: (input: string | Transcript) => VoicePromptInjectionVerdict;
|
|
27
|
+
rules: ReadonlyArray<VoicePromptInjectionRule>;
|
|
28
|
+
sanitize: (input: string) => string;
|
|
29
|
+
};
|
|
30
|
+
export declare const createVoicePromptInjectionGuard: (options?: CreateVoicePromptInjectionGuardOptions) => VoicePromptInjectionGuard;
|