botium-core 1.13.2 → 1.13.3

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.
Files changed (54) hide show
  1. package/.eslintrc.js +6 -3
  2. package/dist/botium-cjs.js +214 -61
  3. package/dist/botium-cjs.js.map +1 -1
  4. package/dist/botium-es.js +213 -61
  5. package/dist/botium-es.js.map +1 -1
  6. package/package.json +3 -1
  7. package/src/Capabilities.js +2 -1
  8. package/src/containers/plugins/SimpleRestContainer.js +20 -16
  9. package/src/grid/inbound/proxy.js +2 -1
  10. package/src/scripting/Convo.js +16 -10
  11. package/src/scripting/MatchFunctions.js +10 -0
  12. package/src/scripting/ScriptingProvider.js +106 -37
  13. package/src/scripting/logichook/LogicHookConsts.js +1 -1
  14. package/src/scripting/logichook/asserter/WerAsserter.js +59 -0
  15. package/src/scripting/logichook/logichooks/UpdateCustomLogicHook.js +3 -2
  16. package/test/compiler/compilercsv.spec.js +104 -3
  17. package/test/compiler/compilerjson.spec.js +0 -2
  18. package/test/compiler/compilerxlsx.spec.js +1 -1
  19. package/test/compiler/convos/csv/utterances_liveperson2.csv +12 -0
  20. package/test/connectors/simplerest.spec.js +1012 -969
  21. package/test/convo/fillAndApplyScriptingMemory.spec.js +804 -785
  22. package/test/convo/partialconvo.spec.js +345 -339
  23. package/test/driver/capabilities.spec.js +156 -151
  24. package/test/logichooks/hookfromsrc.spec.js +79 -73
  25. package/test/plugins/plugins.spec.js +44 -42
  26. package/test/scripting/asserters/buttonsAsserter.spec.js +257 -240
  27. package/test/scripting/asserters/cardsAsserter.spec.js +214 -212
  28. package/test/scripting/asserters/convos/wer_threshold_nok.yml +7 -0
  29. package/test/scripting/asserters/convos/wer_threshold_ok.yml +7 -0
  30. package/test/scripting/asserters/intentConfidenceAsserter.spec.js +34 -35
  31. package/test/scripting/asserters/jsonpathAsserter.spec.js +307 -308
  32. package/test/scripting/asserters/mediaAsserter.spec.js +236 -234
  33. package/test/scripting/asserters/werAsserter.spec.js +51 -0
  34. package/test/scripting/logichooks/setClearScriptingMemory.spec.js +202 -192
  35. package/test/scripting/matching/matchingmode.spec.js +306 -258
  36. package/test/scripting/scriptingProvider.spec.js +666 -633
  37. package/test/scripting/scriptingmemory/fillScriptingMemoryFromFile.spec.js +299 -281
  38. package/test/scripting/scriptingmemory/useScriptingMemoryForAssertion.spec.js +94 -80
  39. package/test/scripting/userinputs/defaultUserInputs.spec.js +233 -127
  40. package/test/scripting/userinputs/mediaInputConvos.spec.js +409 -403
  41. package/test/scripting/utteranceexpansion/associateByIndex.spec.js +259 -0
  42. package/test/scripting/utteranceexpansion/convos/associate_utterances_by_index.json +33 -0
  43. package/test/scripting/utteranceexpansion/convos/media.convo.txt +19 -0
  44. package/test/scripting/utteranceexpansion/files/step0voice0.wav +0 -0
  45. package/test/scripting/utteranceexpansion/files/step0voice1.wav +0 -0
  46. package/test/scripting/utteranceexpansion/files/step0voice2.wav +0 -0
  47. package/test/scripting/utteranceexpansion/files/step1voice0.wav +0 -0
  48. package/test/scripting/utteranceexpansion/files/step2voice0.wav +0 -0
  49. package/test/scripting/utteranceexpansion/files/step2voice1.wav +0 -0
  50. package/test/scripting/utteranceexpansion/files/step2voice2.wav +0 -0
  51. package/test/scripting/utteranceexpansion/files/step2voice4.wav +0 -0
  52. package/test/scripting/utteranceexpansion/files/step2voice5.wav +0 -0
  53. package/test/security/allowUnsafe.spec.js +274 -268
  54. package/test/utils.spec.js +40 -38
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botium-core",
3
- "version": "1.13.2",
3
+ "version": "1.13.3",
4
4
  "description": "The Selenium for Chatbots",
5
5
  "main": "index.js",
6
6
  "module": "dist/botium-es.js",
@@ -66,6 +66,7 @@
66
66
  "swagger-ui-express": "^4.4.0",
67
67
  "uuid": "^8.3.2",
68
68
  "vm2": "^3.9.10",
69
+ "word-error-rate": "0.0.7",
69
70
  "write-yaml": "^1.0.0",
70
71
  "xlsx": "^0.18.5",
71
72
  "xregexp": "^5.1.1",
@@ -82,6 +83,7 @@
82
83
  "eslint": "^8.19.0",
83
84
  "eslint-config-standard": "^17.0.0",
84
85
  "eslint-plugin-import": "^2.26.0",
86
+ "eslint-plugin-mocha": "^10.1.0",
85
87
  "eslint-plugin-n": "^15.2.4",
86
88
  "eslint-plugin-promise": "^6.0.0",
87
89
  "eslint-plugin-standard": "^4.1.0",
@@ -126,8 +126,9 @@ module.exports = {
126
126
  SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS: 'SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS',
127
127
  SCRIPTING_ENABLE_SKIP_ASSERT_ERRORS: 'SCRIPTING_ENABLE_SKIP_ASSERT_ERRORS',
128
128
  SCRIPTING_FORCE_BOT_CONSUMED: 'SCRIPTING_FORCE_BOT_CONSUMED',
129
- // regexp, regexpIgnoreCase, wildcard, wildcardIgnoreCase, wildcardExact, wildcardExactIgnoreCase, include, includeIgnoreCase, equals, equalsIgnoreCase
129
+ // regexp, regexpIgnoreCase, wildcard, wildcardIgnoreCase, wildcardExact, wildcardExactIgnoreCase, include, includeIgnoreCase, equals, equalsIgnoreCase, wer
130
130
  SCRIPTING_MATCHING_MODE: 'SCRIPTING_MATCHING_MODE',
131
+ SCRIPTING_MATCHING_MODE_ARGS: 'SCRIPTING_MATCHING_MODE_ARGS',
131
132
  // all, first, random
132
133
  SCRIPTING_UTTEXPANSION_MODE: 'SCRIPTING_UTTEXPANSION_MODE',
133
134
  SCRIPTING_UTTEXPANSION_RANDOM_COUNT: 'SCRIPTING_UTTEXPANSION_RANDOM_COUNT',
@@ -42,7 +42,7 @@ module.exports = class SimpleRestContainer {
42
42
  return qr[0]
43
43
  })
44
44
  for (const event of sortedEvents) {
45
- setTimeout(() => this._processBodyAsync(event.body, true, !!this.caps[Capabilities.SIMPLEREST_INBOUND_UPDATE_CONTEXT]), 0)
45
+ setTimeout(() => this._processBodyAsync(event.body, event.headers, true, !!this.caps[Capabilities.SIMPLEREST_INBOUND_UPDATE_CONTEXT]), 0)
46
46
  }
47
47
  }, debounceTimeout)
48
48
  }
@@ -142,13 +142,13 @@ module.exports = class SimpleRestContainer {
142
142
  (pingComplete) => {
143
143
  if (this.caps[Capabilities.SIMPLEREST_PING_URL]) {
144
144
  this._makeCall('SIMPLEREST_PING')
145
- .then(body => {
145
+ .then(({ body, headers }) => {
146
146
  if (this.caps[Capabilities.SIMPLEREST_PING_UPDATE_CONTEXT] || this.caps[Capabilities.SIMPLEREST_PING_PROCESS_RESPONSE]) {
147
147
  return this._parseResponseBody(body)
148
148
  .then(body => {
149
149
  if (body) {
150
150
  debug(`Ping Uri ${this.caps[Capabilities.SIMPLEREST_PING_URL]} returned JSON response: ${botiumUtils.shortenJsonString(body)}`)
151
- return this._processBodyAsync(body, !!this.caps[Capabilities.SIMPLEREST_PING_PROCESS_RESPONSE], !!this.caps[Capabilities.SIMPLEREST_PING_UPDATE_CONTEXT])
151
+ return this._processBodyAsync(body, headers, !!this.caps[Capabilities.SIMPLEREST_PING_PROCESS_RESPONSE], !!this.caps[Capabilities.SIMPLEREST_PING_UPDATE_CONTEXT])
152
152
  } else {
153
153
  debug(`Ping Uri ${this.caps[Capabilities.SIMPLEREST_PING_URL]} didn't return JSON response, ignoring it.`)
154
154
  }
@@ -178,13 +178,13 @@ module.exports = class SimpleRestContainer {
178
178
  this.processInbound = true
179
179
  if (this.caps[Capabilities.SIMPLEREST_START_URL]) {
180
180
  this._makeCall('SIMPLEREST_START')
181
- .then(body => {
181
+ .then(({ body, headers }) => {
182
182
  if (this.caps[Capabilities.SIMPLEREST_START_UPDATE_CONTEXT] || this.caps[Capabilities.SIMPLEREST_START_PROCESS_RESPONSE]) {
183
183
  return this._parseResponseBody(body)
184
184
  .then(body => {
185
185
  if (body) {
186
186
  debug(`Start Uri ${this.caps[Capabilities.SIMPLEREST_START_URL]} returned JSON response: ${botiumUtils.shortenJsonString(body)}`)
187
- return this._processBodyAsync(body, !!this.caps[Capabilities.SIMPLEREST_START_PROCESS_RESPONSE], !!this.caps[Capabilities.SIMPLEREST_START_UPDATE_CONTEXT])
187
+ return this._processBodyAsync(body, headers, !!this.caps[Capabilities.SIMPLEREST_START_PROCESS_RESPONSE], !!this.caps[Capabilities.SIMPLEREST_START_UPDATE_CONTEXT])
188
188
  } else {
189
189
  debug(`Start Uri ${this.caps[Capabilities.SIMPLEREST_START_URL]} didn't return JSON response, ignoring it.`)
190
190
  }
@@ -235,10 +235,10 @@ module.exports = class SimpleRestContainer {
235
235
  }
236
236
 
237
237
  // Separated just for better module testing
238
- async _processBodyAsync (body, isFromUser, updateContext) {
238
+ async _processBodyAsync (body, headers, isFromUser, updateContext) {
239
239
  const p = async () => {
240
240
  try {
241
- const results = await this._processBodyAsyncImpl(body, isFromUser, updateContext)
241
+ const results = await this._processBodyAsyncImpl(body, headers, isFromUser, updateContext)
242
242
  if (results) {
243
243
  for (const result of results) {
244
244
  setTimeout(() => this.queueBotSays(result), 0)
@@ -266,7 +266,8 @@ module.exports = class SimpleRestContainer {
266
266
  }
267
267
 
268
268
  // Separated just for better module testing
269
- async _processBodyAsyncImpl (body, isFromUser, updateContext) {
269
+ async _processBodyAsyncImpl (body, headers, isFromUser, updateContext) {
270
+ this.view.response = { body, headers }
270
271
  if (updateContext) {
271
272
  const mergeMode = this.caps[Capabilities.SIMPLEREST_CONTEXT_MERGE_OR_REPLACE]
272
273
  const jsonPathsContext = getAllCapValues(Capabilities.SIMPLEREST_CONTEXT_JSONPATH, this.caps)
@@ -451,7 +452,7 @@ module.exports = class SimpleRestContainer {
451
452
  }
452
453
 
453
454
  if (body) {
454
- debug(`got response code: ${response.statusCode}, body: ${botiumUtils.shortenJsonString(body)}`)
455
+ debug(`got response code: ${response.statusCode}, body: ${botiumUtils.shortenJsonString(body)}, headers: ${botiumUtils.shortenJsonString(response.headers)}`)
455
456
  this._storeCookiesFromResponse(response)
456
457
  try {
457
458
  body = await this._parseResponseBody(body)
@@ -463,7 +464,7 @@ module.exports = class SimpleRestContainer {
463
464
  }
464
465
 
465
466
  if (body) {
466
- this._processBodyAsync(body, isFromUser, updateContext).then(() => resolve(this)).then(() => this._emptyWaitProcessQueue())
467
+ this._processBodyAsync(body, response.headers, isFromUser, updateContext).then(() => resolve(this)).then(() => this._emptyWaitProcessQueue())
467
468
  } else {
468
469
  debug('ignoring response body (no string and no JSON object)')
469
470
  resolve(this)
@@ -599,9 +600,9 @@ module.exports = class SimpleRestContainer {
599
600
  debug(`_waitForUrlResponse success on url check ${pingConfig.uri}: ${response.statusCode}/${response.statusMessage}`)
600
601
  this._storeCookiesFromResponse(response)
601
602
  if (debug.enabled && body) {
602
- debug(botiumUtils.shortenJsonString(body))
603
+ debug(`body: ${botiumUtils.shortenJsonString(body)}, headers: ${botiumUtils.shortenJsonString(response.headers)}`)
603
604
  }
604
- return body
605
+ return { body, headers: response.headers }
605
606
  }
606
607
  }
607
608
  }
@@ -638,7 +639,10 @@ module.exports = class SimpleRestContainer {
638
639
  try {
639
640
  return JSON.parse(raw)
640
641
  } catch (err) {
641
- return new Error(`JSON parsing failed - try to use {{#fnc.jsonify}}{{xxx}}{{/fnc.jsonify}} to escape JSON special characters (ERR: ${err.message})`)
642
+ if (debug.enabled) {
643
+ debug(`JSON parsing failed (${err.message}) for: ${botiumUtils.shortenJsonString(raw)}`)
644
+ }
645
+ throw new Error(`JSON parsing failed - try to use {{#fnc.jsonify}}{{xxx}}{{/fnc.jsonify}} to escape JSON special characters (ERR: ${err.message})`)
642
646
  }
643
647
  } else {
644
648
  return raw
@@ -676,7 +680,7 @@ module.exports = class SimpleRestContainer {
676
680
  this.inboundEvents.push(event)
677
681
  this._processOrderedInboundEventsArrayAsync()
678
682
  } else {
679
- setTimeout(() => this._processBodyAsync(event.body, true, !!this.caps[Capabilities.SIMPLEREST_INBOUND_UPDATE_CONTEXT]), 0)
683
+ setTimeout(() => this._processBodyAsync(event.body, event.headers, true, !!this.caps[Capabilities.SIMPLEREST_INBOUND_UPDATE_CONTEXT]), 0)
680
684
  }
681
685
  }
682
686
 
@@ -800,7 +804,7 @@ module.exports = class SimpleRestContainer {
800
804
  debug(botiumUtils.shortenJsonString(body))
801
805
  }
802
806
  } else if (body) {
803
- debug(`_runPolling: got response code: ${response.statusCode}, body: ${botiumUtils.shortenJsonString(body)}`)
807
+ debug(`_runPolling: got response code: ${response.statusCode}, body: ${botiumUtils.shortenJsonString(body)}, headers: ${botiumUtils.shortenJsonString(response.headers)}`)
804
808
  this._storeCookiesFromResponse(response)
805
809
  try {
806
810
  body = await this._parseResponseBody(body)
@@ -809,7 +813,7 @@ module.exports = class SimpleRestContainer {
809
813
  return
810
814
  }
811
815
  if (body) {
812
- setTimeout(() => this._processBodyAsync(body, true, !!this.caps[Capabilities.SIMPLEREST_POLL_UPDATE_CONTEXT]), 0)
816
+ setTimeout(() => this._processBodyAsync(body, response.headers, true, !!this.caps[Capabilities.SIMPLEREST_POLL_UPDATE_CONTEXT]), 0)
813
817
  } else {
814
818
  debug('_runPolling: ignoring response body (no string and no JSON object)')
815
819
  }
@@ -29,7 +29,8 @@ const setupEndpoints = ({ app, endpoint, middleware, processEvent }) => {
29
29
  processEvent({
30
30
  originalUrl: req.originalUrl,
31
31
  originalMethod: req.method,
32
- body: req.body
32
+ body: req.body,
33
+ headers: req.headers
33
34
  })
34
35
  res.status(200).json({}).end()
35
36
  } else {
@@ -91,9 +91,9 @@ class ConvoStep {
91
91
  '#' + this.sender +
92
92
  ' - ' + (this.optional ? '?' : '') + (this.not ? '!' : '') +
93
93
  (this.messageText || '') +
94
- (this.asserters && this.asserters.length > 0 ? ' ' + this.asserters.map(a => a.toString()).join(' ASS: ') : '') +
95
- (this.logicHooks && this.logicHooks.length > 0 ? ' ' + this.logicHooks.map(l => l.toString()).join(' LH: ') : '') +
96
- (this.userInputs && this.userInputs.length > 0 ? ' ' + this.userInputs.map(u => u.toString()).join(' UI: ') : '')
94
+ (this.asserters && this.asserters.length > 0 ? ' ' + this.asserters.map(a => a.toString()).join(' ') : '') +
95
+ (this.logicHooks && this.logicHooks.length > 0 ? ' ' + this.logicHooks.map(l => l.toString()).join(' ') : '') +
96
+ (this.userInputs && this.userInputs.length > 0 ? ' ' + this.userInputs.map(u => u.toString()).join(' ') : '')
97
97
  }
98
98
  }
99
99
 
@@ -119,7 +119,7 @@ class Transcript {
119
119
  }
120
120
  }
121
121
 
122
- class TranscriptAttachment { // eslint-disable-line no-unused-vars
122
+ class TranscriptAttachment {
123
123
  constructor (fromJson = {}) {
124
124
  this.name = fromJson.name
125
125
  this.mimeType = fromJson.mimeType
@@ -714,9 +714,15 @@ class Convo {
714
714
  }
715
715
  }
716
716
 
717
- module
718
- .exports = {
719
- ConvoHeader,
720
- Convo,
721
- ConvoStep
722
- }
717
+ module.exports = {
718
+ Convo,
719
+ ConvoHeader,
720
+ ConvoStep,
721
+ ConvoStepAssert,
722
+ ConvoStepLogicHook,
723
+ ConvoStepUserInput,
724
+ Transcript,
725
+ TranscriptAttachment,
726
+ TranscriptStep,
727
+ TranscriptError
728
+ }
@@ -1,4 +1,5 @@
1
1
  const _ = require('lodash')
2
+ const speechScorer = require('word-error-rate')
2
3
 
3
4
  const { toString, quoteRegexpString } = require('./helper')
4
5
 
@@ -79,6 +80,12 @@ const equals = (ignoreCase) => (botresponse, utterance) => {
79
80
  return botresponse === utterance
80
81
  }
81
82
 
83
+ const wer = () => (botresponse, utterance, args) => {
84
+ botresponse = _normalize(botresponse || '')
85
+ utterance = toString(utterance || '')
86
+ return speechScorer.wordErrorRate(botresponse, utterance) <= args[0]
87
+ }
88
+
82
89
  const getMatchFunction = (matchingMode) => {
83
90
  if (matchingMode === 'regexp' || matchingMode === 'regexpIgnoreCase') {
84
91
  return regexp(matchingMode === 'regexpIgnoreCase')
@@ -90,6 +97,8 @@ const getMatchFunction = (matchingMode) => {
90
97
  return include(matchingMode === 'includeIgnoreCase' || matchingMode === 'includeLowerCase')
91
98
  } else if (matchingMode === 'equals' || matchingMode === 'equalsIgnoreCase') {
92
99
  return equals(matchingMode === 'equalsIgnoreCase')
100
+ } else if (matchingMode === 'wer') {
101
+ return wer()
93
102
  } else {
94
103
  return equals(false)
95
104
  }
@@ -101,5 +110,6 @@ module.exports = {
101
110
  wildcardExact,
102
111
  include,
103
112
  equals,
113
+ wer,
104
114
  getMatchFunction
105
115
  }
@@ -134,7 +134,8 @@ module.exports = class ScriptingProvider {
134
134
  tomatch = [tomatch]
135
135
  }
136
136
  debug(`assertBotResponse ${stepTag} ${meMsg ? `(${meMsg}) ` : ''}BOT: ${botresponse} = ${tomatch} ...`)
137
- const found = _.find(tomatch, (utt) => this.matchFn(botresponse, utt))
137
+ const found = _.find(tomatch, (utt) => this.matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]))
138
+ const asserterType = this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer' ? 'WerAsserter' : 'TextMatchAsserter'
138
139
  if (_.isNil(found)) {
139
140
  let message = `${stepTag}: Bot response `
140
141
  message += meMsg ? `(on ${meMsg}) ` : ''
@@ -146,7 +147,7 @@ module.exports = class ScriptingProvider {
146
147
  message,
147
148
  {
148
149
  type: 'asserter',
149
- source: 'TextMatchAsserter',
150
+ source: asserterType,
150
151
  context: {
151
152
  stepTag
152
153
  },
@@ -164,7 +165,8 @@ module.exports = class ScriptingProvider {
164
165
  nottomatch = [nottomatch]
165
166
  }
166
167
  debug(`assertBotNotResponse ${stepTag} ${meMsg ? `(${meMsg}) ` : ''}BOT: ${botresponse} != ${nottomatch} ...`)
167
- const found = _.find(nottomatch, (utt) => this.matchFn(botresponse, utt))
168
+ const found = _.find(nottomatch, (utt) => this.matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]))
169
+ const asserterType = this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer' ? 'WerAsserter' : 'TextMatchAsserter'
168
170
  if (!_.isNil(found)) {
169
171
  let message = `${stepTag}: Bot response `
170
172
  message += meMsg ? `(on ${meMsg}) ` : ''
@@ -176,7 +178,7 @@ module.exports = class ScriptingProvider {
176
178
  message,
177
179
  {
178
180
  type: 'asserter',
179
- source: 'TextMatchAsserter',
181
+ source: asserterType,
180
182
  context: {
181
183
  stepTag
182
184
  },
@@ -401,7 +403,7 @@ module.exports = class ScriptingProvider {
401
403
  }
402
404
 
403
405
  Match (botresponse, utterance) {
404
- return this.matchFn(botresponse, utterance)
406
+ return this.matchFn(botresponse, utterance, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS])
405
407
  }
406
408
 
407
409
  Compile (scriptBuffer, scriptFormat, scriptType) {
@@ -859,9 +861,10 @@ module.exports = class ScriptingProvider {
859
861
  * @param currentConvo
860
862
  * @param convoStepIndex
861
863
  * @param convoStepsStack list of ConvoSteps
864
+ * @param context {width: }
862
865
  * @private
863
866
  */
864
- _expandConvo (expandedConvos, currentConvo, convoStepIndex = 0, convoStepsStack = []) {
867
+ _expandConvo (expandedConvos, currentConvo, convoStepIndex = 0, convoStepsStack = [], context = {}) {
865
868
  const utterancePostfix = (lineTag, uttOrUserInput) => {
866
869
  const naming = this.caps[Capabilities.SCRIPTING_UTTEXPANSION_NAMING_MODE] || Defaults.capabilities[Capabilities.SCRIPTING_UTTEXPANSION_NAMING_MODE]
867
870
  if (naming === 'justLineTag') {
@@ -881,7 +884,7 @@ module.exports = class ScriptingProvider {
881
884
  if (currentStep.sender === 'bot' || currentStep.sender === 'begin' || currentStep.sender === 'end') {
882
885
  const currentStepsStack = convoStepsStack.slice()
883
886
  currentStepsStack.push(_.cloneDeep(currentStep))
884
- this._expandConvo(expandedConvos, currentConvo, convoStepIndex + 1, currentStepsStack)
887
+ this._expandConvo(expandedConvos, currentConvo, convoStepIndex + 1, currentStepsStack, context)
885
888
  } else if (currentStep.sender === 'me') {
886
889
  let useUnexpanded = true
887
890
  if (currentStep.messageText) {
@@ -898,29 +901,61 @@ module.exports = class ScriptingProvider {
898
901
  }
899
902
  if (this.utterances[uttName]) {
900
903
  const allutterances = this.utterances[uttName].utterances
901
- let sampleutterances = allutterances
902
- if (this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE] === 'first') {
903
- sampleutterances = [allutterances[0]]
904
- } else if (this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE] === 'random') {
905
- sampleutterances = allutterances
906
- .map(x => ({ x, r: Math.random() }))
907
- .sort((a, b) => a.r - b.r)
908
- .map(a => a.x)
909
- .slice(0, this.caps[Capabilities.SCRIPTING_UTTEXPANSION_RANDOM_COUNT])
904
+ const processSampleUtterances = (sampleutterances, myContext) => {
905
+ sampleutterances.forEach((utt, index) => {
906
+ processSampleUtterance(utt, sampleutterances.length, index, Object.assign({ indexExpansionModeIndex: index }, myContext || context))
907
+ })
910
908
  }
911
- sampleutterances.forEach((utt, index) => {
912
- const lineTag = `${index + 1}`.padStart(`${sampleutterances.length}`.length, '0')
909
+ const processSampleUtterance = (sampleutterance, length, index, myContext) => {
910
+ const lineTag = `${index + 1}`.padStart(`${length}`.length, '0')
913
911
  const currentStepsStack = convoStepsStack.slice()
914
912
  if (uttArgs) {
915
- utt = util.format(utt, ...uttArgs)
913
+ sampleutterance = util.format(sampleutterance, ...uttArgs)
916
914
  }
917
- currentStepsStack.push(Object.assign(_.cloneDeep(currentStep), { messageText: utt }))
915
+ currentStepsStack.push(Object.assign(_.cloneDeep(currentStep), { messageText: sampleutterance }))
918
916
  const currentConvoLabeled = _.cloneDeep(currentConvo)
919
- Object.assign(currentConvoLabeled.header, { name: `${currentConvo.header.name}/${uttName}-${utterancePostfix(lineTag, utt)}` })
917
+ Object.assign(currentConvoLabeled.header, { name: `${currentConvo.header.name}/${uttName}-${utterancePostfix(lineTag, sampleutterance)}` })
920
918
  if (!currentConvoLabeled.sourceTag) currentConvoLabeled.sourceTag = {}
921
919
  if (!currentConvoLabeled.sourceTag.origConvoName) currentConvoLabeled.sourceTag.origConvoName = currentConvo.header.name
922
- this._expandConvo(expandedConvos, currentConvoLabeled, convoStepIndex + 1, currentStepsStack)
923
- })
920
+ this._expandConvo(expandedConvos, currentConvoLabeled, convoStepIndex + 1, currentStepsStack, myContext || context)
921
+ }
922
+ if (this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE] === 'index') {
923
+ if (_.isNil(context.indexExpansionModeWidth)) {
924
+ // executed for the first found utterance
925
+ processSampleUtterances(allutterances, Object.assign({}, context, { indexExpansionModeWidth: allutterances.length }))
926
+ } else {
927
+ if (_.isNil(context.indexExpansionModeIndex)) {
928
+ throw new Error('indexExpansionModeIndex must be set!')
929
+ }
930
+ // executing the current 'thread', if current utterance has no example to current index, fallback to the last one
931
+ const localIndex = Math.min(context.indexExpansionModeIndex, allutterances.length - 1)
932
+ if (localIndex < context.indexExpansionModeIndex && context.indexExpansionModeIndex === context.indexExpansionModeWidth - 1) {
933
+ debug(`While expanding convos by index found in utterance "${uttName}" less examples (${allutterances.length}) as expected (${context.indexExpansionModeWidth})`)
934
+ }
935
+ const myContext = Object.assign({}, context, { indexExpansionModeWidth: Math.max(allutterances.length, context.indexExpansionModeWidth) })
936
+ processSampleUtterance(allutterances[localIndex], allutterances.length, localIndex, myContext)
937
+ if (allutterances.length > context.indexExpansionModeWidth && context.indexExpansionModeIndex + 1 === context.indexExpansionModeWidth) {
938
+ debug(`While expanding convos by index found in utterance "${uttName}" more examples (${allutterances.length}) as expected (${context.indexExpansionModeWidth})`)
939
+ for (let i = context.indexExpansionModeWidth; i < allutterances.length; i++) {
940
+ // if we found a utterance with more examples as any utterances before, we have to start new 'thread'
941
+ const myContext = Object.assign({}, context, { indexExpansionModeWidth: allutterances.length, indexExpansionModeIndex: i })
942
+ processSampleUtterance(allutterances[i], allutterances.length, i, myContext)
943
+ }
944
+ }
945
+ }
946
+ } else {
947
+ if (this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE] === 'first') {
948
+ processSampleUtterances([allutterances[0]])
949
+ } else if (this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE] === 'random') {
950
+ processSampleUtterances(allutterances
951
+ .map(x => ({ x, r: Math.random() }))
952
+ .sort((a, b) => a.r - b.r)
953
+ .map(a => a.x)
954
+ .slice(0, this.caps[Capabilities.SCRIPTING_UTTEXPANSION_RANDOM_COUNT]))
955
+ } else {
956
+ processSampleUtterances(allutterances)
957
+ }
958
+ }
924
959
  useUnexpanded = false
925
960
  }
926
961
  }
@@ -930,18 +965,14 @@ module.exports = class ScriptingProvider {
930
965
  if (userInput && userInput.expandConvo) {
931
966
  const expandedUserInputs = userInput.expandConvo({ convo: currentConvo, convoStep: currentStep, args: ui.args })
932
967
  if (expandedUserInputs && expandedUserInputs.length > 0) {
933
- let sampleinputs = expandedUserInputs
934
- if (this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE] === 'first') {
935
- sampleinputs = [expandedUserInputs[0]]
936
- } else if (this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE] === 'random') {
937
- sampleinputs = expandedUserInputs
938
- .map(x => ({ x, r: Math.random() }))
939
- .sort((a, b) => a.r - b.r)
940
- .map(a => a.x)
941
- .slice(0, this.caps[Capabilities.SCRIPTING_UTTEXPANSION_RANDOM_COUNT])
968
+ // let sampleinputs = expandedUserInputs
969
+ const processSampleInputs = (sampleinputs, myContext, uiIndex) => {
970
+ sampleinputs.forEach((input, index) => {
971
+ processSampleInput(input, sampleinputs.length, index, Object.assign({ indexExpansionModeIndex: index }, myContext || context), uiIndex)
972
+ })
942
973
  }
943
- sampleinputs.forEach((sampleinput, index) => {
944
- const lineTag = `${index + 1}`.padStart(`${sampleinputs.length}`.length, '0')
974
+ const processSampleInput = (sampleinput, length, index, myContext, uiIndex) => {
975
+ const lineTag = `${index + 1}`.padStart(`${length}`.length, '0')
945
976
  const currentStepsStack = convoStepsStack.slice()
946
977
  const currentStepMod = _.cloneDeep(currentStep)
947
978
  currentStepMod.userInputs[uiIndex] = sampleinput
@@ -949,8 +980,46 @@ module.exports = class ScriptingProvider {
949
980
  currentStepsStack.push(currentStepMod)
950
981
  const currentConvoLabeled = _.cloneDeep(currentConvo)
951
982
  Object.assign(currentConvoLabeled.header, { name: `${currentConvo.header.name}/${ui.name}-${utterancePostfix(lineTag, (sampleinput.args && sampleinput.args.length) ? sampleinput.args.join(', ') : 'no-args')}` })
952
- this._expandConvo(expandedConvos, currentConvoLabeled, convoStepIndex + 1, currentStepsStack)
953
- })
983
+ this._expandConvo(expandedConvos, currentConvoLabeled, convoStepIndex + 1, currentStepsStack, myContext || context)
984
+ }
985
+ if (this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE] === 'index') {
986
+ if (_.isNil(context.indexExpansionModeWidth)) {
987
+ processSampleInputs(expandedUserInputs, Object.assign({}, context, { indexExpansionModeWidth: expandedUserInputs.length }), uiIndex)
988
+ } else {
989
+ if (_.isNil(context.indexExpansionModeIndex)) {
990
+ throw new Error('indexExpansionModeIndex must be set!')
991
+ }
992
+ // executing the current 'thread', if current utterance has no example to current index, fallback to the last one
993
+ const localIndex = Math.min(context.indexExpansionModeIndex, expandedUserInputs.length - 1)
994
+ if (localIndex < context.indexExpansionModeIndex && context.indexExpansionModeIndex === context.indexExpansionModeWidth - 1) {
995
+ debug(`While expanding convos by index found user input "${ui.name}, ${ui.args}" less examples (${expandedUserInputs.length}) as expected (${context.indexExpansionModeWidth})`)
996
+ }
997
+ const myContext = Object.assign({}, context, { indexExpansionModeWidth: Math.max(expandedUserInputs.length, context.indexExpansionModeWidth) })
998
+ processSampleInput(expandedUserInputs[localIndex], expandedUserInputs.length, localIndex, myContext, uiIndex)
999
+ if (expandedUserInputs.length > context.indexExpansionModeWidth && context.indexExpansionModeIndex + 1 === context.indexExpansionModeWidth) {
1000
+ debug(`While expanding convos by index found user input "${ui.name}, ${ui.args}" more examples (${expandedUserInputs.length}) as expected (${context.indexExpansionModeWidth})`)
1001
+ for (let i = context.indexExpansionModeWidth; i < expandedUserInputs.length; i++) {
1002
+ const myContext = Object.assign({}, context, { indexExpansionModeWidth: expandedUserInputs.length, indexExpansionModeIndex: i })
1003
+ processSampleInput(expandedUserInputs[i], expandedUserInputs.length, i, myContext, uiIndex)
1004
+ }
1005
+ }
1006
+ }
1007
+ } else {
1008
+ if (this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE] === 'first') {
1009
+ processSampleInputs([expandedUserInputs[0]], context, uiIndex)
1010
+ } else if (this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE] === 'random') {
1011
+ processSampleInputs(expandedUserInputs
1012
+ .map(x => ({
1013
+ x,
1014
+ r: Math.random()
1015
+ }))
1016
+ .sort((a, b) => a.r - b.r)
1017
+ .map(a => a.x)
1018
+ .slice(0, this.caps[Capabilities.SCRIPTING_UTTEXPANSION_RANDOM_COUNT]), context, uiIndex)
1019
+ } else {
1020
+ processSampleInputs(expandedUserInputs, context, uiIndex)
1021
+ }
1022
+ }
954
1023
  useUnexpanded = false
955
1024
  }
956
1025
  }
@@ -959,7 +1028,7 @@ module.exports = class ScriptingProvider {
959
1028
  if (useUnexpanded) {
960
1029
  const currentStepsStack = convoStepsStack.slice()
961
1030
  currentStepsStack.push(_.cloneDeep(currentStep))
962
- this._expandConvo(expandedConvos, currentConvo, convoStepIndex + 1, currentStepsStack)
1031
+ this._expandConvo(expandedConvos, currentConvo, convoStepIndex + 1, currentStepsStack, context)
963
1032
  }
964
1033
  }
965
1034
  } else {
@@ -45,7 +45,7 @@ module.exports = {
45
45
  { name: 'TEXT_EQUALS_IC', className: 'TextEqualsAnyICAsserter' },
46
46
  { name: 'TEXT', className: 'TextEqualsAnyAsserter' },
47
47
  { name: 'TEXT_IC', className: 'TextEqualsAnyICAsserter' },
48
-
48
+ { name: 'TEXT_WER', className: 'WerAsserter' },
49
49
  { name: 'BOT_CONSUMED', className: 'BotRepliesConsumedAsserter' },
50
50
  { name: 'BOT_UNCONSUMED_COUNT', className: 'BotRepliesUnconsumedCountAsserter' }
51
51
  ],
@@ -0,0 +1,59 @@
1
+ // const _ = require('lodash')
2
+ const speechScorer = require('word-error-rate')
3
+ const { BotiumError } = require('../../BotiumError')
4
+
5
+ module.exports = class WerAsserter {
6
+ constructor (context, caps = {}) {
7
+ this.context = context
8
+ this.caps = caps
9
+ this.name = 'WerAsserter'
10
+ }
11
+
12
+ assertConvoStep ({ convo, convoStep, args, botMsg }) {
13
+ if (!args || args.length < 1) {
14
+ return Promise.reject(new BotiumError(`${convoStep.stepTag}: WerAsserter Missing argument`,
15
+ {
16
+ type: 'asserter',
17
+ subtype: 'wrong parameters',
18
+ source: this.name,
19
+ cause: { args }
20
+ }
21
+ ))
22
+ }
23
+ if (args.length > 2) {
24
+ return Promise.reject(new BotiumError(`${convoStep.stepTag}: WerAsserter Too much argument "${args}"`,
25
+ {
26
+ type: 'asserter',
27
+ subtype: 'wrong parameters',
28
+ source: this.name,
29
+ cause: { args }
30
+ }
31
+ ))
32
+ }
33
+
34
+ const utterance = args[0]
35
+ const threshold = args[1]
36
+
37
+ const wer = speechScorer.wordErrorRate(botMsg.messageText, utterance)
38
+ if (wer > threshold) {
39
+ return Promise.reject(new BotiumError(
40
+ `${convoStep.stepTag}: Word error rate ${wer} > ${threshold} for ${utterance}`,
41
+ {
42
+ type: 'asserter',
43
+ source: this.name,
44
+ context: {
45
+ params: {
46
+ args
47
+ }
48
+ },
49
+ cause: {
50
+ expected: `Word error rate <= ${threshold}`,
51
+ actual: `Word error rate = ${wer}`
52
+ }
53
+ }
54
+ ))
55
+ }
56
+
57
+ return Promise.resolve()
58
+ }
59
+ }
@@ -1,5 +1,6 @@
1
1
  const util = require('util')
2
2
  const _ = require('lodash')
3
+ const { ConvoStepLogicHook } = require('../../Convo')
3
4
 
4
5
  module.exports = class UpdateCustomLogicHook {
5
6
  constructor (context, caps = {}, globalArgs = {}) {
@@ -14,10 +15,10 @@ module.exports = class UpdateCustomLogicHook {
14
15
  const validConvoSteps = convo.conversation.filter(s => s.sender === 'me')
15
16
  for (const convoStep of validConvoSteps) {
16
17
  convoStep.logicHooks = (convoStep.logicHooks || [])
17
- convoStep.logicHooks.push({
18
+ convoStep.logicHooks.push(new ConvoStepLogicHook({
18
19
  name: 'UPDATE_CUSTOM',
19
20
  args
20
- })
21
+ }))
21
22
  }
22
23
  }
23
24