initial commit of distributor before upgrades and optimizations.

This commit is contained in:
Peter Morton 2025-05-29 10:15:12 -05:00
parent d9934cd2ad
commit db05f06137
8 changed files with 2371 additions and 0 deletions

View File

@ -0,0 +1,35 @@
{
"nlu": {
"apiBaseURL": "http://ivas-router.prd1",
"modelName": "MVP",
"settings": {
"intentConfidenceThreshold": 0.4
}
},
"suffix": "6647ad7b84df93835bbb4346",
"waitHoldTimeout": "8s",
"waitHoldQueueStatus": "20",
"responses": {
"welcome": "Hello, I'm a virtual assistant. How can I help you?",
"unrecognized": "I did not understand what you said. Can you please say it again?",
"unanswered": "I understand what you said, but I have not been taught how to answer your question... yet.",
"noinput": "If you said something, I did not hear it. Can you please repeat that?",
"speechtimeout": "We may be having noise issues with our call, Can you repeat that?",
"transferCall": "Forwarding to a live operator",
"pleaseContinueToHold": "Please continue to wait for the next available representative.",
"safetynet": "There was a problem processing your request. How else can I help?"
},
"ivr": {
"hostname": "https://router.ivastudio.verint.live",
"applicationName": "SC Distr"
},
"transfer": {
"destination": ""
},
"integrations": {
"CAInterface": {
"enabled": false,
"suffix": ""
}
}
}

View File

@ -0,0 +1,17 @@
console.log(req.body); // <- Post request body
console.log(res.query); // <- Query strings
const vxmlResponse = `<?xml version="1.0" encoding="UTF-8"?>
<vxml version="2.1" xmlns="http://www.w3.org/2001/vxml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.w3.org/2001/vxml http://www.w3.org/TR/2007/REC-voicexml21-20070619/vxml.xsd">
<form>
<block>
<prompt>An error has occurred</prompt>
<exit />
</block>
</form>
</vxml>`;
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
res.setHeader('etag', nanoid());
res.type('text/xml');
res.send(vxmlResponse);

View File

@ -0,0 +1,20 @@
console.log({
frontdoor: "1.0",
body: req.body,
query: req.query,
params: req.params
});
const brand = req.query.brand;
(async () => {
const vxmlResponse = vxmlgen_gvf_6647ad7b84df93835bbb4346().frontDoor({
hostname: voiceSettings_6647ad7b84df93835bbb4346.ivr.hostname,
workspaceId: req.params.workspaceId,
branch: req.params.branch,
suffix: voiceSettings_6647ad7b84df93835bbb4346.suffix,
brand
});
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
res.setHeader('etag', nanoid());
res.type('text/xml');
res.send(vxmlResponse);
})();

View File

@ -0,0 +1,431 @@
const utterance = req.body.utterance;
const sessionId = req.body.sessionId;
console.log({
loop: "1.6",
utterance,
sessionId
});
const getNested = (obj, ...args) => {
return args.reduce((obj, level) => {
return obj && obj[level];
}, obj);
};
if (req.body.body && req.body.query) {
const tmpBody = JSON.parse(JSON.stringify(req.body.body));
const tmpQuery = JSON.parse(JSON.stringify(req.body.query));
req.query = tmpQuery;
req.body = tmpBody;
}
if (req.body.sessionJSON) {
try {
req.body.sessionJSON = JSON.parse(req.body.sessionJSON);
} catch (e) {
console.log(e.message);
}
}
const verbose = voiceSettings_6647ad7b84df93835bbb4346.verbose || false;
const useDomain = req.body.useDomain;
const route = req.params && req.params[0] && req.params[0].split('/').slice(-1)[0];
const requestId = uuidv4();
const safetyNet = voiceSettings_6647ad7b84df93835bbb4346.responses.safetynet;
const waitHoldTimeout = voiceSettings_6647ad7b84df93835bbb4346.waitHoldTimeout;
let promptText = voiceSettings_6647ad7b84df93835bbb4346.responses.welcome;
let disconnect;
let waveform;
let gcsUri;
let fileName;
let recognizedData;
if (req.body.waveform) {
try {
waveform = url.parse(req.body.waveform, true).query.googleUri;
fileName = url.parse(waveform, true);
if (fileName && fileName.pathName) gcsUri = `gs:/${fileName.pathname}`;
} catch (e) {
console.log(e.message);
}
}
const isCA = () => {
return voiceSettings_6647ad7b84df93835bbb4346?.integrations?.CAInterface?.enabled || false;
};
const getCA = () => {
if (isCA()) {
let ca = "CAInterface";
if (voiceSettings_6647ad7b84df93835bbb4346.integrations.CAInterface.suffix) {
ca += voiceSettings_6647ad7b84df93835bbb4346.integrations.CAInterface.suffix;
}
ca += "()";
return eval(ca);
}
return undefined;
};
const getInteractionId = async (ani, dnis, xSpVerintId) => {
const suffix = voiceSettings_6647ad7b84df93835bbb4346.suffix && `_${voiceSettings_6647ad7b84df93835bbb4346.suffix}` || "";
const callback = `${voiceSettings_6647ad7b84df93835bbb4346.ivr.hostname}/ProxyScript/run/${req.params.workspaceId}/${req.params.branch}/vxml_transfer${suffix}?sessionId=${sessionId}`;
let tisId;
const CA = getCA();
if (CA) {
try {
const OIDC = await CA.getOIDC();
if (OIDC.access_token) {
tisId = await CA.createTelephonyInteraction(OIDC, ani, dnis, xSpVerintId, callback);
} else {
console.log("getInteractionId failed: Bad OIDC credentials");
console.log(OIDC);
}
} catch (error) {
console.log(`getInteractionId failed. error: ${error.message}`);
}
}
return tisId;
};
const blockWaitLiveOperator = async (smStr, sessionMap, CA) => {
const start = Date.now();
let rd;
const loopWait = 100;
const total = voiceSettings_6647ad7b84df93835bbb4346.waitHoldQueueStatus * 1000 / loopWait;
for (let i = 0; i < total; i++) {
const sessionMap = await redis.hGetAll(smStr);
if (sessionMap.transferCall) {
console.log("Loop: transfer call");
rd = {
answers: [voiceSettings_6647ad7b84df93835bbb4346.responses.transferCall],
endCall: true
};
return rd;
}
await new Promise(r => setTimeout(r, loopWait));
}
if (CA) {
try {
const OIDC = await CA.getOIDC();
// fallthrough, provide a queue status update
const predictions = await CA.getWaitTime(OIDC, sessionMap.interactionId);
rd = await run_app_event(sessionMap, smStr, "VXML Queue Status Event", "vxmlqueuestatus", voiceSettings_6647ad7b84df93835bbb4346.responses.pleaseContinueToHold, {
predictions
});
} catch (error) {
console.log("/predictions failed" + error.message);
}
}
if (!rd) {
rd = {
answers: [voiceSettings_6647ad7b84df93835bbb4346.responses.pleaseContinueToHold]
};
}
console.log({
blockWaitElapsed: Date.now() - start
});
return rd;
};
const sequenceApiCalls = async (smStr, interactionId, apiCalls) => {
const CA = getCA();
if (CA) {
await redis.hSet(smStr, {
waitForApi: "true"
});
const OIDC = await CA.getOIDC();
for (let api of apiCalls) {
try {
if (api.api == "patchCallInfo") {
await CA.patchCallInfo(OIDC, interactionId, api.params);
} else if (api.api = "connectWithLiveOperator") {
const isVirtualHold = api.params.isVirtualHold;
const callReason = api.params.callReason;
if (isVirtualHold || callReason) {
await CA.patchCallInfo(OIDC, interactionId, {
isVirtualHold,
callReason
});
}
await CA.connectWithLiveOperator(OIDC, interactionId);
redis.hSet(smStr, {
waitForLiveOperator: "true",
lastQueueStatus: JSON.stringify(Date.now())
});
redis.hDel(smStr, "pendingAPIs");
} else {
console.log({
badapi: api
});
}
} catch (err) {
console.log(`Error: ${api.api} failed. ${err.message}`);
}
}
redis.hDel(smStr, "waitForApi");
}
};
const run_app_event = async (sessionMap, smStr, intent, input, defaultAnswer, metadata = {}) => {
const configuration = {
directIntentHit: intent
};
return await run_model_and_report(sessionMap, smStr, input, defaultAnswer, configuration, metadata);
};
const run_model_and_report = async (sessionMap, smStr, input, defaultAnswer, configuration, metadata = {}) => {
let privateData = voiceSettings_6647ad7b84df93835bbb4346.ivr.privateData || false;
let interactionId = sessionMap.interactionId;
let {
data: recognizedData
} = await axios.post(`${voiceSettings_6647ad7b84df93835bbb4346.nlu.apiBaseURL}/Model/run/${req.params.workspaceId}/${req.params.branch}/${voiceSettings_6647ad7b84df93835bbb4346.nlu.modelName}`, {
input,
configuration,
conversationId: sessionMap.conversationId,
settings: voiceSettings_6647ad7b84df93835bbb4346.nlu.settings,
channel: "voice",
// for old multi-channel widgets
metadata: {
channel: 'voice',
gcsUriAudio: waveform,
ani: sessionMap.ani,
dnis: sessionMap.dnis,
callid: sessionMap.callid,
xSpVerintId: sessionMap.xSpVerintId,
interactionId,
sessionId,
syntheticAudio: true,
utteranceFileName: `${fileName?.pathname?.replace('/asr-archive/', '') || ""}`,
...metadata
}
});
privateData = recognizedData.ivrSettings?.privateData || privateData;
if (!privateData && recognizedData.conversationId && sessionMap.conversationId === undefined) {
db.analytics.addConversation({
id: recognizedData.conversationId
});
}
if (recognizedData.conversationId) {
await redis.hSet(smStr, {
conversationId: recognizedData.conversationId
});
}
if (defaultAnswer && recognizedData.answers.length === 0) {
recognizedData.answers = [defaultAnswer];
} else if (recognizedData.classificationResults.length === 0 && recognizedData.answers.length === 0) {
recognizedData.answers = [voiceSettings_6647ad7b84df93835bbb4346.responses.unrecognized];
} else if (recognizedData.answers.length === 0) {
recognizedData.answers = [voiceSettings_6647ad7b84df93835bbb4346.responses.unanswered];
}
if (!recognizedData.ivrSettings) {
recognizedData.ivrSettings = {};
}
let timeout;
// look for the wait-hold event from the flow
if (recognizedData.vxmlCAApis) {
timeout = waitHoldTimeout;
// async
if (recognizedData.vxmlCAApis.find(api => api.api == "connectWithLiveOperator")) {
// defer api until after the current prompt finishes. next loop
console.log("Deferring VC Apis");
await redis.hSet(smStr, {
waitForLiveOperator: "true",
pendingAPIs: JSON.stringify(recognizedData.vxmlCAApis)
});
sessionMap.waitForLiveOperator = true;
} else {
sequenceApiCalls(smStr, interactionId, recognizedData.vxmlCAApis);
}
} else if (sessionMap.waitForLiveOperator) {
timeout = waitHoldTimeout;
}
if (timeout && !recognizedData.ivrSettings.timeout) {
recognizedData.ivrSettings.timeout = timeout;
}
// add dialogName hueristically
if (!recognizedData.ivrSettings.dialogName) {
let dialogName = "";
if (recognizedData.classificationResults.length) {
dialogName = recognizedData.classificationResults[0].label.split(/\s+/).join('_');
}
if (recognizedData.answers.length) {
let words = recognizedData.answers[0].split(/\s+/).map(word => {
return word.replace(/[^a-z0-9]/gi, '');
}).filter(word => word.length > 0);
for (let i = 0; i < Math.min(6, words.length); i++) {
dialogName += "_" + words[i];
}
}
if (dialogName.length) {
recognizedData.ivrSettings.dialogName = dialogName;
}
}
if (!privateData && input !== "vxmlqueuestatus") {
db.analytics.addTransaction(recognizedData);
}
return recognizedData;
};
// DEMO DISTRIBUTOR CODE
console.log("req.body.interpretation");
console.log(req.body.interpretation);
const selectedWorkspaceOption = req.body.interpretation;
console.log("Selected Option:");
console.log(selectedWorkspaceOption);
let vxmlResponse;
const generateVXML = async () => {
try {
// Common properties used in all cases, unless specified in the case block
const commonProperties = {
hostname: "https://router.ivastudio.verint.live",
branch: "current",
requestId,
promptText,
disconnect,
recognizedData,
...voiceSettings_6647ad7b84df93835bbb4346.ivr,
};
switch (selectedWorkspaceOption) {
case "1":
// Peter's Workspace
vxmlResponse = vxmlgen_gvf_6647ad7b84df93835bbb4346().frontDoor({
workspaceId: "67bca862210071627d32ef12",
suffix: "6647ad7b84df93835bbb4346",
...commonProperties,
});
break;
case "2":
// David's Workspace
vxmlResponse = vxmlgen_gvf_6647ad7b84df93835bbb4346().frontDoor({
workspaceId: "67c9ddd765e40fd3d60ef441",
suffix: "6647ad7b84df93835bbb4346",
...commonProperties,
});
break;
case "3":
// Ramzi's Workspace
vxmlResponse = vxmlgen_gvf_6647ad7b84df93835bbb4346().frontDoor({
workspaceId: "67c758bf8e90ca81122106a5",
suffix: "6647ad7b84df93835bbb4346",
...commonProperties,
});
break;
case "4":
// Amber's Workspace
vxmlResponse = vxmlgen_gvf_6647ad7b84df93835bbb4346().frontDoor({
workspaceId: "67c9dde765e40fd3d60ef446",
suffix: "6647ad7b84df93835bbb4346",
...commonProperties,
});
break;
case "5":
// Ring Central Workspace
vxmlResponse = vxmlgen_gvf_6647ad7b84df93835bbb4346().frontDoor({
workspaceId: "68098e35ad4260eaf807cff2",
suffix: "6647ad7b84df93835bbb4346",
...commonProperties,
});
break;
default:
vxmlResponse = vxmlgen_gvf_6647ad7b84df93835bbb4346().loop({
hostname: voiceSettings_6647ad7b84df93835bbb4346.ivr.hostname,
workspaceId: req.params.workspaceId,
suffix: voiceSettings_6647ad7b84df93835bbb4346.suffix,
branch: req.params.branch,
sessionId,
...commonProperties,
});
break;
}
// END OF DEMO DISTRIBUTOR CODE
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
res.setHeader('etag', nanoid());
res.type('text/xml');
if (verbose) {
console.log('============================');
console.log(vxmlResponse);
console.log('++++++++++++++++++++++++++++');
}
res.send(vxmlResponse);
} catch (e) {
console.log(e.message);
if (e.toJSON) console.log(e.toJSON());
res.status(500);
}
};
if (sessionId) {
const smStr = `session-map-${sessionId}`;
(async () => {
if (req.body.sessionJSON && req.body.sessionJSON) {
const ani = getNested(req.body, 'sessionJSON', 'telephone', 'ani').split('@')[0];
const dnis = getNested(req.body, 'sessionJSON', 'connection', 'protocol', 'sip', 'requesturi').split('@')[0];
const callid = getNested(req.body, 'sessionJSON', 'connection', 'protocol', 'sip', 'headers', 'call-id').split('_')[0];
let xSpVerintId = getNested(req.body, 'sessionJSON', 'connection', 'protocol', 'sip', 'headers', 'x-sp-verintid') || callid;
if (utterance == undefined) {
getInteractionId(ani, dnis, callid).then(interactionId => {
if (interactionId) {
redis.hSet(smStr, {
interactionId
});
}
});
await redis.hSet(smStr, {
ani,
dnis,
callid,
xSpVerintId
});
await redis.expire(smStr, 60 * 60 * 24);
}
}
const sessionMap = await redis.hGetAll(smStr);
Object.keys(sessionMap).forEach((key, i) => {
try {
sessionMap[key] = JSON.parse(sessionMap[key]);
} catch (e) {}
});
console.log(sessionMap);
if (sessionMap.waitForApi) {
promptText = undefined;
recognizedData = {
answers: [""],
ivrSettings: {
timeout: waitHoldTimeout
}
};
} else if (sessionMap.endCall) {
promptText = undefined;
recognizedData = {
answers: [""],
endCall: true
};
} else if (sessionMap.transferCall) {
recognizedData = await run_app_event(sessionMap, smStr, 'VXML Transfer Call Event', 'vxmltransfercall', voiceSettings_6647ad7b84df93835bbb4346.responses.transferCall);
recognizedData.endCall = true;
} else if (sessionMap.waitForLiveOperator) {
if (sessionMap.pendingAPIs) {
// async. success will delete sessionMap.pendingApis
sequenceApiCalls(smStr, sessionMap.interactionId, sessionMap.pendingAPIs);
}
recognizedData = await blockWaitLiveOperator(smStr, sessionMap, getCA());
} else if (route === 'noinput') {
recognizedData = await run_app_event(sessionMap, smStr, 'VXML Noinput Event', 'vxmlnoinput', voiceSettings_6647ad7b84df93835bbb4346.responses.noinput);
} else if (route === 'nomatch') {
recognizedData = await run_app_event(sessionMap, smStr, 'VXML Unrecognized Event', 'vxmlunrecognized', voiceSettings_6647ad7b84df93835bbb4346.responses.unrecognized);
} else if (route === 'maxspeechtimeout') {
recognizedData = await run_app_event(sessionMap, smStr, 'VXML Max Speech Timeout', 'vxmlmaxspeechtimeout', voiceSettings_6647ad7b84df93835bbb4346.responses.speechtimeout);
} else if (utterance) {
recognizedData = await run_model_and_report(sessionMap, smStr, utterance);
} else {
recognizedData = await run_app_event(sessionMap, smStr, 'VXML Welcome Event', 'vxmlwelcome', voiceSettings_6647ad7b84df93835bbb4346.responses.welcome);
}
if (recognizedData && recognizedData.transferCall) {
recognizedData.endCall = true;
await redis.hSet(smStr, {
transferCall: "true",
transferTo: recognizedData.transferTo || voiceSettings_6647ad7b84df93835bbb4346.transfer.destination
});
}
promptText = recognizedData && recognizedData.answers && recognizedData.answers.join(' ') || promptText;
disconnect = recognizedData && recognizedData.endCall || false;
generateVXML(sessionMap);
})().catch(e => {
console.log(e.message);
promptText = safetyNet;
generateVXML();
});
} else {
generateVXML();
}

View File

@ -0,0 +1,57 @@
console.log({
stop: "1.5",
body: req.body,
query: req.query,
params: req.params
});
const getCA = () => {
if (voiceSettings_6647ad7b84df93835bbb4346?.integrations?.CAInterface?.enabled) {
let ca = "CAInterface";
if (voiceSettings_6647ad7b84df93835bbb4346.integrations.CAInterface.suffix) {
ca += voiceSettings_6647ad7b84df93835bbb4346.integrations.CAInterface.suffix;
}
ca += "()";
return eval(ca);
}
return undefined;
};
const sessionId = req.body.sessionId;
(async () => {
const smStr = `session-map-${sessionId}`;
const sessionMap = await redis.hGetAll(smStr);
const terminationObject = {
type: 'RETURN'
};
const transferCall = sessionMap.transferCall || req.headers["transfer-call-tester"];
if (transferCall) {
terminationObject.type = 'TRANSFER';
let dest = sessionMap.transferTo || voiceSettings_6647ad7b84df93835bbb4346.transfer.destination;
const params = [];
if (sessionMap.xSpVerintId) params.push("X-SP-VerintID=" + encodeURIComponent(sessionMap.xSpVerintId));
for (let [index, value] of params.entries()) {
if (index == 0) {
dest += "?";
} else {
dest += "&";
}
dest += value;
}
terminationObject.destination = dest;
console.log({
terminationObject
});
} else if (sessionMap.waitForLiveOperator === "true" && sessionMap.interactionId) {
const CA = getCA();
if (CA) {
CA.endTelephonyInteraction(await CA.getOIDC(), sessionMap.interactionId, "abandoned-in-queue");
}
}
const vxmlResponse = vxmlgen_gvf_6647ad7b84df93835bbb4346().stop({
terminationObject
});
console.log(vxmlResponse);
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
res.setHeader('etag', nanoid());
res.type('text/xml');
res.send(vxmlResponse);
})();

View File

@ -0,0 +1,22 @@
console.log({
transfer: "1.2",
query: req.query
});
const sessionId = req.query.sessionId;
console.log("Transfer", sessionId);
if (sessionId) {
const smStr = `session-map-${sessionId}`;
(async () => {
await redis.hSet(smStr, {
transferCall: "true",
waitForLiveOperator: "false"
});
res.send(200);
})().catch(e => {
console.log("error");
console.log(e.message);
res.send(500);
});
} else {
res.send(400);
}