botium-core 1.15.8 → 1.15.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/botium-cjs.js +147 -8
- package/dist/botium-cjs.js.map +1 -1
- package/dist/botium-es.js +147 -8
- package/dist/botium-es.js.map +1 -1
- package/package.json +1 -1
- package/src/BotDriver.js +2 -2
- package/src/helpers/TranscriptUtils.js +66 -0
- package/src/scripting/Convo.js +9 -0
- package/src/scripting/ScriptingProvider.js +46 -6
- package/src/scripting/logichook/userinput/MediaInput.js +10 -2
- package/test/driver/capabilities.spec.js +9 -0
package/package.json
CHANGED
package/src/BotDriver.js
CHANGED
|
@@ -241,8 +241,8 @@ module.exports = class BotDriver {
|
|
|
241
241
|
if (_.isString(newCaps[capKey])) {
|
|
242
242
|
try {
|
|
243
243
|
caps[capKey] = JSON.parse(newCaps[capKey])
|
|
244
|
-
if (_.isFinite(
|
|
245
|
-
caps[capKey] =
|
|
244
|
+
if (_.isFinite(Number(newCaps[capKey]))) {
|
|
245
|
+
caps[capKey] = newCaps[capKey].toString()
|
|
246
246
|
}
|
|
247
247
|
} catch (err) {
|
|
248
248
|
caps[capKey] = newCaps[capKey]
|
|
@@ -1,4 +1,70 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs')
|
|
1
3
|
const _ = require('lodash')
|
|
4
|
+
const { parse: csvParseSync } = require('csv-parse/sync')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Find transcription for an audio file: .txt same base name, or transcript.csv in parent dirs.
|
|
8
|
+
* @param {string} baseDir - Base directory for resolving paths
|
|
9
|
+
* @param {string} audioFile - Relative path to audio file
|
|
10
|
+
* @param {object} [options] - Optional: { csvCache: {}, onError: (msg) => {} }
|
|
11
|
+
* @returns {string|null} Transcription text or null
|
|
12
|
+
*/
|
|
13
|
+
module.exports.findTranscription = (baseDir, audioFile, options = {}) => {
|
|
14
|
+
const { csvCache = {}, onError } = options
|
|
15
|
+
const transcriptionFilename = `${audioFile.substring(0, audioFile.lastIndexOf('.'))}.txt`
|
|
16
|
+
const transcriptionFilenameAbs = path.resolve(baseDir, transcriptionFilename)
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(transcriptionFilenameAbs)) {
|
|
19
|
+
return fs.readFileSync(transcriptionFilenameAbs, { encoding: 'utf-8' }).trim()
|
|
20
|
+
}
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (onError) onError(`Transcription File ${transcriptionFilenameAbs} not readable: ${err.message}`)
|
|
23
|
+
throw new Error(`Reading transcription file ${transcriptionFilename} for ${audioFile} failed`)
|
|
24
|
+
}
|
|
25
|
+
if (csvCache[audioFile]) {
|
|
26
|
+
return csvCache[audioFile]
|
|
27
|
+
}
|
|
28
|
+
const audioFileComponents = audioFile.split('/')
|
|
29
|
+
for (let parentIndex = audioFileComponents.length - 1; parentIndex >= 0; parentIndex--) {
|
|
30
|
+
const csvDirectory = audioFileComponents.slice(0, parentIndex)
|
|
31
|
+
const csvFilename = path.join(...csvDirectory, 'transcript.csv')
|
|
32
|
+
const csvFilenameAbs = path.resolve(baseDir, csvFilename)
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(csvFilenameAbs)) {
|
|
35
|
+
const records = csvParseSync(fs.readFileSync(csvFilenameAbs, { encoding: 'utf-8' }).trim(), {
|
|
36
|
+
columns: ['filename', 'transcription'],
|
|
37
|
+
delimiter: [',', ';', ':', '\t'],
|
|
38
|
+
trim: true,
|
|
39
|
+
skip_empty_lines: true
|
|
40
|
+
})
|
|
41
|
+
if (records && records.length > 0) {
|
|
42
|
+
for (const record of records) {
|
|
43
|
+
const fnKey = path.join(...csvDirectory, record.filename)
|
|
44
|
+
csvCache[fnKey] = record.transcription
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (onError) onError(`Transcription CSV File ${csvFilenameAbs} not readable: ${err.message}`)
|
|
50
|
+
throw new Error(`Reading transcription CSV file for ${csvFilename} failed`)
|
|
51
|
+
}
|
|
52
|
+
if (csvCache[audioFile]) {
|
|
53
|
+
return csvCache[audioFile]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Derive transcription text from audio filename (basename without extension, underscores/hyphens → spaces).
|
|
61
|
+
* @param {string} audioFile - Path or filename of audio file
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
module.exports.transcriptionFromFilename = (audioFile) => {
|
|
65
|
+
const filename = path.basename(audioFile, path.extname(audioFile))
|
|
66
|
+
return filename.split(/[_-]+/).join(' ')
|
|
67
|
+
}
|
|
2
68
|
|
|
3
69
|
module.exports.hasWaitForBotTimeout = (transciptError) => {
|
|
4
70
|
if (!transciptError) {
|
package/src/scripting/Convo.js
CHANGED
|
@@ -270,6 +270,7 @@ class Convo {
|
|
|
270
270
|
|
|
271
271
|
async runConversation (container, scriptingMemory, transcript) {
|
|
272
272
|
const transcriptSteps = []
|
|
273
|
+
transcript.steps = transcriptSteps
|
|
273
274
|
try {
|
|
274
275
|
let lastMeConvoStep = null
|
|
275
276
|
let botMsg = null
|
|
@@ -416,6 +417,14 @@ class Convo {
|
|
|
416
417
|
throw failErr
|
|
417
418
|
}
|
|
418
419
|
} else if (convoStep.sender === 'bot') {
|
|
420
|
+
if (this.scriptingEvents.executeBotStep) {
|
|
421
|
+
const executeBotStepResult = await this.scriptingEvents.executeBotStep({ convo: this, convoStep, container, scriptingMemory, transcript, transcriptStep, transcriptSteps })
|
|
422
|
+
if (executeBotStepResult) {
|
|
423
|
+
skipTranscriptStep = true
|
|
424
|
+
continue
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
419
428
|
if (waitForBotSays) {
|
|
420
429
|
botMsg = null
|
|
421
430
|
} else {
|
|
@@ -115,6 +115,25 @@ module.exports = class ScriptingProvider {
|
|
|
115
115
|
onBotEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
116
116
|
return this._createLogicHookPromises({ hookType: 'onBotEnd', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
|
|
117
117
|
},
|
|
118
|
+
executeBotStep: ({ convo, convoStep, container, scriptingMemory, ...rest }) => {
|
|
119
|
+
const logicHooks = (convoStep?.logicHooks || [])
|
|
120
|
+
const executeBotStepHooks = logicHooks.filter(l => this.logicHooks[l.name] && typeof this.logicHooks[l.name].executeBotStep === 'function')
|
|
121
|
+
if (executeBotStepHooks.length > 1) {
|
|
122
|
+
throw new Error(`${convo?.header?.name}/${convoStep?.stepTag}: Multiple logic hooks implement executeBotStep: ${executeBotStepHooks.map(l => l.name).join(', ')}. Only one is allowed per step.`)
|
|
123
|
+
}
|
|
124
|
+
if (executeBotStepHooks.length === 1) {
|
|
125
|
+
const lh = executeBotStepHooks[0]
|
|
126
|
+
return this.logicHooks[lh.name].executeBotStep({
|
|
127
|
+
convo,
|
|
128
|
+
convoStep,
|
|
129
|
+
scriptingMemory,
|
|
130
|
+
container,
|
|
131
|
+
args: ScriptingMemory.applyToArgs(lh.args, scriptingMemory, container.caps),
|
|
132
|
+
...rest
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
return null
|
|
136
|
+
},
|
|
118
137
|
assertConvoBegin: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
119
138
|
return this._createAsserterPromises({ asserterType: 'assertConvoBegin', asserters: (convo?.beginAsserter || []), convo, convoStep, scriptingMemory, ...rest })
|
|
120
139
|
},
|
|
@@ -377,7 +396,15 @@ module.exports = class ScriptingProvider {
|
|
|
377
396
|
throw Error(`Unknown hookType ${hookType}`)
|
|
378
397
|
}
|
|
379
398
|
|
|
380
|
-
|
|
399
|
+
let localHooks = (logicHooks || []).filter(l => this.logicHooks[l.name][hookType])
|
|
400
|
+
// Scripting memory file are injected via SET_SCRIPTING_MEMORY in the BEGIN step
|
|
401
|
+
// But there might be other logic hooks that need the scripting memory variables
|
|
402
|
+
// Order is important (SET_SCRIPTING_MEMORY in begin can be because the user added it,
|
|
403
|
+
// or because the scripting memory file added it. User one has to be the last one.
|
|
404
|
+
localHooks = [
|
|
405
|
+
...localHooks.filter(l => l.name === 'SET_SCRIPTING_MEMORY'),
|
|
406
|
+
...localHooks.filter(l => l.name !== 'SET_SCRIPTING_MEMORY')
|
|
407
|
+
]
|
|
381
408
|
|
|
382
409
|
const convoStepPromises = localHooks
|
|
383
410
|
.map(l => p(this.retryHelperLogicHook, () => this.logicHooks[l.name][hookType]({
|
|
@@ -385,6 +412,7 @@ module.exports = class ScriptingProvider {
|
|
|
385
412
|
convoStep,
|
|
386
413
|
scriptingMemory,
|
|
387
414
|
container,
|
|
415
|
+
// Do this more sensitve for SET_SCRIPTING_MEMORY? It can have scripting variables in the args
|
|
388
416
|
args: ScriptingMemory.applyToArgs(l.args, scriptingMemory, container.caps, rest.botMsg),
|
|
389
417
|
isGlobal: false,
|
|
390
418
|
...rest
|
|
@@ -471,6 +499,7 @@ module.exports = class ScriptingProvider {
|
|
|
471
499
|
onBotStart: this.scriptingEvents.onBotStart.bind(this),
|
|
472
500
|
onBotPrepare: this.scriptingEvents.onBotPrepare.bind(this),
|
|
473
501
|
onBotEnd: this.scriptingEvents.onBotEnd.bind(this),
|
|
502
|
+
executeBotStep: this.scriptingEvents.executeBotStep.bind(this),
|
|
474
503
|
setUserInput: this.scriptingEvents.setUserInput.bind(this),
|
|
475
504
|
fail: this.scriptingEvents.fail && this.scriptingEvents.fail.bind(this)
|
|
476
505
|
}
|
|
@@ -995,7 +1024,11 @@ module.exports = class ScriptingProvider {
|
|
|
995
1024
|
// use skip and keep, or justHeader
|
|
996
1025
|
justHeader: false,
|
|
997
1026
|
// drop unwanted convos
|
|
998
|
-
convoFilter: null
|
|
1027
|
+
convoFilter: null,
|
|
1028
|
+
mediaInput: {
|
|
1029
|
+
// MESSAGE_TEXT_FROM_FILENAME or MESSAGE_TEXT_FROM_TRANSCRIPTION or falsy
|
|
1030
|
+
messageTextMode: null
|
|
1031
|
+
}
|
|
999
1032
|
}, options)
|
|
1000
1033
|
const expandedConvos = []
|
|
1001
1034
|
// The globalContext is going to keep the data even if the Object.assign which happening to create the myContext in _expandConvo function
|
|
@@ -1034,7 +1067,11 @@ module.exports = class ScriptingProvider {
|
|
|
1034
1067
|
ExpandConvosIterable (options = {}) {
|
|
1035
1068
|
options = Object.assign({
|
|
1036
1069
|
// drop unwanted convos
|
|
1037
|
-
convoFilter: null
|
|
1070
|
+
convoFilter: null,
|
|
1071
|
+
mediaInput: {
|
|
1072
|
+
// MESSAGE_TEXT_FROM_FILENAME or MESSAGE_TEXT_FROM_TRANSCRIPTION or falsy
|
|
1073
|
+
messageTextMode: null
|
|
1074
|
+
}
|
|
1038
1075
|
}, options)
|
|
1039
1076
|
// The globalContext is going to keep the data even if the Object.assign which happening to create the myContext in _expandConvo function
|
|
1040
1077
|
const context = {
|
|
@@ -1167,7 +1204,7 @@ module.exports = class ScriptingProvider {
|
|
|
1167
1204
|
const ui = currentStep.userInputs[uiIndex]
|
|
1168
1205
|
const userInput = this.userInputs[ui.name]
|
|
1169
1206
|
if (userInput && userInput.expandConvo) {
|
|
1170
|
-
const expandedUserInputs = userInput.expandConvo({ convo: currentConvo, convoStep: currentStep, args: ui.args })
|
|
1207
|
+
const expandedUserInputs = userInput.expandConvo({ convo: currentConvo, convoStep: currentStep, args: ui.args, options })
|
|
1171
1208
|
if (expandedUserInputs && expandedUserInputs.length > 0) {
|
|
1172
1209
|
// let sampleinputs = expandedUserInputs
|
|
1173
1210
|
const processSampleInputs = function * (sampleinputs, myContext, uiIndex) {
|
|
@@ -1176,10 +1213,13 @@ module.exports = class ScriptingProvider {
|
|
|
1176
1213
|
}
|
|
1177
1214
|
}
|
|
1178
1215
|
const processSampleInput = function * (sampleinput, length, index, myContext, uiIndex) {
|
|
1216
|
+
const { messageText, ...userInput } = sampleinput
|
|
1179
1217
|
const currentStepsStack = convoStepsStack.slice()
|
|
1180
1218
|
const currentStepMod = _.cloneDeep(currentStep)
|
|
1181
|
-
currentStepMod.userInputs[uiIndex] =
|
|
1182
|
-
|
|
1219
|
+
currentStepMod.userInputs[uiIndex] = userInput
|
|
1220
|
+
if (messageText) {
|
|
1221
|
+
currentStepMod.messageText = messageText
|
|
1222
|
+
}
|
|
1183
1223
|
currentStepsStack.push(currentStepMod)
|
|
1184
1224
|
const currentConvoLabeled = _.cloneDeep(currentConvo)
|
|
1185
1225
|
if (length > 1) {
|
|
@@ -8,6 +8,7 @@ const _ = require('lodash')
|
|
|
8
8
|
const { BotiumMockMedia } = require('../../../../src/mocks/BotiumMockRichMessageTypes')
|
|
9
9
|
const { BotiumError } = require('../../../../src/scripting/BotiumError')
|
|
10
10
|
const Capabilities = require('../../../../src/Capabilities')
|
|
11
|
+
const TranscriptUtils = require('../../../helpers/TranscriptUtils')
|
|
11
12
|
|
|
12
13
|
const DEFAULT_BASE_SELECTOR = 'sourceTag.testSetId'
|
|
13
14
|
|
|
@@ -151,7 +152,7 @@ module.exports = class MediaInput {
|
|
|
151
152
|
return mime.lookup(arg)
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
expandConvo ({ convo, convoStep, args }) {
|
|
155
|
+
expandConvo ({ convo, convoStep, args, options }) {
|
|
155
156
|
const hasWildcard = args.findIndex(a => this._isWildcard(a)) >= 0
|
|
156
157
|
|
|
157
158
|
if (args && (args.length > 1 || hasWildcard)) {
|
|
@@ -161,10 +162,17 @@ module.exports = class MediaInput {
|
|
|
161
162
|
// we need to escape brackets to find files
|
|
162
163
|
const mediaFiles = globSync(arg.replace(/[()[\]{}]/g, '\\$&'), { cwd: baseDir })
|
|
163
164
|
mediaFiles.forEach(mf => {
|
|
165
|
+
let messageText = null
|
|
166
|
+
if (options.mediaInput.messageTextMode === 'MESSAGE_TEXT_FROM_FILENAME') {
|
|
167
|
+
messageText = TranscriptUtils.transcriptionFromFilename(mf)
|
|
168
|
+
} else if (options.mediaInput.messageTextMode === 'MESSAGE_TEXT_FROM_TRANSCRIPTION') {
|
|
169
|
+
messageText = TranscriptUtils.findTranscription(baseDir, mf)
|
|
170
|
+
}
|
|
164
171
|
e.push({
|
|
165
172
|
name: 'MEDIA',
|
|
166
173
|
args: [mf],
|
|
167
|
-
convoPostfix: _.last(mf.split('/'))
|
|
174
|
+
convoPostfix: _.last(mf.split('/')),
|
|
175
|
+
...messageText ? { messageText } : {}
|
|
168
176
|
})
|
|
169
177
|
})
|
|
170
178
|
} else {
|
|
@@ -75,6 +75,15 @@ describe('driver.capabilities', function () {
|
|
|
75
75
|
assert.isString(driver.caps.CAP_STRING_1)
|
|
76
76
|
assert.isString(driver.caps.CAP_STRING_2)
|
|
77
77
|
})
|
|
78
|
+
it('should merge string caps when there are numbers', function () {
|
|
79
|
+
const myCaps = {
|
|
80
|
+
CAP_STRING_1: 'Test',
|
|
81
|
+
CAP_STRING_2: '1234567892343434344'
|
|
82
|
+
}
|
|
83
|
+
const driver = new BotDriver(myCaps)
|
|
84
|
+
assert.strictEqual(driver.caps.CAP_STRING_1, myCaps.CAP_STRING_1)
|
|
85
|
+
assert.strictEqual(driver.caps.CAP_STRING_2, myCaps.CAP_STRING_2)
|
|
86
|
+
})
|
|
78
87
|
it('should merge boolean envs', function () {
|
|
79
88
|
process.env.BOTIUM_SIMPLEREST_PING_PROCESS_RESPONSE = 'NO'
|
|
80
89
|
const driver = new BotDriver()
|