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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botium-core",
3
- "version": "1.15.8",
3
+ "version": "1.15.10",
4
4
  "description": "The Selenium for Chatbots",
5
5
  "main": "index.js",
6
6
  "module": "dist/botium-es.js",
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(caps[capKey])) {
245
- caps[capKey] = caps[capKey].toString()
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) {
@@ -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
- const localHooks = (logicHooks || []).filter(l => this.logicHooks[l.name][hookType])
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] = sampleinput
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()