botium-core
Version:
The Selenium for Chatbots
653 lines (604 loc) • 28.3 kB
JavaScript
const LogicHookUtils = require('./logichook/LogicHookUtils')
const util = require('util')
const fs = require('fs')
const path = require('path')
const glob = require('glob')
const _ = require('lodash')
require('promise.allsettled').shim()
const debug = require('debug')('botium-ScriptingProvider')
const Constants = require('./Constants')
const Capabilities = require('../Capabilities')
const { Convo } = require('./Convo')
const ScriptingMemory = require('./ScriptingMemory')
const { BotiumError, botiumErrorFromList } = require('./BotiumError')
const globPattern = '**/+(*.convo.txt|*.utterances.txt|*.pconvo.txt|*.scriptingmemory.txt|*.xlsx|*.convo.csv|*.pconvo.csv)'
const p = (fn) => new Promise((resolve, reject) => {
try {
resolve(fn())
} catch (err) {
reject(err)
}
})
module.exports = class ScriptingProvider {
constructor (caps = {}) {
this.caps = caps
this.compilers = {}
this.convos = []
this.utterances = {}
this.matchFn = null
this.asserters = {}
this.globalAsserter = {}
this.logicHooks = {}
this.globalLogicHook = {}
this.userInputs = {}
this.partialConvos = {}
this.scriptingMemories = []
this.scriptingEvents = {
onConvoBegin: ({ convo, convoStep, scriptingMemory, ...rest }) => {
return this._createLogicHookPromises({ hookType: 'onConvoBegin', logicHooks: (convo.beginLogicHook || []), convo, convoStep, scriptingMemory, ...rest })
},
onConvoEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
return this._createLogicHookPromises({ hookType: 'onConvoEnd', logicHooks: (convo.endLogicHook || []), convo, convoStep, scriptingMemory, ...rest })
},
onMeStart: ({ convo, convoStep, scriptingMemory, ...rest }) => {
return this._createLogicHookPromises({ hookType: 'onMeStart', logicHooks: (convoStep.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
},
onMeEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
return this._createLogicHookPromises({ hookType: 'onMeEnd', logicHooks: (convoStep.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
},
onBotStart: ({ convo, convoStep, scriptingMemory, ...rest }) => {
return this._createLogicHookPromises({ hookType: 'onBotStart', logicHooks: (convoStep.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
},
onBotEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
return this._createLogicHookPromises({ hookType: 'onBotEnd', logicHooks: (convoStep.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
},
assertConvoBegin: ({ convo, convoStep, scriptingMemory, ...rest }) => {
return this._createAsserterPromises({ asserterType: 'assertConvoBegin', asserters: (convo.beginAsserter || []), convo, convoStep, scriptingMemory, ...rest })
},
assertConvoStep: ({ convo, convoStep, scriptingMemory, ...rest }) => {
return this._createAsserterPromises({ asserterType: 'assertConvoStep', asserters: (convoStep.asserters || []), convo, convoStep, scriptingMemory, ...rest })
},
assertConvoEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
return this._createAsserterPromises({ asserterType: 'assertConvoEnd', asserters: (convo.endAsserter || []), convo, convoStep, scriptingMemory, ...rest })
},
setUserInput: ({ convo, convoStep, scriptingMemory, ...rest }) => {
return this._createUserInputPromises({ convo, convoStep, scriptingMemory, ...rest })
},
resolveUtterance: ({ utterance }) => {
if (this.utterances[utterance]) {
return this.utterances[utterance].utterances
} else {
return [utterance]
}
},
assertBotResponse: (botresponse, tomatch, stepTag, meMsg) => {
if (!_.isArray(tomatch)) {
tomatch = [tomatch]
}
debug(`assertBotResponse ${stepTag} ${meMsg ? `(${meMsg}) ` : ''}BOT: ${botresponse} = ${tomatch} ...`)
const found = _.find(tomatch, (utt) => {
if (_.isString(botresponse)) {
return this.matchFn(botresponse, utt)
} else {
return botresponse === utt
}
})
if (found === undefined) {
throw new BotiumError(
`${stepTag}: Expected bot response ${meMsg ? `(on ${meMsg}) ` : ''}"${botresponse}" to match one of "${tomatch}"`,
{
type: 'asserter',
source: 'TextMatchAsserter',
context: {
stepTag
},
cause: {
expected: tomatch,
actual: botresponse
}
}
)
}
},
assertBotNotResponse: (botresponse, nottomatch, stepTag, meMsg) => {
debug(`assertBotNotResponse ${stepTag} ${meMsg ? `(${meMsg}) ` : ''}BOT: ${botresponse} != ${nottomatch} ...`)
try {
this.scriptingEvents.assertBotResponse(botresponse, nottomatch, stepTag)
} catch (err) {
return
}
throw new BotiumError(
`${stepTag}: Expected bot response ${meMsg ? `(on ${meMsg}) ` : ''}"${botresponse}" NOT to match one of "${nottomatch}"`,
{
type: 'asserter',
source: 'TextNotMatchAsserter',
context: {
stepTag
},
cause: {
expected: nottomatch,
actual: botresponse
}
}
)
},
fail: null
}
}
_createAsserterPromises ({ asserterType, asserters, convo, convoStep, scriptingMemory, ...rest }) {
if (!this._isValidAsserterType(asserterType)) {
throw Error(`Unknown asserterType ${asserterType}`)
}
const convoAsserter = asserters
.filter(a => this.asserters[a.name][asserterType])
.map(a => p(() => this.asserters[a.name][asserterType]({
convo,
convoStep,
scriptingMemory,
args: ScriptingMemory.applyToArgs(a.args, scriptingMemory),
isGlobal: false,
...rest
})))
const globalAsserter = Object.values(this.globalAsserter)
.filter(a => a[asserterType])
.map(a => p(() => a[asserterType]({ convo, convoStep, scriptingMemory, args: [], isGlobal: true, ...rest })))
const allPromises = [...convoAsserter, ...globalAsserter]
if (this.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
return Promise.allSettled(allPromises).then((results) => {
const rejected = results.filter(result => result.status === 'rejected').map(result => result.reason)
if (rejected.length) {
throw botiumErrorFromList(rejected, {})
}
return results.filter(result => result.status === 'fulfilled').map(result => result.value)
})
}
return Promise.all(allPromises)
}
_createLogicHookPromises ({ hookType, logicHooks, convo, convoStep, scriptingMemory, ...rest }) {
if (hookType !== 'onMeStart' && hookType !== 'onMeEnd' && hookType !== 'onBotStart' && hookType !== 'onBotEnd' &&
hookType !== 'onConvoBegin' && hookType !== 'onConvoEnd'
) {
throw Error(`Unknown hookType ${hookType}`)
}
const convoStepPromises = (logicHooks || [])
.filter(l => this.logicHooks[l.name][hookType])
.map(l => p(() => this.logicHooks[l.name][hookType]({
convo,
convoStep,
scriptingMemory,
args: ScriptingMemory.applyToArgs(l.args, scriptingMemory),
isGlobal: false,
...rest
})))
const globalPromises = Object.values(this.globalLogicHook)
.filter(l => l[hookType])
.map(l => p(() => l[hookType]({ convo, convoStep, scriptingMemory, args: [], isGlobal: true, ...rest })))
const allPromises = [...convoStepPromises, ...globalPromises]
return Promise.all(allPromises)
}
_createUserInputPromises ({ convo, convoStep, scriptingMemory, ...rest }) {
const convoStepPromises = (convoStep.userInputs || [])
.filter(ui => this.userInputs[ui.name])
.map(ui => p(() => this.userInputs[ui.name].setUserInput({
convo,
convoStep,
scriptingMemory,
args: ScriptingMemory.applyToArgs(ui.args, scriptingMemory),
...rest
})))
return Promise.all(convoStepPromises)
}
_isValidAsserterType (asserterType) {
return ['assertConvoBegin', 'assertConvoStep', 'assertConvoEnd'].some(t => asserterType === t)
}
_buildScriptContext () {
return {
AddConvos: this.AddConvos.bind(this),
AddUtterances: this.AddUtterances.bind(this),
AddPartialConvos: this.AddPartialConvos.bind(this),
AddScriptingMemories: this.AddScriptingMemories.bind(this),
Match: this.Match.bind(this),
IsAsserterValid: this.IsAsserterValid.bind(this),
IsLogicHookValid: this.IsLogicHookValid.bind(this),
IsUserInputValid: this.IsUserInputValid.bind(this),
GetPartialConvos: this.GetPartialConvos.bind(this),
scriptingEvents: {
assertConvoBegin: this.scriptingEvents.assertConvoBegin.bind(this),
assertConvoStep: this.scriptingEvents.assertConvoStep.bind(this),
assertConvoEnd: this.scriptingEvents.assertConvoEnd.bind(this),
resolveUtterance: this.scriptingEvents.resolveUtterance.bind(this),
assertBotResponse: this.scriptingEvents.assertBotResponse.bind(this),
assertBotNotResponse: this.scriptingEvents.assertBotNotResponse.bind(this),
onConvoBegin: this.scriptingEvents.onConvoBegin.bind(this),
onConvoEnd: this.scriptingEvents.onConvoEnd.bind(this),
onMeStart: this.scriptingEvents.onMeStart.bind(this),
onMeEnd: this.scriptingEvents.onMeEnd.bind(this),
onBotStart: this.scriptingEvents.onBotStart.bind(this),
onBotEnd: this.scriptingEvents.onBotEnd.bind(this),
setUserInput: this.scriptingEvents.setUserInput.bind(this),
fail: this.scriptingEvents.fail && this.scriptingEvents.fail.bind(this)
}
}
}
Build () {
const CompilerXlsx = require('./CompilerXlsx')
this.compilers[Constants.SCRIPTING_FORMAT_XSLX] = new CompilerXlsx(this._buildScriptContext(), this.caps)
this.compilers[Constants.SCRIPTING_FORMAT_XSLX].Validate()
const CompilerTxt = require('./CompilerTxt')
this.compilers[Constants.SCRIPTING_FORMAT_TXT] = new CompilerTxt(this._buildScriptContext(), this.caps)
this.compilers[Constants.SCRIPTING_FORMAT_TXT].Validate()
const CompilerCsv = require('./CompilerCsv')
this.compilers[Constants.SCRIPTING_FORMAT_CSV] = new CompilerCsv(this._buildScriptContext(), this.caps)
this.compilers[Constants.SCRIPTING_FORMAT_CSV].Validate()
debug('Using matching mode: ' + this.caps[Capabilities.SCRIPTING_MATCHING_MODE])
if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'regexp') {
this.matchFn = (botresponse, utterance) => (new RegExp(utterance, 'i')).test(botresponse)
} else if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'include') {
this.matchFn = (botresponse, utterance) => botresponse.indexOf(utterance) >= 0
} else if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'includeLowerCase') {
this.matchFn = (botresponse, utterance) => botresponse.toLowerCase().indexOf(utterance.toLowerCase()) >= 0
} else {
this.matchFn = (botresponse, utterance) => botresponse === utterance
}
const logicHookUtils = new LogicHookUtils({ buildScriptContext: this._buildScriptContext(), caps: this.caps })
this.asserters = logicHookUtils.asserters
this.globalAsserter = logicHookUtils.getGlobalAsserter()
this.logicHooks = logicHookUtils.logicHooks
this.globalLogicHook = logicHookUtils.getGlobalLogicHook()
this.userInputs = logicHookUtils.userInputs
}
IsAsserterValid (name) {
return this.asserters[name] || false
}
IsLogicHookValid (name) {
return this.logicHooks[name] || false
}
IsUserInputValid (name) {
return this.userInputs[name] || false
}
Match (botresponse, utterance) {
return this.matchFn(botresponse, utterance)
}
Compile (scriptBuffer, scriptFormat, scriptType) {
const compiler = this.GetCompiler(scriptFormat)
return compiler.Compile(scriptBuffer, scriptType)
}
Decompile (convos, scriptFormat) {
const compiler = this.GetCompiler(scriptFormat)
return compiler.Decompile(convos)
}
GetCompiler (scriptFormat) {
const result = this.compilers[scriptFormat]
if (result) return result
throw new Error(`No compiler found for scriptFormat ${scriptFormat}`)
}
ReadScriptsFromDirectory (convoDir, globFilter) {
const filelist = glob.sync(globPattern, { cwd: convoDir })
if (globFilter) {
const filelistGlobbed = glob.sync(globFilter, { cwd: convoDir })
_.remove(filelist, (file) => filelistGlobbed.indexOf(file) < 0)
}
debug(`ReadConvosFromDirectory(${convoDir}) found filenames: ${filelist}`)
const dirConvos = []
const dirUtterances = []
const dirPartialConvos = []
const dirScriptingMemories = []
filelist.forEach((filename) => {
const { convos, utterances, pconvos, scriptingMemories } = this.ReadScript(convoDir, filename)
if (convos) dirConvos.push(...convos)
if (utterances) dirUtterances.push(...utterances)
if (pconvos) dirPartialConvos.push(...pconvos)
if (scriptingMemories) dirScriptingMemories.push(...scriptingMemories)
})
debug(`ReadConvosFromDirectory(${convoDir}) found convos:\n ${dirConvos.length ? dirConvos.join('\n') : 'none'}`)
debug(`ReadConvosFromDirectory(${convoDir}) found utterances:\n ${dirUtterances.length ? _.map(dirUtterances, (u) => u).join('\n') : 'none'}`)
debug(`ReadConvosFromDirectory(${convoDir}) found partial convos:\n ${dirPartialConvos.length ? dirPartialConvos.join('\n') : 'none'}`)
debug(`ReadConvosFromDirectory(${convoDir}) scripting memories:\n ${dirScriptingMemories.length ? dirScriptingMemories.map((dirScriptingMemory) => util.inspect(dirScriptingMemory)).join('\n') : 'none'}`)
return { convos: dirConvos, utterances: dirUtterances, pconvos: dirPartialConvos, scriptingMemories: dirScriptingMemories }
}
ReadScript (convoDir, filename) {
let fileConvos = []
let fileUtterances = []
let filePartialConvos = []
let fileScriptingMemories = []
const scriptBuffer = fs.readFileSync(path.resolve(convoDir, filename))
if (filename.endsWith('.xlsx')) {
fileUtterances = this.Compile(scriptBuffer, Constants.SCRIPTING_FORMAT_XSLX, Constants.SCRIPTING_TYPE_UTTERANCES)
filePartialConvos = this.Compile(scriptBuffer, Constants.SCRIPTING_FORMAT_XSLX, Constants.SCRIPTING_TYPE_PCONVO)
fileConvos = this.Compile(scriptBuffer, Constants.SCRIPTING_FORMAT_XSLX, Constants.SCRIPTING_TYPE_CONVO)
fileScriptingMemories = this.Compile(scriptBuffer, Constants.SCRIPTING_FORMAT_XSLX, Constants.SCRIPTING_TYPE_SCRIPTING_MEMORY)
} else if (filename.endsWith('.convo.txt')) {
fileConvos = this.Compile(scriptBuffer, Constants.SCRIPTING_FORMAT_TXT, Constants.SCRIPTING_TYPE_CONVO)
} else if (filename.endsWith('.pconvo.txt')) {
filePartialConvos = this.Compile(scriptBuffer, Constants.SCRIPTING_FORMAT_TXT, Constants.SCRIPTING_TYPE_PCONVO)
} else if (filename.endsWith('.utterances.txt')) {
fileUtterances = this.Compile(scriptBuffer, Constants.SCRIPTING_FORMAT_TXT, Constants.SCRIPTING_TYPE_UTTERANCES)
} else if (filename.endsWith('.scriptingmemory.txt')) {
fileScriptingMemories = this.Compile(scriptBuffer, Constants.SCRIPTING_FORMAT_TXT, Constants.SCRIPTING_TYPE_SCRIPTING_MEMORY)
} else if (filename.endsWith('.convo.csv')) {
fileConvos = this.Compile(scriptBuffer, Constants.SCRIPTING_FORMAT_CSV, Constants.SCRIPTING_TYPE_CONVO)
} else if (filename.endsWith('.pconvo.csv')) {
filePartialConvos = this.Compile(scriptBuffer, Constants.SCRIPTING_FORMAT_CSV, Constants.SCRIPTING_TYPE_PCONVO)
}
// Compilers saved the convos, and we alter here the saved version too
if (fileConvos) {
fileConvos.forEach((fileConvo) => {
fileConvo.sourceTag = { convoDir, filename }
if (!fileConvo.header.name) {
fileConvo.header.name = filename
}
})
}
if (filePartialConvos) {
filePartialConvos.forEach((filePartialConvo) => {
filePartialConvo.sourceTag = { convoDir, filename }
if (!filePartialConvo.header.name) {
filePartialConvo.header.name = filename
}
})
}
if (fileScriptingMemories && fileScriptingMemories.length) {
fileScriptingMemories.forEach((scriptingMemory) => {
scriptingMemory.sourceTag = { filename }
})
}
if (fileUtterances) {
this.fileUtterances = this._tagAndCleanupUtterances(fileUtterances, convoDir, filename)
}
return { convos: fileConvos, utterances: fileUtterances, pconvos: filePartialConvos, scriptingMemories: fileScriptingMemories }
}
_tagAndCleanupUtterances (utteranceFiles, convoDir, filename) {
return utteranceFiles.map((fileUtt) => {
fileUtt.sourceTag = { convoDir, filename }
fileUtt.utterances = fileUtt.utterances
.filter(u => u)
return fileUtt
})
}
ExpandScriptingMemoryToConvos () {
if (!this.caps[Capabilities.SCRIPTING_ENABLE_MEMORY]) {
debug('ExpandScriptingMemoryToConvos - Scripting memory turned off, no convos expanded')
return
}
const variablesToScriptingMemory = new Map()
this.scriptingMemories.forEach((scriptingMemory) => {
const key = JSON.stringify(Object.keys(scriptingMemory.values).sort())
if (variablesToScriptingMemory.has(key)) {
variablesToScriptingMemory.get(key).push(scriptingMemory)
} else {
variablesToScriptingMemory.set(key, [scriptingMemory])
}
})
let convosExpandedAll = []
const convosOriginalAll = []
this.convos.forEach((convo) => {
const convoVariables = convo.GetScriptingMemoryAllVariables(this)
debug(`ExpandScriptingMemoryToConvos - Convo "${convo.header.name}" - Variables to replace, all: "${util.inspect(convoVariables)}"`)
if (!convoVariables.length) {
debug(`ExpandScriptingMemoryToConvos - Convo "${convo.header.name}" - skipped, no variable found to replace`)
}
// // debug output, & filling fileToVariables
// variables.forEach((variable) => {
// const alreadyUsedVariable = convo.beginLogicHook.filter((logicHook) => {
// // .substring(1): cut the $ because logichooks
// return (logicHook.name === 'SET_SCRIPTING_MEMORY' || logicHook.name === 'CLEAR_SCRIPTING_MEMORY') &&
// logicHook.args.indexOf(variable.substring(1)) >= 0
// })
//
// if (alreadyUsedVariable.length) {
// debug(`ExpandScriptingMemoryToConvos - Convo "${convo.header.name}" - Scripting memory variable "${variable}" defined external (scripting memory file?), and in logicHook(s) "${util.inspect(alreadyUsedVariable)}"`)
// }
// })
let convosToExpand = [convo]
let convosExpandedConvo = []
// just for debug output. If we got 6 expanded convo, then this array can be for example [2, 3]
const multipliers = []
for (const [key, scriptingMemories] of variablesToScriptingMemory.entries()) {
const variableNames = JSON.parse(key)
if (_.intersection(variableNames, convoVariables).length) {
const convosExpandedVariable = []
multipliers.push(scriptingMemories.length)
scriptingMemories.forEach((scriptingMemory) => {
// Appending the case name to name
for (const convoToExpand of convosToExpand) {
const convoExpanded = _.cloneDeep(convoToExpand)
convoExpanded.header.name = convoToExpand.header.name + '.' + scriptingMemory.header.name
variableNames.forEach((name) => {
const value = scriptingMemory.values[name]
if (value) {
convoExpanded.beginLogicHook.push({ name: 'SET_SCRIPTING_MEMORY', args: [name.substring(1), scriptingMemory.values[name]] })
} else {
convoExpanded.beginLogicHook.push({ name: 'CLEAR_SCRIPTING_MEMORY', args: [name.substring(1)] })
}
})
convosExpandedVariable.push(convoExpanded)
}
})
// This is a bit tricky. If the loop is done, then convosExpandedConvo will be used,
// otherwise convosToExpand. They could be one variable
convosToExpand = convosExpandedVariable
convosExpandedConvo = convosExpandedVariable
} else {
debug(`ExpandScriptingMemoryToConvos - Convo "${convo.header.name}" - Scripting memory ${key} ignored because there is no common variable with convo ${util.inspect(convoVariables)}`)
}
}
debug(`ExpandScriptingMemoryToConvos - Convo "${convo.header.name}" - Expanding convo "${convo.header.name}" Expanded ${convosExpandedConvo.length} convo. (Details: ${convosExpandedConvo.length} = ${multipliers ? multipliers.join('*') : 0})`)
if (convosExpandedConvo.length) {
convosExpandedAll = convosExpandedAll.concat(convosExpandedConvo)
convosOriginalAll.push(convo)
}
})
if (this.caps[Capabilities.SCRIPTING_MEMORYEXPANSION_KEEP_ORIG] !== true) {
debug(`ExpandScriptingMemoryToConvos - Deleting ${convosOriginalAll.length} original convo`)
this.convos = this.convos.filter((convo) => convosOriginalAll.indexOf(convo) === -1)
}
debug(`ExpandScriptingMemoryToConvos - ${convosExpandedAll.length} convo expanded, added to convos (${this.convos.length}). Result ${convosExpandedAll.length + this.convos.length} convo`)
this.convos = this.convos.concat(convosExpandedAll)
}
ExpandUtterancesToConvos () {
const expandedConvos = []
const incomprehensionUtt = this.caps[Capabilities.SCRIPTING_UTTEXPANSION_INCOMPREHENSION]
if (incomprehensionUtt && !this.utterances[incomprehensionUtt]) {
throw new Error(`ExpandUtterancesToConvos - incomprehension utterance '${incomprehensionUtt}' undefined`)
}
if (incomprehensionUtt) {
debug(`ExpandUtterancesToConvos - Using incomprehension utterance expansion mode: ${incomprehensionUtt}`)
}
_.keys(this.utterances).filter(u => u !== incomprehensionUtt).forEach(uttName => {
const utt = this.utterances[uttName]
expandedConvos.push(new Convo(this._buildScriptContext(), {
header: {
name: utt.name,
description: `Expanded Utterances - ${utt.name}`
},
conversation: [
{
sender: 'me',
messageText: utt.name,
stepTag: 'Step 1 - tell utterance'
},
incomprehensionUtt
? {
sender: 'bot',
messageText: incomprehensionUtt,
stepTag: 'Step 2 - check incomprehension',
not: true
}
: {
sender: 'bot',
messageText: '',
stepTag: 'Step 2 - check bot response',
not: false
}
],
sourceTag: utt.sourceTag
}))
})
this.convos = this.convos.concat(expandedConvos)
this._sortConvos()
}
ExpandConvos () {
const expandedConvos = []
debug(`ExpandConvos - Using utterances expansion mode: ${this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE]}`)
this.convos.forEach((convo) => {
this._expandConvo(expandedConvos, convo)
})
this.convos = expandedConvos
this._sortConvos()
}
/**
*
* @param expandedConvos
* @param currentConvo
* @param convoStepIndex
* @param convoStepsStack list of ConvoSteps
* @private
*/
_expandConvo (expandedConvos, currentConvo, convoStepIndex = 0, convoStepsStack = []) {
if (convoStepIndex < currentConvo.conversation.length) {
const currentStep = currentConvo.conversation[convoStepIndex]
if (currentStep.sender === 'bot' || currentStep.sender === 'begin' || currentStep.sender === 'end') {
const currentStepsStack = convoStepsStack.slice()
currentStepsStack.push(_.cloneDeep(currentStep))
this._expandConvo(expandedConvos, currentConvo, convoStepIndex + 1, currentStepsStack)
} else if (currentStep.sender === 'me') {
if (currentStep.messageText) {
const parts = currentStep.messageText.split(' ')
const uttName = parts[0]
const uttArgs = parts.slice(1)
if (this.utterances[uttName]) {
const allutterances = this.utterances[uttName].utterances
let sampleutterances = allutterances
if (this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE] === 'first') {
sampleutterances = [allutterances[0]]
} else if (this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE] === 'random') {
sampleutterances = allutterances
.map(x => ({ x, r: Math.random() }))
.sort((a, b) => a.r - b.r)
.map(a => a.x)
.slice(0, this.caps[Capabilities.SCRIPTING_UTTEXPANSION_RANDOM_COUNT])
}
sampleutterances.forEach((utt, index) => {
const currentStepsStack = convoStepsStack.slice()
if (uttArgs) {
utt = util.format(utt, ...uttArgs)
}
currentStepsStack.push(Object.assign(_.cloneDeep(currentStep), { messageText: utt }))
const currentConvoLabeled = Object.assign(_.cloneDeep(currentConvo), { header: Object.assign({}, currentConvo.header, { name: currentConvo.header.name + '/' + uttName + '-L' + (index + 1) }) })
this._expandConvo(expandedConvos, currentConvoLabeled, convoStepIndex + 1, currentStepsStack)
})
return
}
}
const currentStepsStack = convoStepsStack.slice()
currentStepsStack.push(_.cloneDeep(currentStep))
this._expandConvo(expandedConvos, currentConvo, convoStepIndex + 1, currentStepsStack)
}
} else {
expandedConvos.push(Object.assign(_.cloneDeep(currentConvo), { conversation: convoStepsStack }))
}
}
_sortConvos () {
this.convos = _.sortBy(this.convos, [(convo) => convo.header.sort || convo.header.name])
let i = 0
this.convos.forEach((convo) => {
convo.header.order = ++i
})
}
AddConvos (convos) {
if (convos && _.isArray(convos)) {
this.convos = _.concat(this.convos, convos)
} else if (convos) {
this.convos.push(convos)
}
this._sortConvos()
}
AddUtterances (utterances) {
if (utterances && !_.isArray(utterances)) {
utterances = [utterances]
}
if (utterances) {
_.forEach(utterances, (utt) => {
const eu = this.utterances[utt.name]
if (eu) {
eu.utterances = _.uniq(_.concat(eu.utterances, utt.utterances))
} else {
this.utterances[utt.name] = utt
}
})
}
}
AddPartialConvos (convos) {
if (convos && _.isArray(convos)) {
for (let i = 0; i < convos.length; i++) {
const convo = convos[i]
this.AddPartialConvos(convo)
}
} else if (convos) {
if (!convos.header || !convos.header.name) {
throw Error(`Header name is mandatory: ${JSON.stringify(convos.header)}`)
}
if (convos.header.name.indexOf('|') >= 0) {
throw Error(`Invalid partial convo name: ${convos.header.name}`)
}
const name = convos.header.name
if (this.partialConvos[name]) {
throw Error(`Duplicate partial convo: ${name}`)
}
this.partialConvos[name] = convos
}
}
GetPartialConvos () {
return this.partialConvos
}
AddScriptingMemories (scriptingMemories) {
if (scriptingMemories && _.isArray(scriptingMemories)) {
for (let i = 0; i < scriptingMemories.length; i++) {
const scriptingMemory = scriptingMemories[i]
this.AddScriptingMemories(scriptingMemory)
}
} else if (scriptingMemories) {
this.scriptingMemories.push(scriptingMemories)
}
}
}