UNPKG

botpress

Version:

The world's first CMS for bots. Easily create, manage and extend chatbots.

906 lines (742 loc) 30.5 kB
'use strict'; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _lodash = require('lodash'); var _lodash2 = _interopRequireDefault(_lodash); var _mustache = require('mustache'); var _mustache2 = _interopRequireDefault(_mustache); var _bluebird = require('bluebird'); var _bluebird2 = _interopRequireDefault(_bluebird); var _vm = require('vm2'); var _mware = require('mware'); var _mware2 = _interopRequireDefault(_mware); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new _bluebird2.default(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return _bluebird2.default.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } const callSubflowRegex = /(.+\.flow\.json)\s?@?\s?(.+)?/i; // e.g. './login.flow.json' or './login.flow.json @ username' const MAX_STACK_SIZE = 100; const TRUEISH_WORDS = { true: true, always: true, yes: true }; const compileExp = _lodash2.default.memoize(expr => new _vm.VMScript(expr)); /** The Dialog Engine (or Dialog Manager) is the component that handles the flow logic. It it responsible for executing flows, including executing the actions and flowing to the nodes, redirections etc. @namespace DialogEngine @example bp.dialogEngine.processMessage(...) */ class DialogEngine { constructor({ flowProvider, stateManager, options, logger }) { this.onError = fn => this.errorHandlers.push(fn); this.logger = logger; this.flowProvider = flowProvider; this.stateManager = stateManager; this._flowsLoadingPromise = null; this.flows = []; this.defaultFlow = _lodash2.default.get(options, 'defaultFlow') || 'main.flow.json'; this.outputProcessors = []; this.errorHandlers = []; this.actions = {}; this.actionMetadataProviders = []; this.vm = new _vm.VM({ timeout: 5000 }); /** * @typedef {Function} DialogEngine~DialogMiddleware * @param {object} ctx A mutable context object * @param {function} next Call this to continue processing */ /** * Middleware triggered before a new session is started. * > **Note:** This middleware allows you to alter `ctx.flowName` to change * > which flow will be selected to start the conversation. * @function DialogEngine#onBeforeCreated * @param {DialogEngine~DialogMiddleware} middleware * @example bp.dialogEngine.onBeforeCreated((ctx, next) => { ctx.flowName = 'example.flow.json' next() }) */ this.onBeforeCreated = (0, _mware2.default)(); /** * Middleware triggered **after** a new session is started. * `ctx` is not mutable. * @function DialogEngine#onAfterCreated * @param {DialogEngine~DialogMiddleware} middleware * @example bp.dialogEngine.onAfterCreated((ctx, next) => { // Do something here next() }) */ this.onAfterCreated = (0, _mware2.default)(); /** * Middleware triggered **before** a conversation is ended for any reason. * `ctx` is not mutable. * @function DialogEngine#onBeforeEnd * @param {DialogEngine~DialogMiddleware} middleware * @example bp.dialogEngine.onBeforeEnd((ctx, next) => { // Do something here next() }) */ this.onBeforeEnd = (0, _mware2.default)(); /** * Middleware triggered **before** a node is entered (before the `onEnter` execution). * > **⚠️ Warn:** It is **not** recommended to mutate `ctx.node` for now, it might break in a future version of Botpress. * @function DialogEngine#onBeforeNodeEnter * @param {DialogEngine~DialogMiddleware} middleware * @example bp.dialogEngine.onBeforeNodeEnter((ctx, next) => { // Do something here next() }) */ this.onBeforeNodeEnter = (0, _mware2.default)(); /** * Middleware triggered **before** a conversation/session times out. * > **Note:** You can't prevent it from timing out at this point. * > You also can't change the timeout behavior/location at this time. * @function DialogEngine#onBeforeSessionTimeout * @param {DialogEngine~DialogMiddleware} middleware * @example bp.dialogEngine.onBeforeSessionTimeout((ctx, next) => { // Do something here next() }) */ this.onBeforeSessionTimeout = (0, _mware2.default)(); flowProvider.on('flowsChanged', () => { this._flowsLoadingPromise = null; this.flows = []; }); } _processMessage(stateId, event) { var _this = this; return _asyncToGenerator(function* () { yield _this.loadFlows(); const context = yield _this._getOrCreateContext(stateId); let state = yield _this.stateManager.getState(stateId); if (event.type === 'bp_dialog_timeout') { state = yield _this._processTimeout(stateId, state, context, event); if (state != null) { yield _this.stateManager.setState(stateId, state); } return state; } _this._trace('<~', 'RECV', `"${(event.text || '').substr(0, 20)}"`, context, state); if (!context.currentFlow) { throw new Error('Expected currentFlow to be defined for stateId=' + stateId); } const catchAllOnReceive = _lodash2.default.get(context, 'currentFlow.catchAll.onReceive'); if (catchAllOnReceive) { _this._trace('!!', 'KALL', '', context, state); state = yield _this._processInstructions(catchAllOnReceive, state, event, context); } // If there's a 'next' defined in catchAll, this will try to match any condition and if it is matched it // will run the node defined in the next instead of the current context node const catchAllNext = _lodash2.default.get(context, 'currentFlow.catchAll.next'); if (catchAllNext) { _this._trace('..', 'KALL', '', context, state); for (const transition of catchAllNext) { if (_this._evaluateCondition(transition.condition, state, event)) { return _this._processNode(stateId, state, context, transition.node, event); } } _this._trace('?X', 'KALL', '', context, state); } state = yield _this._processNode(stateId, state, context, context.node, event); if (state != null) { yield _this.stateManager.setState(stateId, state); } return state; })(); } /** * Process a new incoming message from the user. * This will execute and run the flow until the flow ends or gets paused by user input * @param {string} stateId The Id of the state. * This is usually unique per user/group/channel, depending on the platform. * @param {BPIncomingEvent} event The incoming event (message) * @return {Promise<State>} Returns a promise that resolves with the new state * when the flow is done processing */ processMessage(stateId, event) { var _this2 = this; return _asyncToGenerator(function* () { try { yield _this2._processMessage(stateId, event); } catch (e) { _this2.errorHandlers.forEach(function (errorHandler) { return errorHandler(e); }); } })(); } /** * Make the stateId jump to the specified flow and node * regardless of if there was already an active flow or not * in execution. If there was already an active flow executing, * this will override it. Note that by default, the current state * will be preserved; if you wish to reset the state as well, * set `resetState` to `true`. * Note that this will not continue processing, i.e. the user must send a message or * you should call {@link BotEngine#processMessage} manually to continue execution. * @example * // inside a bp.hear (...) * await bp.dialogEngine.jumpTo(stateId, 'main.flow.json') * await bp.dialogEngine.processMessage(stateId, event) // Continue processing * @param {string} stateId The stateId of the user/channel/group to make jump. * @param {string} flowName The name of the flow, e.g. `main.flow.json` * @param {string} [nodeName=null] The name of the node to jump to. Defaults to the flow's entry point. * @param {boolean} [options.resetState=false] Whether or not the state should be reset */ jumpTo(stateId, flowName, nodeName = null, options) { var _this3 = this; return _asyncToGenerator(function* () { options = _extends({ resetState: false }, options); yield _this3.loadFlows(); const flow = yield _this3._findFlow(flowName, true); if (nodeName) { // We're just calling for throwing if doesn't exist DialogEngine._findNode(flow, nodeName, true); } yield _this3._setContext(stateId, { currentFlow: flow, node: nodeName || flow.startNode, hasJumped: true, flowStack: [{ flow: flow.name, node: nodeName || flow.startNode }] }); if (options.resetState) { yield _this3.stateManager.setState(stateId, {}); } })(); } /** * Get the current flow and node for a specific stateId * @param {string} stateId * @return {{ flow: string, node: string }} Returns the current flow and node */ getCurrentPosition(stateId) { var _this4 = this; return _asyncToGenerator(function* () { const context = yield _this4._getContext(stateId); if (context) { return { flow: context.currentFlow && context.currentFlow.name, node: context.node }; } return { flow: null, node: null }; })(); } /** * Ends the flow for a specific stateId if there's an active flow, * otherwise does nothing. * @param {string} stateId [description] */ endFlow(stateId) { var _this5 = this; return _asyncToGenerator(function* () { return _this5._endFlow(stateId); })(); } loadFlows() { if (!this._flowsLoadingPromise) { this._trace('**', 'LOAD', ''); this._flowsLoadingPromise = this.flowProvider.loadAll().then(flows => { this.flows = flows; }); } return this._flowsLoadingPromise; } getFlows() { var _this6 = this; return _asyncToGenerator(function* () { yield _this6.loadFlows(); return _this6.flows; })(); } /** * Registers a new output processor (there can be many, which all get triggered on output). * @param {OutpoutProcessor} processor - Is an object with {id, send} * @param {string} processor.id - The unique id of the processor * @param {Function} processor.send - The `send` function of the processor * @returns {void} * @private */ registerOutputProcessor(processor) { if (_lodash2.default.isNil(processor) || !_lodash2.default.isFunction(processor.send) || !_lodash2.default.isString(processor.id)) { throw new Error('Invalid processor. Processor must have a function `send` defined and a valid `id`'); } // For now we only ever support a single output processor // We might want many output processors in the future, for example to hook up a debugger or a test suite this.outputProcessors = [processor]; } /** * @typedef {object} DialogEngine~ActionMetadata * @var {string} title * @var {string} description * @var {boolean} required * @var {string} default * @var {string} type * @private */ /** * A metadata provider returns metadata for an action or * returns null, in which case other providers will be called * @typedef {Function} DialogEngine~MetadataProvider * @param {string} action The name of the action * @returns {DialogEngine~ActionMetadata} * @private */ /** * Adds a new provider of function metadata * @param {DialogEngine~MetadataProvider} provider * @returns {void} * @private */ registerActionMetadataProvider(provider) { if (!_lodash2.default.isFunction(provider)) { throw new Error('Expected the function metadata provider to be a function'); } if (!this.actionMetadataProviders.includes(provider)) { this.actionMetadataProviders.push(provider); } } /** * Introduce new actions to the Flows that they can call. * @param {Object} fnMap * @param {bool} [overwrite=false] - Whether or not it should overwrite existing actions with the same name. * Note that if overwrite is false, an error will be thrown on conflict. * @returns {Promise.<void>} */ registerActions(fnMap, overwrite = false) { var _this7 = this; return _asyncToGenerator(function* () { _lodash2.default.keys(fnMap).forEach(function (name) { if (_this7.actions[name] && !overwrite) { throw new Error(`There is already a function named "${name}" registered`); } let handler = fnMap[name]; let metadata = null; if (!_lodash2.default.isFunction(handler)) { if (!_lodash2.default.isObject(handler) || !_lodash2.default.isFunction(handler.handler)) { throw new Error(`Expected function "${name}" to be a function or an object with a 'hander' function`); } handler = handler.handler; metadata = _extends({}, fnMap[name], { name, handler: null }); } for (const provider of _this7.actionMetadataProviders) { const extra = provider(name); if (extra) { metadata = _extends({}, extra, metadata); break; } } _this7.actions[name] = { name, metadata, fn: handler // Make the method available in the conditions evaluation context };_this7.vm.freeze(handler, name); }); })(); } /** * @deprecated Use registerActions() instead */ registerFunctions(fnMap, overwrite = false) { return this.registerActions(fnMap, overwrite); } /** * Returns all the available actions along with their metadata * @private */ getAvailableActions() { return _lodash2.default.values(this.actions).filter(x => !String(x.name).startsWith('__')).map(x => _extends({}, x, { fn: null })); } _processTimeout(stateId, userState, context, event) { var _this8 = this; return _asyncToGenerator(function* () { const beforeCtx = { stateId }; yield _bluebird2.default.fromCallback(function (callback) { return _this8.onBeforeSessionTimeout.run(beforeCtx, callback); }); const currentNodeTimeout = _lodash2.default.get(DialogEngine._findNode(context.currentFlow, context.node), 'timeoutNode'); const currentFlowTimeout = _lodash2.default.get(context, 'currentFlow.timeoutNode'); const fallbackTimeoutNode = DialogEngine._findNode(context.currentFlow, 'timeout'); const fallbackTimeoutFlow = yield _this8._findFlow('timeout.flow.json'); if (currentNodeTimeout) { _this8._trace('<>', 'SNDE', '', context); userState = yield _this8._processNode(stateId, userState, context, currentNodeTimeout, event); } else if (currentFlowTimeout) { _this8._trace('<>', 'SFLW', '', context); userState = yield _this8._processNode(stateId, userState, context, currentFlowTimeout, event); } else if (fallbackTimeoutNode) { _this8._trace('<>', 'DNDE', '', context); userState = yield _this8._processNode(stateId, userState, context, fallbackTimeoutNode.name, event); } else if (fallbackTimeoutFlow) { _this8._trace('<>', 'DFLW', '', context); userState = yield _this8._processNode(stateId, userState, context, fallbackTimeoutFlow.name, event); } else { _this8._trace('<>', 'NTHG', '', context); userState = yield _this8._endFlow(stateId); } return userState; })(); } _processNode(stateId, userState, context, nodeName, event) { var _this9 = this; return _asyncToGenerator(function* () { let switchedFlow = false; let switchedNode = context.hasJumped; context = _extends({}, context, { hasJumped: false }); const originalFlow = context.currentFlow.name; if (callSubflowRegex.test(nodeName)) { _this9._trace('>>', 'FLOW', `"${nodeName}"`, context, null); context = yield _this9._gotoSubflow(nodeName, context); switchedFlow = true; } else if (nodeName && nodeName[0] === '#') { // e.g. '#success' _this9._trace('<<', 'FLOW', `"${nodeName}"`, context, null); context = yield _this9._gotoPreviousFlow(nodeName, context); switchedFlow = true; } else if (context.node !== nodeName) { _this9._trace('>>', 'FLOW', `"${nodeName}"`); switchedNode = true; context = _extends({}, context, { node: nodeName }); } else if (context.node == null) { // We just created the context switchedNode = true; context = _extends({}, context, { node: context.currentFlow.startNode }); } const node = DialogEngine._findNode(context.currentFlow, context.node); if (!node || !node.name) { userState = yield _this9._endFlow(stateId); return userState; // TODO Trace error // throw new Error(`Could not find node "${context.node}" in flow "${context.currentFlow.name}"`) } if (switchedFlow || switchedNode) { const flowStack = context.flowStack.concat({ flow: context.currentFlow.name, node: context.node }); // Flattens the stack to only include flow jumps, not node jumps context = _extends({}, context, { // Flattens the stack to only include flow jumps, not node jumps flowStack: flowStack.filter(function (el, i) { return i === flowStack.length - 1 || flowStack[i + 1].flow !== el.flow; }) }); if (context.flowStack.length >= MAX_STACK_SIZE) { throw new Error(`Exceeded maximum flow stack size (${MAX_STACK_SIZE}). This might be due to an unexpected infinite loop in your flows. Current flow: ${context.currentFlow.name} Current node: ${context.node}`); } yield _this9._setContext(stateId, context); const beforeCtx = { stateId, node }; yield _bluebird2.default.fromCallback(function (callback) { return _this9.onBeforeNodeEnter.run(beforeCtx, callback); }); if (node.onEnter) { _this9._trace('!!', 'ENTR', '', context, userState); userState = yield _this9._processInstructions(node.onEnter, userState, event, context); } if (!node.onReceive) { _this9._trace('..', 'NOWT', '', context, userState); if (node.type === 'skill-call' && originalFlow !== node.flow) { userState = yield _this9._processNode(stateId, userState, context, node.flow, event); } else { userState = yield _this9._transitionToNextNodes(node, context, userState, stateId, event); } } } else { // i.e. we were already on that node before we received the message if (node.onReceive) { _this9._trace('!!', 'RECV', '', context, userState); userState = yield _this9._processInstructions(node.onReceive, userState, event, context); } _this9._trace('..', 'RECV', '', context, userState); if (node.type === 'skill-call' && originalFlow !== node.flow) { userState = yield _this9._processNode(stateId, userState, context, node.flow, event); } else { userState = yield _this9._transitionToNextNodes(node, context, userState, stateId, event); } } return userState; })(); } _transitionToNextNodes(node, context, userState, stateId, event) { var _this10 = this; return _asyncToGenerator(function* () { const nextNodes = node.next || []; for (const nextNode of nextNodes) { if (_this10._evaluateCondition(nextNode.condition, userState, event)) { _this10._trace('??', 'MTCH', `cond = "${nextNode.condition}"`, context); if (/^end$/i.test(nextNode.node)) { // Node "END" or "end" ends the flow (reserved keyword) return _this10._endFlow(stateId); } else { return _this10._processNode(stateId, userState, context, nextNode.node, event); } } } if (!nextNodes.length) { // You reach this if there were no next nodes, in which case we end the flow return _this10._endFlow(stateId); } return userState; })(); } _endFlow(stateId) { var _this11 = this; return _asyncToGenerator(function* () { const beforeCtx = { stateId }; yield _bluebird2.default.fromCallback(function (callback) { return _this11.onBeforeEnd.run(beforeCtx, callback); }); _this11._trace('--', 'ENDF', '', null, null); yield _this11.stateManager.deleteState(stateId, ['context']); return null; })(); } _getOrCreateContext(stateId) { var _this12 = this; return _asyncToGenerator(function* () { let state = yield _this12._getContext(stateId); if (state && state.currentFlow) { return state; } const beforeCtx = { stateId, flowName: _this12.defaultFlow }; yield _bluebird2.default.fromCallback(function (callback) { return _this12.onBeforeCreated.run(beforeCtx, callback); }); const flow = yield _this12._findFlow(beforeCtx.flowName, true); if (!flow) { throw new Error(`Could not find the default flow "${_this12.defaultFlow}"`); } state = { currentFlow: flow, flowStack: [{ flow: flow.name, node: flow.startNode }] }; yield _this12._setContext(stateId, state); yield _bluebird2.default.fromCallback(function (callback) { return _this12.onAfterCreated.run(_extends({}, beforeCtx), callback); }); return state; })(); } _getContext(stateId) { return this.stateManager.getState(stateId + '___context'); } _setContext(stateId, state) { return this.stateManager.setState(stateId + '___context', state); } _gotoSubflow(nodeName, context) { var _this13 = this; return _asyncToGenerator(function* () { const [, subflow, subflowNode] = nodeName.match(callSubflowRegex); const flow = yield _this13._findFlow(subflow, true); return _extends({}, context, { currentFlow: flow, node: subflowNode || flow.startNode }); })(); } _gotoPreviousFlow(nodeName, context) { var _this14 = this; return _asyncToGenerator(function* () { if (!context.flowStack) { context = _extends({}, context, { flowStack: [] }); } else { const flowStack = [...context.flowStack]; const currentFlow = context.currentFlow.name; while (flowStack[flowStack.length - 1].flow === currentFlow) { flowStack.pop(); } context = _extends({}, context, { flowStack }); } if (context.flowStack.length < 1) { _this14._trace('Flow tried to go back to previous flow but there was none. Exiting flow.', context, null); // TODO END FLOW return context; } let { flow, node } = _lodash2.default.last(context.flowStack); if (nodeName !== '#') { node = nodeName.substr(1); } return _extends({}, context, { currentFlow: yield _this14._findFlow(flow, true), node }); })(); } _processInstructions(instructions, userState, event, context) { var _this15 = this; return _asyncToGenerator(function* () { if (!_lodash2.default.isArray(instructions)) { instructions = [instructions]; } yield _bluebird2.default.mapSeries(instructions, (() => { var _ref = _asyncToGenerator(function* (instruction) { if (!_lodash2.default.isString(instruction) || !instruction.startsWith('say ')) { userState = yield _this15._invokeAction(instruction, userState, event, context); return; } const chunks = instruction.split(' '); if (chunks.length < 2) { _this15.trace('ERROR Invalid text instruction. Expected an instruction along "say #text Something"'); return; } yield _this15._dispatchOutput({ type: chunks[1], // e.g. "#text" or "#!trivia-12342" value: chunks.slice(2).join(' ') // e.g. Any additional parameter provided to the template }, userState, event, context); }); return function (_x) { return _ref.apply(this, arguments); }; })()); return userState; })(); } _dispatchOutput(output, userState, event, context) { var _this16 = this; return _asyncToGenerator(function* () { const msg = String(output.type + (output.value || '')).substr(0, 20); _this16._trace('~>', 'SEND', `"${msg}"`); return _bluebird2.default.map(_this16.outputProcessors, function (processor) { return processor.send({ message: output, state: userState, originalEvent: event, flowContext: context }); }); })(); } _invokeAction(instruction, userState, event, context) { var _this17 = this; return _asyncToGenerator(function* () { let name = null; let args = {}; if (_lodash2.default.isString(instruction)) { if (instruction.includes(' ')) { const chunks = instruction.split(' '); const argsStr = _lodash2.default.tail(chunks).join(' '); name = _lodash2.default.first(chunks); try { args = JSON.parse(argsStr); const actionCtx = { state: userState, s: userState, event: event, e: event }; args = _lodash2.default.mapValues(args, function (value) { if (_lodash2.default.isString(value) && value.includes('{{')) { try { return _mustache2.default.render(value, actionCtx); } catch (err) { _this17.logger.error(`Error rendering Mustache string ${value}: ${err.message}`); return value; } } return value; }); } catch (err) { throw new Error('ERROR function has invalid arguments (not a valid JSON string): ' + argsStr); } } else { name = instruction; } } else { _this17._trace(`ERROR function is not a valid string`); } if (!_this17.actions[name]) { _this17._trace(`ERROR function "${name}" not found`, context, userState); } else { try { _this17._trace('!!', 'EXEC', `func "${name}"`, context, userState); const ret = yield _this17.actions[name].fn(Object.freeze(userState), event, args || {}); if (ret && _lodash2.default.isObject(ret)) { if (Object.isFrozen(ret)) { _this17._trace(`ERROR function "${name}" returned the original (frozen) state. You should clone the state (see 'Object.assign()') instead of returning the original state.`, context, userState); } else { _this17._trace('!!', 'SSET', '', context); return ret; } } } catch (err) { throw new Error(`ERROR function "${name}" thrown an error: ${err && err.message}`); } } return userState; })(); } _evaluateCondition(condition, userState, event) { if (TRUEISH_WORDS[condition] || condition === '') { return true; } const vm = this.vm; vm.freeze(userState, 's'); vm.freeze(userState, 'state'); vm.freeze(event, 'event'); vm.freeze(event, 'e'); try { return !!vm.run(compileExp(condition)); } catch (err) { throw new Error(`ERROR evaluating condition "${condition}": ${err.message}`); } } static _findNode(flow, nodeName, throwIfNotFound = false) { if (throwIfNotFound && !flow) { throw new Error(`Could not find node ${nodeName} because the flow was not defined (null)`); } const node = _lodash2.default.find(flow.nodes, { name: nodeName }); if (throwIfNotFound && !node) { throw new Error(`Could not find node "${nodeName}" in flow "${flow.name}"`); } return node; } _findFlow(flowName, throwIfNotFound = false) { var _this18 = this; return _asyncToGenerator(function* () { const flows = yield _this18.getFlows(); const flow = _lodash2.default.find(flows, { name: flowName }); if (throwIfNotFound && !flow) { throw new Error(`Could not find flow "${flowName}"`); } return flow; })(); } log(message, context) { const flow = _lodash2.default.get(context, 'currentFlow.name', 'NONE').replace(/\.flow\.json$/i, ''); const node = context && context.node || 'NONE'; const msg = `Dialog: [${flow} – ${node}]\t${message}`; this.logger.debug(msg); } /** * Dialog: [flow] (node) \t OP RESN "Description" * @param message * @param context * @param state * @private */ _trace(operation, reason, message, context) { if (this.logger.level !== 'debug') { // don't do string formatting if we're not going to log it anyway return; } let flow = _lodash2.default.get(context, 'currentFlow.name', ' N/A ').replace(/\.flow\.json/i, ''); let node = context && context.node || ' N/A '; flow = flow.length > 13 ? flow.substr(0, 13) + '&' : flow; node = node.length > 13 ? node.substr(0, 13) + '&' : node; const spc = _lodash2.default.repeat(' ', 30 - flow.length - node.length); const msg = `Dialog: [${flow}] (${node}) ${spc} ${operation} ${reason} \t ${message}`; this.logger.debug(msg); } } module.exports = DialogEngine; //# sourceMappingURL=engine.js.map