bot-script
Version:
Scripting tool to Write Bot's Workflow
1,370 lines (1,327 loc) • 80.8 kB
JavaScript
/**
* Created by agnibha on 12/1/17.
*/
var ScriptLoader = require("./loader");
var NLPParser = require("./nlpparser");
var path = require("path");
var fs = require("fs");
var AUXILIARY_STACK_IDENTIFIER = "auxiliary_stack";
var placeholders_regex = /({{)(\w+)(}})/g;
var single_component_regex = /({{)(\w+)(}})/;
var appDir = process.cwd();
var has = Object.prototype.hasOwnProperty;
var request = require("request");
var config = require("../resources/config.json");
var TYPING_ON = JSON.stringify({
type : "typing",
status: "on"
});
var predefinedGenerators = require("./dataGenerator");
/**
* Default Options Set
* @type {{current_dir: *, start_section: string, nlp_threshold: number}}
*/
var default_options = {
current_dir : appDir,
start_section : "default.main",
nlp_threshold : 0.2,
data : predefinedGenerators,
fromExceptionHandler: false,
isAuxiliaryFlow : false
};
var cached_refmsgid_stateid_map = {};
var lastMessages = [];
/**
* Main exposed method.
* @param options
* @param event
* @param context
*/
function execute(options, event, context) {
try {
var parseOptions = {};
parseOptions.success = function (script, executors, refmsg_stateid_map) {
//creating default options support.
var executorOptions = Object.assign({}, default_options, options);
// executorOptions.stateIdentifier =
// getStateIdentifier(executorOptions, event, context) || "botscript";
if (event.messageobj && event.messageobj.type === "event") {
handleBotEvents(
executorOptions,
event,
context,
(options, event, context) => {
setupExecutorOptions(
options,
event,
context,
script,
executors,
refmsg_stateid_map
);
}
);
} else {
setupExecutorOptions(
executorOptions,
event,
context,
script,
executors,
refmsg_stateid_map
);
}
};
event = extractMessageIdForFlow(options, context, event);
console.log(`Modified Event => ${JSON.stringify(event)}`);
parseOptions.error = options.error ? options.error : console.error;
if (!options.script) {
ScriptLoader.parse(parseOptions, options.current_dir);
} else {
parseOptions.success(options.script, {}, options.refmsg_stateid_map);
}
}
catch (e) {
( options.error || console.error )(e.message);
}
}
function setupExecutorOptions(
options,
event,
context,
script,
executors,
refmsg_stateid_map
) {
options.script = script;
if (!options.data) {
options.data = {};
}
options.nextProgramCounter = nextProgramCounter;
options.getCurrentStateData = getCurrentStateData;
options.getCurrentSectionName = getCurrentSectionName;
options.updateCurrentStateData = updateCurrentStateData;
options.hasSection = hasSection;
options.executeCommonState = generateMethodForSpecialModule(
ScriptLoader.COMMON
);
cached_refmsgid_stateid_map = Object.assign(
cached_refmsgid_stateid_map,
refmsg_stateid_map
);
if (options.enableMultiStateJump) {
options.fallthrough = false;
}
if (executors) {
options.executors = executors;
}
if (options.escapeKeywords) {
options.escapeKeywords = options.escapeKeywords.map(function (
element,
index,
array
) {
return element.toLowerCase();
});
}
generateRequiredParameters(options, event, context);
}
function generateRequiredParameters(options, event, context) {
"use strict";
let pathToBotConfig = path.join(options.current_dir, ".botconfig");
fs.exists(pathToBotConfig, exists => {
if (exists && ( !options.apikey || options.apikey.length === 0 )) {
let propertiesReader = require("properties-reader");
let property = propertiesReader(pathToBotConfig);
options.apikey = property.get("apikey");
options.botUUID = property.get("botUUID");
} else {
console.error(".botconfig file not found. Cannot Load APIKEY.");
}
options.stateIdentifier =
getStateIdentifier(options, event, context) || "botscript";
ensureState(options, context);
if (
options.isAuxiliaryFlow &&
event.messageobj &&
event.messageobj.referralParam
) {
logAuxiliaryFlow(options, event, context, () => {
parseSuccess(options, event, context);
});
} else {
parseSuccess(options, event, context);
}
});
}
function handleBotEvents(options, event, context, onSuccess) {
let eventHandler;
if (options.eventHandlers) {
eventHandler = options.eventHandler[event.message];
}
if (eventHandler && typeof eventHandler === "function") {
eventHandler(options, event, context, onSuccess);
} else {
switch (event.message) {
case "botmappedevent":
case "startchattingevent":
case "referrallinkclicked":
delete context.simpledb.roomleveldata[options.stateIdentifier + ":pc"];
delete context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
];
break;
}
}
onSuccess(options, event, context);
}
function logAuxiliaryFlow(options, event, context, callback) {
if (options.apikey) {
console.log("Logging Auxiliary Flow");
var options = {
method : "PUT",
url : `${
process.env.db_environment
? config[process.env.db_environment]
: "https://api.gupshup.io"
}/sm/api/v1/bot/botcampaign/report`,
headers: {
"cache-control": "no-cache",
"content-type" : "application/x-www-form-urlencoded",
apikey : options.apikey
},
form : {
contextObj: JSON.stringify(event.contextobj),
refParam : event.messageobj.referralParam,
personName: event.senderobj.display
}
};
request(options, function (error, response, body) {
if (error) console.log(`Error While Logging `);
callback();
});
} else {
console.log("APIKEY Not Found. Cannot Log Auxiliary Flow.");
callback();
}
}
function getStateIdentifier(options, event, context) {
"use strict";
if (
event.messageobj.type === "event" &&
event.message === "referrallinkclicked" &&
event.messageobj &&
event.messageobj.referralParam
) {
let referralParam = tryParseJSON(atob(event.messageobj.referralParam));
if (
event.messageobj &&
event.messageobj.type === "event" &&
referralParam &&
referralParam.botUUID === options.botUUID
) {
delete context.simpledb.roomleveldata[AUXILIARY_STACK_IDENTIFIER + ":pc"];
delete context.simpledb.roomleveldata[
AUXILIARY_STACK_IDENTIFIER + ":call_stack"
];
options.isAuxiliaryFlow = true;
options.start_section = referralParam.flowName;
return AUXILIARY_STACK_IDENTIFIER;
}
}
if (
context.simpledb.roomleveldata.hasOwnProperty(
AUXILIARY_STACK_IDENTIFIER + ":pc"
)
) {
options.isAuxiliaryFlow = true;
return AUXILIARY_STACK_IDENTIFIER;
}
return options.stateIdentifier;
}
/**
* Generates DB and State Data From RoomLevelData.
* @param options
* @param context
*/
function ensureState(options, context) {
if (context.simpledb.roomleveldata.hasOwnProperty("pc")) {
var programCounter = context.simpledb.roomleveldata.pc;
if (
programCounter.hasOwnProperty("section") &&
programCounter.hasOwnProperty("state") &&
programCounter.hasOwnProperty("call_stack")
) {
console.log("Old Program Counter Storage Found. Chaniging Keys");
context.simpledb.roomleveldata[options.stateIdentifier + ":pc"] = JSON.parse(JSON.stringify(context.simpledb.roomleveldata.pc));
delete context.simpledb.roomleveldata.pc;
}
} else if (
context.simpledb.roomleveldata.hasOwnProperty(
options.stateIdentifier + ":pc"
)
) {
options.PC =
context.simpledb.roomleveldata[options.stateIdentifier + ":pc"];
}
if (
!context.simpledb.roomleveldata[options.stateIdentifier + ":call_stack"]
) {
context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
] = [];
}
if (
context.simpledb.roomleveldata.call_stack &&
context.simpledb.roomleveldata.call_stack.length > 0
) {
try {
var stackObject = JSON.parse(
context.simpledb.roomleveldata.call_stack[0]
);
if (
stackObject.hasOwnProperty("pc") &&
stackObject.hasOwnProperty("data")
) {
console.log("Previous Data Found. Creating Copy in new key");
context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
] = JSON.parse(
JSON.stringify(context.simpledb.roomleveldata.call_stack)
);
delete context.simpledb.roomleveldata["call_stack"];
} else {
console.log("Not Our Object call_stack");
}
}
catch (e) {
console.log("Not Our Object call_stack");
}
}
}
/**
* Called on script parsing success.
* @param options
* @param event
* @param context
*/
function parseSuccess(options, event, context) {
try {
if (!options.PC) {
options.PC = new ProgramCounter(
options.start_section,
getStartState(options.script, options.start_section, options.error),
[]
);
executeNextState(options, event, context);
} else {
if (
event.messageobj &&
event.messageobj.refmsgid &&
cached_refmsgid_stateid_map.hasOwnProperty(event.messageobj.refmsgid)
) {
var matchedData =
cached_refmsgid_stateid_map[event.messageobj.refmsgid];
console.log(
`MSG Id Match Found ::: Section => ${matchedData.section} Label => ${
matchedData.state.label
}`
);
if (options.PC.section !== matchedData.section) {
console.log(
"Executing Different Section State jump form refmsgid " +
event.messageobj.refmsgid
);
context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
].push(new ThreadData(options.PC, options.data));
}
options.PC = new ProgramCounter(
matchedData.section,
matchedData.state.label,
[]
);
options.PC.call_stack = getPathToRoot(options);
}
handleBotMessage(options, event, context);
}
}
catch (e) {
if (options.error) {
options.error(e);
} else {
console.error(e);
}
}
}
/**
* Function to handle Non-reFid Messages.
* @param options
* @param event
* @param context
*/
function handleBotMessage(options, event, context) {
options.next_state = -1;
var parser = getParser(options);
if (
options.escapeKeywords &&
options.escapeKeywords.indexOf(event.message.toLowerCase()) > -1
) {
if (event.type === "event") {
eventParser(options, event, context);
} else {
fallbackParser(options, event, context);
}
} else if (
event.messageobj.subType === "p_menu" &&
options.hasSection(ScriptLoader.PERSISTENT_MENU)
) {
generateMethodForSpecialModule(ScriptLoader.PERSISTENT_MENU)(
options,
event,
context,
() => {
"use strict";
if (!options.fallthrough) {
if (!options.exception) {
options.exception = new ScriptException(
"NLP Response is less than the threshold value"
);
}
handleException(options, event, context);
} else {
options.next_state = options.lastRememberedState;
options.output_messages = options.lastRememberedOutputMessages;
options = generateNextState(options);
stopContinuingExecution(options, event, context);
}
}
);
} else if (parser) {
if (typeof parser === "function") {
options.parserChain = [parser];
if (options.addDefaultParser) {
options.parserChain.push(fallbackParser);
}
options.parserChain.reverse();
executeParserChain(options, event, context);
} else if (Array.isArray(parser)) {
options.parserChain = JSON.parse(JSON.stringify(parser));
if (options.addDefaultParser) {
options.parserChain.push(fallbackParser);
}
options.parserChain.reverse();
executeParserChain(options, event, context);
}
} else {
if (event.type === "event") {
eventParser(options, event, context);
} else {
fallbackParser(options, event, context);
}
}
}
/**
* This method is used to execute the parser Chain. The parser chain has the last parser as the fallback parser
* so need not to worry about the what if a next state is not provided. The fallback is supposed to give a next state
* whenever possible otherwise the exception handler will get called.
* @param options
* @param event
* @param context
*/
function executeParserChain(options, event, context) {
if (
options.parserChain.length > 0 &&
( !options.next_state || options.next_state === -1 )
) {
options.parserChain.pop()(options, event, context, executeParserChain);
} else if (options.next_state && options.next_state !== -1) {
executeNextState(options, event, context);
} else if (
options.parserChain.length === 0 &&
( !options.next_state || options.next_state === -1 )
) {
//Exception Handler
if (!options.exception) {
options.exception = new ScriptException("NO Next State Found", 4);
}
handleException(options, event, context);
}
}
/**
* This is the main function to handle exceptions. It traverses over the stack trace to found out the exception handler
* otherwise finds for the default handler and if nothing can be found then the default exception handler is called.
* @param options
* @param event
* @param context
*/
function handleException(options, event, context) {
let nextStatesHavingOnException = options
.getCurrentStateData()
.nextstates.filter(item => {
"use strict";
let nextStateData = getStateData(
options.script,
options.PC.section,
item,
options.error
);
return nextStateData.action.action === ScriptLoader.ON_EXCEPTION;
});
if (nextStatesHavingOnException.length === 1) {
console.log(
`OnException Found at State => ${
nextStatesHavingOnException[0]
}, executing that state`
);
options.next_state = nextStatesHavingOnException[0];
executeNextState(options, event, context);
} else if (nextStatesHavingOnException.length > 1) {
console.log(
`Multiple Children Found Having OnException. States are ${nextStatesHavingOnException.join(
","
)}. Executing the First State.`
);
options.next_state = nextStatesHavingOnException[0];
executeNextState(options, event, context);
} else {
if (options.exception) {
console.log(
"Handle Exception Called for Reason =>" +
options.exception.message +
" at state => " +
options.PC.state
);
}
if (!options.exception_call_stack) {
options.exception_call_stack = JSON.parse(
JSON.stringify(
context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
]
)
);
}
if (!options.exception_PC) {
options.exception_PC = JSON.parse(JSON.stringify(options.PC));
}
if (!options.previous_PC) {
options.previous_PC = JSON.parse(JSON.stringify(options.PC));
}
var state,
exceptionHandler,
defEx = false,
stateData;
outerLoop: do {
while (options.exception_PC.call_stack.length > 0) {
state = options.exception_PC.call_stack.pop();
stateData = getStateData(
options.script,
options.exception_PC.section,
state,
options.error
);
if (stateData && stateData.type === ScriptLoader.BOT) {
exceptionHandler = getExceptionHandler(options, state);
if (exceptionHandler) {
break outerLoop;
} else {
if (stateData && stateData.action.action === ScriptLoader.DEFEX) {
defEx = true;
break outerLoop;
}
}
}
}
if (options.exception_call_stack.length > 0) {
options.exception_PC = options.exception_call_stack.pop().pc;
} else {
options.exception_PC = undefined;
}
} while (typeof options.exception_PC !== "undefined");
if (exceptionHandler && typeof exceptionHandler === "function") {
exceptionHandler(options, event, context, executeOnException);
} else {
if (defEx) {
options.exception_PC = new ProgramCounter(
options.exception_PC.section,
state,
options.exception_PC.call_stack
);
options.PC = JSON.parse(JSON.stringify(options.exception_PC));
fallbackParser(options, event, context);
} else {
options.PC = JSON.parse(JSON.stringify(options.previous_PC));
delete options.exception_call_stack;
delete options.previous_PC;
delete options.exception_PC;
delete options.exception;
defaultExceptionHandler(options, event, context);
}
}
}
}
/**
* Function to execute on exception. If exception Handler is called this is the callback of that function.
* @param options
* @param event
* @param context
*/
function executeOnException(options, event, context) {
if (options.next_state && options.next_state !== -1) {
context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
] = JSON.parse(JSON.stringify(options.exception_call_stack));
options.PC = JSON.parse(JSON.stringify(options.exception_PC));
delete options.exception_call_stack;
delete options.previous_PC;
delete options.exception_PC;
delete options.exception;
executeNextState(options, event, context);
} else {
handleException(options, event, context);
}
}
/**
* This method executes the next state of the current program counter. This also has a recursive behavior when
* the function's next state is evaluated and then the function checks if the evaluation can be continued and then
* eventually continues the execution.
* @param options
* @param event
* @param context
*/
function executeNextState(options, event, context) {
if (options.next_state) {
options = generateNextState(options);
}
if (options.getCurrentStateData()) {
if (options.google_analytics) {
var google_analytics = require("./google_analytics");
google_analytics.callGoogleAnalytics(context, event, options);
}
if (options.getCurrentStateData().type === ScriptLoader.BOT) {
if (!options.output_messages) {
options.output_messages = [];
}
options.output_messages.push(generateBotOutput(options, event, context));
}
if (canContinueExecution(options, true)) {
if (options.getCurrentStateData().action.action === ScriptLoader.GOTO) {
executeAction(options, event, context, ScriptLoader.GOTO);
} else if (
options.getCurrentStateData().action.action === ScriptLoader.CALL
) {
executeAction(options, event, context, ScriptLoader.CALL);
} else if (
options.getCurrentStateData().action.action === ScriptLoader.DELAY
) {
if (options.apikey || event.apikey) {
options.output_messages.push(TYPING_ON);
context.sendMessage(
options,
event.contextobj,
JSON.stringify(options.output_messages)
);
options.output_messages = [];
setTimeout(function () {
options.next_state = evaluateNextState(options);
options.PC = options.nextProgramCounter();
executeNextState(options, event, context);
}, options.getCurrentStateData().action.delay);
} else {
console.error(
"Delay Events need apikey in the event or in options. Please provide the same else continuing the same flow."
);
options.next_state = evaluateNextState(options);
options.PC = options.nextProgramCounter();
executeNextState(options, event, context);
}
} else {
continueExecution(options, event, context);
}
} else {
if (
options.getCurrentStateData().nextstates.length === 0 &&
options.leafNodeHandler &&
typeof options.leafNodeHandler === "function"
) {
console.log("Found LeafNode Handler.");
options.leafNodeHandler(
options,
context,
event,
stopContinuingExecution
);
} else {
stopContinuingExecution(options, event, context);
}
}
} else {
console.error("NO State found for PC ".concat(JSON.stringify(options.PC)));
}
}
/**
* Generates the next state if next state is not found
* @param options
* @returns {*}
*/
function generateNextState(options) {
if (options.next_state !== -1) {
options.PC = options.nextProgramCounter();
} else {
options.next_state = evaluateNextState(options);
options.PC = options.nextProgramCounter();
}
delete options.next_state;
return options;
}
/**
* After stopping execution it checks whether the state has return or not. If it has return then execute that action
* otherwise stopContinuing Execution.
* @param options
* @param event
* @param context
*/
function stopContinuingExecution(options, event, context) {
if (
options.getCurrentStateData().action.action === ScriptLoader.RETURN ||
options.getCurrentStateData().return
) {
executeAction(options, event, context, ScriptLoader.RETURN);
} else {
finalizeExecution(options, context, event);
}
}
/**
* Generates the bot output. Replaces the placeholder.
* @param options
* @returns {*}
*/
function generateBotOutput(options, event, context) {
if (options.getCurrentStateData().hasPlaceHolder) {
var substitutedValue = substituteVars(options, event, context);
var parsedValue = tryParseJSON(substitutedValue);
if (parsedValue && parsedValue.msgid) {
if (
cached_refmsgid_stateid_map.hasOwnProperty(parsedValue.msgid) &&
options.getCurrentStateData().label !==
cached_refmsgid_stateid_map[parsedValue.msgid].state.label
) {
console.error(
"Same Refmsgid map found for => " +
options.getCurrentStateData().label +
" and at " +
cached_refmsgid_stateid_map[parsedValue.msgid].state.label
);
} else {
cached_refmsgid_stateid_map[parsedValue.msgid] = {
state : options.getCurrentStateData(),
section: options.PC.section
};
}
}
if (!parsedValue && substitutedValue.trim().startsWith("{")) {
console.info(
"The message at state " +
options.getCurrentStateData().label +
" is not "
);
}
return substitutedValue;
} else {
return getRandomMessage(options.getCurrentStateData().output);
}
}
/**
* This function substitutes variables from the equivalent data value.
* @param output
* @param data
* @returns {*}
*/
/* function substituteVars(output, data) {
var matched_vars = output.match(placeholders_regex);
if (matched_vars !== null) {
for (var index = 0; index < matched_vars.length; index++) {
output = output.replace(matched_vars[index], data[matched_vars[index].match(single_component_regex)[2]] ? data[matched_vars[index].match(single_component_regex)[2]] : '');
}
}
return output;
} */
function substituteVars(options, event, context) {
let output = getRandomMessage(options.getCurrentStateData().output);
let matched_vars = output.match(placeholders_regex);
if (matched_vars) {
matched_vars.forEach(element => {
let placeholderName = element.match(single_component_regex)[2];
let replacedValue = "";
if (has.call(options.data, placeholderName)) {
let shouldBeReplacedWith = options.data[placeholderName];
if (
shouldBeReplacedWith instanceof Function ||
typeof shouldBeReplacedWith === "function"
) {
replacedValue = shouldBeReplacedWith(options, event, context);
} else if (
shouldBeReplacedWith instanceof String ||
typeof shouldBeReplacedWith === "string"
) {
replacedValue = shouldBeReplacedWith;
}
}
output = output.split(element).join(replacedValue);
});
}
return output;
}
/**
* This function checks if we can continue the execution.
* @param options
* @param considerActions
* @returns {boolean}
*/
function canContinueExecution(options, considerActions) {
if (
considerActions &&
( options.getCurrentStateData().action.action === ScriptLoader.GOTO ||
options.getCurrentStateData().action.action === ScriptLoader.CALL ||
options.getCurrentStateData().action.action === ScriptLoader.DELAY )
) {
return true;
}
if (
options.getCurrentStateData().type === ScriptLoader.BOT &&
options.fallthrough &&
options.getCurrentStateData().nextstates.length > 0 &&
getStateData(
options.script,
options.PC.section,
options.getCurrentStateData().nextstates[0],
options.error
).type === ScriptLoader.USER
) {
return true;
}
if (options.getCurrentStateData()) {
if (options.getCurrentStateData().nextstates.length > 0) {
if (
getStateData(
options.script,
options.PC.section,
options.getCurrentStateData().nextstates[0],
options.error
).type === ScriptLoader.BOT
) {
return true;
}
}
} else {
console.error(
"Current State is undefined for ",
JSON.stringify(options.PC)
);
}
return false;
}
/**
* Function that is executed whenever a user state is found.
* @param options
* @param event
* @param context
*/
function continueExecution(options, event, context) {
options.next_state = -1;
var handler = getHandler(options);
if (handler && typeof handler === "function") {
handler(options, event, context, executeNextState);
} else if (
options.enableMultiStateJump &&
options.getCurrentStateData().type === ScriptLoader.BOT &&
options.getCurrentStateData().nextstates.length > 0 &&
getStateData(
options.script,
options.PC.section,
options.getCurrentStateData().nextstates[0],
options.error
).type === ScriptLoader.USER
) {
fallbackParser(options, event, context);
} else {
executeNextState(options, event, context);
}
}
/**
*
* @param options
* @param context
* @param event
*/
function finalizeExecution(options, context, event) {
if (options.output_messages) {
options.output_messages = options.output_messages.map(item => {
if (item) return item.split("\\#n").join("\\n");
return "";
});
let newOutputMessages = [];
options.output_messages.forEach(message => {
let parsedMessage = tryParseJSON(message);
if (parsedMessage && Array.isArray(parsedMessage)) {
parsedMessage.forEach(multiMessage =>
newOutputMessages.push(multiMessage)
);
} else {
newOutputMessages.push(message);
}
});
options.output_messages = newOutputMessages;
if (options.restateLastMessage && !options.fromExceptionHandler) {
lastMessages = options.output_messages;
}
}
options = generateFlowNameInStructuredMessage(options, context, event);
if (event.type !== "event") {
if (
( !options.isAuxiliaryFlow && !options.getCurrentStateData().isCommon ) ||
( options.isAuxiliaryFlow &&
options.getCurrentStateData().action.action !== ScriptLoader.RETURN )
) {
context.simpledb.roomleveldata[options.stateIdentifier + ":pc"] =
options.PC;
}
options.success(options.output_messages, options);
} else {
context.simpledb.roomleveldata[options.stateIdentifier + ":pc"] =
options.PC;
context.simpledb.saveData(function (err) {
if (err) {
options.error(err);
} else {
context.sendMessage(
options,
event.contextobj,
JSON.stringify(options.output_messages),
function () {
options.success({
success: true,
message: "success"
},
options
);
}
);
}
});
}
}
/**
* Excutes an action. Actions are of 3 type,
* 1. Call - To call a section. Just like function call.
*
* 2. GOTO - To goto to a specific label within that section. GOTOs are post Type.
* Post type of execution means that if GOTO to state s1 then, all the childrens of the state will get executed.
*
* 3. RETURN - Returns are of two types, implicit and explicit.
* 3.1 Implicit Return - Implicit Returns are when there are no state to go.
* 3.2 Explicit Return - Explicit returns are when it is forcefully returned through return statement.
*
* @param options
* @param event
* @param context
* @param action
*/
function executeAction(options, event, context, action) {
switch (action) {
case ScriptLoader.GOTO:
options.PC = new ProgramCounter(
options.PC.section,
options.getCurrentStateData().action.label,
[]
);
options.fallthrough = false;
options.PC.call_stack = getPathToRoot(options);
if (canContinueExecution(options, true)) {
if (options.getCurrentStateData().action.action === ScriptLoader.GOTO) {
executeAction(options, event, context, ScriptLoader.GOTO);
} else if (
options.getCurrentStateData().action.action === ScriptLoader.CALL
) {
executeAction(options, event, context, ScriptLoader.CALL);
} else {
continueExecution(options, event, context);
}
} else {
finalizeExecution(options, context, event);
}
break;
case ScriptLoader.CALL:
if (
!context.simpledb.roomleveldata[options.stateIdentifier + ":call_stack"]
) {
context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
] = [];
}
options.fallthrough = false;
let callSection = options.getCurrentStateData().action.section;
//Handling For Common State Calls
if (options.getCurrentStateData().isCommon) {
while (options.PC.call_stack.length > 0) {
options.PC.state = options.PC.call_stack.pop();
if (!options.getCurrentStateData().isCommon) {
break;
}
}
}
context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
].push(new ThreadData(options.PC, options.data));
options.PC = new ProgramCounter(
callSection,
getStartState(
options.script,
callSection,
options.error
),
[]
);
if (options.removeAction) {
let updatedStateData = options.getCurrentStateData();
updatedStateData.action = options.previousAction;
options.updateCurrentStateData(updatedStateData);
delete options.previousAction;
options.removeAction = false;
delete options.next_state;
}
executeNextState(options, event, context);
break;
case ScriptLoader.RETURN:
if (
options.isAuxiliaryFlow &&
options.getCurrentStateData().action.action === ScriptLoader.RETURN
) {
delete context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
];
delete context.simpledb.roomleveldata[options.stateIdentifier + ":pc"];
finalizeExecution(options, context, event);
} else {
options.fallthrough = false;
if (
options.getCurrentStateData().action.action === ScriptLoader.RETURN
) {
if (
context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
] &&
context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
].length > 0
) {
var threadData = context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
].pop();
options.data = threadData.data;
options.PC = threadData.pc;
if (canContinueExecution(options, false)) {
continueExecution(options, event, context);
} else {
if (options.getCurrentStateData().return) {
executeAction(options, event, context, ScriptLoader.RETURN);
} else {
finalizeExecution(options, context, event);
}
}
} else {
for (
var index = options.PC.call_stack.length - 1; index >= 0; index--
) {
var stateData = getStateData(
options.script,
options.PC.section,
options.PC.call_stack[index],
options.error
);
if (
stateData.type === ScriptLoader.BOT &&
( stateData.action.action === ScriptLoader.DONE ||
stateData.action.action === ScriptLoader.DEFEX )
) {
options.next_state = options.PC.call_stack[index];
options.PC = options.nextProgramCounter();
options.PC.call_stack = getPathToRoot(options);
break;
}
}
finalizeExecution(options, context, event);
}
} else {
if (options.PC.call_stack.length > 0) {
for (
var index = options.PC.call_stack.length - 1; index >= 0; index--
) {
var stateData = getStateData(
options.script,
options.PC.section,
options.PC.call_stack[index],
options.error
);
if (
stateData.type === ScriptLoader.BOT &&
( stateData.action.action === ScriptLoader.DONE ||
stateData.action.action === ScriptLoader.DEFEX )
) {
options.next_state = options.PC.call_stack[index];
options.PC = options.nextProgramCounter();
options.PC.call_stack = getPathToRoot(options);
break;
}
}
finalizeExecution(options, context, event);
} else {
if (
context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
] &&
context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
].length > 0
) {
var threadData = context.simpledb.roomleveldata[
options.stateIdentifier + ":call_stack"
].pop();
options.data = threadData.data;
options.PC = threadData.pc;
if (canContinueExecution(options, false)) {
continueExecution(options, event, context);
} else {
if (options.getCurrentStateData().return) {
executeAction(options, event, context, ScriptLoader.RETURN);
} else {
finalizeExecution(options, context, event);
}
}
} else {
finalizeExecution(options, context, event);
}
}
}
}
break;
}
}
/**
*
* @param options
* @returns {*}
*/
function evaluateNextState(options) {
if (options.getCurrentStateData().nextstates.length === 1) {
return options.getCurrentStateData().nextstates[0];
} else if (options.getCurrentStateData().nextstates.length > 1) {
if (options.getCurrentStateData().type === ScriptLoader.BOT) {
if (
options.resolverStrategy &&
typeof options.resolverStrategy === "string"
) {
switch (options.resolverStrategy) {
case "FIRST":
return options.getCurrentStateData().nextstates[0];
case "RANDOM":
return options.getCurrentStateData().nextstates[
Math.floor(
Math.random() * options.getCurrentStateData().nextstates.length
)
];
default:
sendMultipleStateError(options);
break;
}
} else {
sendMultipleStateError(options);
}
}
} else {
sendMultipleStateError(options);
}
}
/**
* Send Multiple State Error.
* @param options
*/
function sendMultipleStateError(options) {
var error = new Error(
"Multiple States Found to go without a resolver. Please Provide appropiate resolver for the state. Current Location of Program Counter ".concat(
"section: ",
options.PC.section,
" state:",
options.PC.state
)
);
if (options.error) {
options.error(error);
} else {
console.error(
new Error(
"Multiple States Found to go without a resolver. Please Provide appropiate resolver for the state. Current Location of Program Counter ".concat(
"section: ",
options.PC.section,
" state:",
options.PC.state
)
)
);
}
}
/**
* Constructs Program Counter Objects.
* @param section
* @param state
* @param call_stack
* @constructor
*/
function ProgramCounter(section, state, call_stack) {
this.section = section;
this.state = state;
this.call_stack = JSON.parse(JSON.stringify(call_stack));
}
/**
* Gives the next Program Counter.
* @returns {ProgramCounter}
*/
function nextProgramCounter() {
if (this.PC && !this.getCurrentStateData().isCommon) {
this.PC.call_stack.push(this.PC.state);
}
return new ProgramCounter(
this.PC.section,
this.next_state,
this.PC ? JSON.parse(JSON.stringify(this.PC.call_stack)) : []
);
}
/**
* Gets the state's data that is passed in the argument list.
* @param script
* @param section
* @param state
* @returns {*}
*/
function getStateData(script, section, state, onError) {
if (script.hasOwnProperty(section) && script[section].hasOwnProperty(state)) {
return script[section][state];
} else {
var fileName = section.split(".")[0];
if (
script.hasOwnProperty(fileName.concat(".", ScriptLoader.COMMON)) &&
script[fileName.concat(".", ScriptLoader.COMMON)].hasOwnProperty(state)
) {
return script[fileName.concat(".", ScriptLoader.COMMON)][state];
} else if (
script.hasOwnProperty(
fileName.concat(".", ScriptLoader.PERSISTENT_MENU)
) &&
script[fileName.concat(".", ScriptLoader.PERSISTENT_MENU)].hasOwnProperty(
state
)
) {
return script[fileName.concat(".", ScriptLoader.PERSISTENT_MENU)][state];
} else {
onError(
"In the script section " +
section +
" and state " +
state +
" is not present."
);
}
}
}
function setStateData(script, section, state, onError, updatedValue) {
if (script.hasOwnProperty(section) && script[section].hasOwnProperty(state)) {
script[section][state] = updatedValue;
} else {
var fileName = section.split(".")[0];
if (
script.hasOwnProperty(fileName.concat(".", ScriptLoader.COMMON)) &&
script[fileName.concat(".", ScriptLoader.COMMON)].hasOwnProperty(state)
) {
script[fileName.concat(".", ScriptLoader.COMMON)][state] = updatedValue;
} else if (
script.hasOwnProperty(
fileName.concat(".", ScriptLoader.PERSISTENT_MENU)
) &&
script[fileName.concat(".", ScriptLoader.PERSISTENT_MENU)].hasOwnProperty(
state
)
) {
script[fileName.concat(".", ScriptLoader.PERSISTENT_MENU)][
state
] = updatedValue;
} else {
onError(
"In the script section " +
section +
" and state " +
state +
" is not present."
);
}
}
}
function getStartState(script, section, onError) {
if (script[section]) {
for (var state in script[section]) {
if (script[section].hasOwnProperty(state)) {
if (script[section][state].isStartState) {
return state;
}
}
}
}
onError(section + " not found within the script. Please review it once.");
}
/**
* Implementation of the fallback parser. The fallback parser eventually calls the nlp and entity parser. The first
* step is to analyze if the parser is getting an exact match. In case of an exact match that state is executed as the next
* state if an exact match is not found then nlp parser is triggered. Also entity check is done in a separate call as
* of now. In future implementations there will be only one call to the nlp engine for both. There are also provision for
* multiple state jump. In case of multiple state jump the code is meant to extract the matched part of the speech from the
* user message and process the remaining with the remaining state.
* @param options
* @param event
* @param context
*/
function fallbackParser(options, event, context) {
var current_state_data = getStateData(
options.script,
options.PC.section,
options.PC.state,
options.error
);
var intent_state_map = [];
var intents = [];
var utterence = event.message;
var next_states = current_state_data.nextstates;
var matchFound = false;
if (options.enableMultiStateJump && options.fallthrough) {
options.lastRememberedState = options.PC.state;
options.lastRememberedOutputMessages = options.output_messages;
options.output_messages = [];
}
//Returning if number of next state is 1 and there is no variation.
if (next_states.length === 1) {
let hasSmallTalkOffAction =
options.getCurrentStateData().action.action ===
ScriptLoader.SMALLTALK_OFF;
let hasCommonSection = options.hasSection(ScriptLoader.COMMON);
if (
hasSmallTalkOffAction ||
( !hasSmallTalkOffAction && !hasCommonSection )
) {
var next_state_data = getStateData(
options.script,
options.PC.section,
next_states[0],
options.error
);
if (!next_state_data.hasPlaceHolder) {
if (next_state_data) {
if (next_state_data.input && !next_state_data.eventType)