UNPKG

bot-script

Version:

Scripting tool to Write Bot's Workflow

1,370 lines (1,327 loc) 80.8 kB
/** * 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)