botium-core 1.13.0 → 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 (57) hide show
  1. package/.eslintrc.js +6 -3
  2. package/dist/botium-cjs.js +305 -123
  3. package/dist/botium-cjs.js.map +1 -1
  4. package/dist/botium-es.js +323 -142
  5. package/dist/botium-es.js.map +1 -1
  6. package/package.json +17 -15
  7. package/src/Capabilities.js +2 -1
  8. package/src/containers/plugins/SimpleRestContainer.js +23 -16
  9. package/src/grid/inbound/proxy.js +2 -1
  10. package/src/helpers/RetryHelper.js +13 -7
  11. package/src/scripting/Convo.js +36 -10
  12. package/src/scripting/MatchFunctions.js +10 -0
  13. package/src/scripting/ScriptingProvider.js +106 -37
  14. package/src/scripting/logichook/LogicHookConsts.js +1 -1
  15. package/src/scripting/logichook/LogicHookUtils.js +1 -1
  16. package/src/scripting/logichook/asserter/WerAsserter.js +59 -0
  17. package/src/scripting/logichook/logichooks/UpdateCustomLogicHook.js +3 -2
  18. package/test/compiler/compilercsv.spec.js +104 -3
  19. package/test/compiler/compilerjson.spec.js +0 -2
  20. package/test/compiler/compilerxlsx.spec.js +1 -1
  21. package/test/compiler/convos/csv/utterances_liveperson2.csv +12 -0
  22. package/test/connectors/simplerest.spec.js +1012 -969
  23. package/test/convo/fillAndApplyScriptingMemory.spec.js +804 -785
  24. package/test/convo/partialconvo.spec.js +345 -339
  25. package/test/convo/retryconvo.spec.js +134 -0
  26. package/test/driver/capabilities.spec.js +156 -151
  27. package/test/logichooks/hookfromsrc.spec.js +79 -73
  28. package/test/plugins/plugins.spec.js +44 -42
  29. package/test/scripting/asserters/buttonsAsserter.spec.js +257 -240
  30. package/test/scripting/asserters/cardsAsserter.spec.js +214 -212
  31. package/test/scripting/asserters/convos/wer_threshold_nok.yml +7 -0
  32. package/test/scripting/asserters/convos/wer_threshold_ok.yml +7 -0
  33. package/test/scripting/asserters/intentConfidenceAsserter.spec.js +34 -35
  34. package/test/scripting/asserters/jsonpathAsserter.spec.js +307 -308
  35. package/test/scripting/asserters/mediaAsserter.spec.js +236 -234
  36. package/test/scripting/asserters/werAsserter.spec.js +51 -0
  37. package/test/scripting/logichooks/setClearScriptingMemory.spec.js +202 -192
  38. package/test/scripting/matching/matchingmode.spec.js +306 -258
  39. package/test/scripting/scriptingProvider.spec.js +666 -633
  40. package/test/scripting/scriptingmemory/fillScriptingMemoryFromFile.spec.js +299 -281
  41. package/test/scripting/scriptingmemory/useScriptingMemoryForAssertion.spec.js +94 -80
  42. package/test/scripting/userinputs/defaultUserInputs.spec.js +233 -127
  43. package/test/scripting/userinputs/mediaInputConvos.spec.js +409 -403
  44. package/test/scripting/utteranceexpansion/associateByIndex.spec.js +259 -0
  45. package/test/scripting/utteranceexpansion/convos/associate_utterances_by_index.json +33 -0
  46. package/test/scripting/utteranceexpansion/convos/media.convo.txt +19 -0
  47. package/test/scripting/utteranceexpansion/files/step0voice0.wav +0 -0
  48. package/test/scripting/utteranceexpansion/files/step0voice1.wav +0 -0
  49. package/test/scripting/utteranceexpansion/files/step0voice2.wav +0 -0
  50. package/test/scripting/utteranceexpansion/files/step1voice0.wav +0 -0
  51. package/test/scripting/utteranceexpansion/files/step2voice0.wav +0 -0
  52. package/test/scripting/utteranceexpansion/files/step2voice1.wav +0 -0
  53. package/test/scripting/utteranceexpansion/files/step2voice2.wav +0 -0
  54. package/test/scripting/utteranceexpansion/files/step2voice4.wav +0 -0
  55. package/test/scripting/utteranceexpansion/files/step2voice5.wav +0 -0
  56. package/test/security/allowUnsafe.spec.js +274 -268
  57. 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.0",
3
+ "version": "1.13.3",
4
4
  "description": "The Selenium for Chatbots",
5
5
  "main": "index.js",
6
6
  "module": "dist/botium-es.js",
@@ -32,17 +32,17 @@
32
32
  },
33
33
  "homepage": "https://www.botium.ai",
34
34
  "dependencies": {
35
- "@babel/runtime": "^7.18.3",
35
+ "@babel/runtime": "^7.18.6",
36
36
  "async": "^3.2.4",
37
37
  "body-parser": "^1.20.0",
38
38
  "boolean": "^3.2.0",
39
39
  "bottleneck": "^2.19.5",
40
- "csv-parse": "^5.2.0",
40
+ "csv-parse": "^5.3.0",
41
41
  "debug": "^4.3.4",
42
42
  "esprima": "^4.0.1",
43
43
  "express": "^4.18.1",
44
44
  "globby": "11.0.4",
45
- "ioredis": "^5.0.6",
45
+ "ioredis": "^5.1.0",
46
46
  "is-class": "^0.0.9",
47
47
  "is-json": "^2.0.1",
48
48
  "jsonpath": "^1.1.1",
@@ -50,7 +50,7 @@
50
50
  "markdown-it": "^13.0.1",
51
51
  "mime-types": "^2.1.35",
52
52
  "mkdirp": "^1.0.4",
53
- "moment": "^2.29.3",
53
+ "moment": "^2.29.4",
54
54
  "mustache": "^4.2.0",
55
55
  "promise-retry": "^2.0.1",
56
56
  "promise.allsettled": "^1.0.5",
@@ -65,33 +65,35 @@
65
65
  "swagger-jsdoc": "^6.2.1",
66
66
  "swagger-ui-express": "^4.4.0",
67
67
  "uuid": "^8.3.2",
68
- "vm2": "^3.9.9",
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",
72
73
  "yaml": "^2.1.1"
73
74
  },
74
75
  "devDependencies": {
75
- "@babel/core": "^7.18.5",
76
- "@babel/node": "^7.18.5",
77
- "@babel/plugin-transform-runtime": "^7.18.5",
78
- "@babel/preset-env": "^7.18.2",
76
+ "@babel/core": "^7.18.6",
77
+ "@babel/node": "^7.18.6",
78
+ "@babel/plugin-transform-runtime": "^7.18.6",
79
+ "@babel/preset-env": "^7.18.6",
79
80
  "chai": "^4.3.6",
80
81
  "chai-as-promised": "^7.1.1",
81
82
  "cross-env": "^7.0.3",
82
- "eslint": "^8.18.0",
83
+ "eslint": "^8.19.0",
83
84
  "eslint-config-standard": "^17.0.0",
84
85
  "eslint-plugin-import": "^2.26.0",
85
- "eslint-plugin-n": "^15.2.3",
86
+ "eslint-plugin-mocha": "^10.1.0",
87
+ "eslint-plugin-n": "^15.2.4",
86
88
  "eslint-plugin-promise": "^6.0.0",
87
89
  "eslint-plugin-standard": "^4.1.0",
88
90
  "license-checker": "^25.0.1",
89
91
  "license-compatibility-checker": "^0.3.5",
90
92
  "mocha": "^10.0.0",
91
- "nock": "^13.2.7",
92
- "npm-check-updates": "^14.0.1",
93
+ "nock": "^13.2.8",
94
+ "npm-check-updates": "^15.2.6",
93
95
  "nyc": "^15.1.0",
94
- "rollup": "^2.75.6",
96
+ "rollup": "^2.76.0",
95
97
  "rollup-plugin-babel": "^4.4.0",
96
98
  "rollup-plugin-commonjs": "^10.1.0",
97
99
  "rollup-plugin-json": "^4.0.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)
@@ -520,6 +521,9 @@ module.exports = class SimpleRestContainer {
520
521
  try {
521
522
  requestOptions.body = this._getMustachedCap(Capabilities.SIMPLEREST_BODY_TEMPLATE, !bodyRaw)
522
523
  requestOptions.json = !bodyRaw
524
+ if (requestOptions.json && (!requestOptions.body || Object.keys(requestOptions.body).length === 0)) {
525
+ debug(`warning: requestOptions.body content seems to be empty - ${requestOptions.body} - capability: "${this.caps[Capabilities.SIMPLEREST_BODY_TEMPLATE]}"`)
526
+ }
523
527
  } catch (err) {
524
528
  throw new Error(`composing body from SIMPLEREST_BODY_TEMPLATE failed (${err.message})`)
525
529
  }
@@ -596,9 +600,9 @@ module.exports = class SimpleRestContainer {
596
600
  debug(`_waitForUrlResponse success on url check ${pingConfig.uri}: ${response.statusCode}/${response.statusMessage}`)
597
601
  this._storeCookiesFromResponse(response)
598
602
  if (debug.enabled && body) {
599
- debug(botiumUtils.shortenJsonString(body))
603
+ debug(`body: ${botiumUtils.shortenJsonString(body)}, headers: ${botiumUtils.shortenJsonString(response.headers)}`)
600
604
  }
601
- return body
605
+ return { body, headers: response.headers }
602
606
  }
603
607
  }
604
608
  }
@@ -635,7 +639,10 @@ module.exports = class SimpleRestContainer {
635
639
  try {
636
640
  return JSON.parse(raw)
637
641
  } catch (err) {
638
- 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})`)
639
646
  }
640
647
  } else {
641
648
  return raw
@@ -673,7 +680,7 @@ module.exports = class SimpleRestContainer {
673
680
  this.inboundEvents.push(event)
674
681
  this._processOrderedInboundEventsArrayAsync()
675
682
  } else {
676
- 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)
677
684
  }
678
685
  }
679
686
 
@@ -797,7 +804,7 @@ module.exports = class SimpleRestContainer {
797
804
  debug(botiumUtils.shortenJsonString(body))
798
805
  }
799
806
  } else if (body) {
800
- 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)}`)
801
808
  this._storeCookiesFromResponse(response)
802
809
  try {
803
810
  body = await this._parseResponseBody(body)
@@ -806,7 +813,7 @@ module.exports = class SimpleRestContainer {
806
813
  return
807
814
  }
808
815
  if (body) {
809
- 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)
810
817
  } else {
811
818
  debug('_runPolling: ignoring response body (no string and no JSON object)')
812
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 {
@@ -1,13 +1,9 @@
1
1
  const util = require('util')
2
2
  const _ = require('lodash')
3
+ const debug = require('debug')('botium-core-RetryHelper')
3
4
 
4
5
  module.exports = class RetryHelper {
5
- constructor (caps, section) {
6
- this.retrySettings = {
7
- retries: caps[`RETRY_${section.toUpperCase()}_NUMRETRIES`] || 1,
8
- factor: caps[`RETRY_${section.toUpperCase()}_FACTOR`] || 1,
9
- minTimeout: caps[`RETRY_${section.toUpperCase()}_MINTIMEOUT`] || 1000
10
- }
6
+ constructor (caps, section, options = {}) {
11
7
  this.retryErrorPatterns = []
12
8
  const onErrorRegexp = caps[`RETRY_${section.toUpperCase()}_ONERROR_REGEXP`] || []
13
9
  if (onErrorRegexp) {
@@ -22,10 +18,20 @@ module.exports = class RetryHelper {
22
18
  this.retryErrorPatterns.push(onErrorRegexp)
23
19
  }
24
20
  }
21
+
22
+ // to turn on retries, NUMRETRIES or ONERROR_REGEXP has to be set
23
+ this.retrySettings = {
24
+ retries: caps[`RETRY_${section.toUpperCase()}_NUMRETRIES`] || (!_.isNil(options.numRetries) ? options.numRetries : (this.retryErrorPatterns.length === 0) ? 0 : 1),
25
+ factor: caps[`RETRY_${section.toUpperCase()}_FACTOR`] || (_.isNil(options.factor) ? 1 : options.factor),
26
+ minTimeout: caps[`RETRY_${section.toUpperCase()}_MINTIMEOUT`] || (_.isNil(options.minTimeout) ? 1000 : options.minTimeout)
27
+ }
28
+
29
+ debug(`Retry for ${section} is ${this.retrySettings.retries > 0 ? 'enabled' : 'disabled'}. Settings: ${JSON.stringify(this.retrySettings)} Patterns: ${JSON.stringify(this.retryErrorPatterns.map(r => r.toString()))}`)
25
30
  }
26
31
 
27
32
  shouldRetry (err) {
28
- if (!err || this.retryErrorPatterns.length === 0) return false
33
+ if (!err) return false
34
+ if (this.retryErrorPatterns.length === 0) return true
29
35
  const errString = util.inspect(err)
30
36
  for (const re of this.retryErrorPatterns) {
31
37
  if (errString.match(re)) return true
@@ -1,6 +1,7 @@
1
1
  const util = require('util')
2
2
  const _ = require('lodash')
3
3
  const debug = require('debug')('botium-core-Convo')
4
+ const promiseRetry = require('promise-retry')
4
5
 
5
6
  const BotiumMockMessage = require('../mocks/BotiumMockMessage')
6
7
  const Capabilities = require('../Capabilities')
@@ -8,6 +9,7 @@ const Events = require('../Events')
8
9
  const ScriptingMemory = require('./ScriptingMemory')
9
10
  const { BotiumError, botiumErrorFromErr, botiumErrorFromList } = require('./BotiumError')
10
11
  const { normalizeText, toString, removeBuffers, splitStringInNonEmptyLines } = require('./helper')
12
+ const RetryHelper = require('../helpers/RetryHelper')
11
13
 
12
14
  const { LOGIC_HOOK_INCLUDE } = require('./logichook/LogicHookConsts')
13
15
 
@@ -89,9 +91,9 @@ class ConvoStep {
89
91
  '#' + this.sender +
90
92
  ' - ' + (this.optional ? '?' : '') + (this.not ? '!' : '') +
91
93
  (this.messageText || '') +
92
- (this.asserters && this.asserters.length > 0 ? ' ' + this.asserters.map(a => a.toString()).join(' ASS: ') : '') +
93
- (this.logicHooks && this.logicHooks.length > 0 ? ' ' + this.logicHooks.map(l => l.toString()).join(' LH: ') : '') +
94
- (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(' ') : '')
95
97
  }
96
98
  }
97
99
 
@@ -117,7 +119,7 @@ class Transcript {
117
119
  }
118
120
  }
119
121
 
120
- class TranscriptAttachment { // eslint-disable-line no-unused-vars
122
+ class TranscriptAttachment {
121
123
  constructor (fromJson = {}) {
122
124
  this.name = fromJson.name
123
125
  this.mimeType = fromJson.mimeType
@@ -210,6 +212,24 @@ class Convo {
210
212
  }
211
213
 
212
214
  async Run (container) {
215
+ const retryHelper = new RetryHelper(container.caps, 'CONVO')
216
+ return promiseRetry(async (retry, number) => {
217
+ return this.RunImpl(container).catch(err => {
218
+ const retryRemaining = retryHelper.retrySettings.retries - number + 1
219
+ if (retryHelper.shouldRetry(err)) {
220
+ debug(`Convo failed with error "${err.message || JSON.stringify(err)}". Retry ${retryRemaining > 0 ? 'enabled' : 'disabled'} (remaining #${retryRemaining}/${retryHelper.retrySettings.retries}, criterion matches)`)
221
+ retry(err)
222
+ } else {
223
+ if (retryHelper.retryErrorPatterns.length > 0) {
224
+ debug(`Convo failed with error "${err.message || JSON.stringify(err)}". Retry 'disabled' (remaining (#${retryRemaining}/${retryHelper.retrySettings.retries}), criterion does not match)`)
225
+ }
226
+ throw err
227
+ }
228
+ })
229
+ }, retryHelper.retrySettings)
230
+ }
231
+
232
+ async RunImpl (container) {
213
233
  const transcript = new Transcript({
214
234
  steps: [],
215
235
  attachments: [],
@@ -694,9 +714,15 @@ class Convo {
694
714
  }
695
715
  }
696
716
 
697
- module
698
- .exports = {
699
- ConvoHeader,
700
- Convo,
701
- ConvoStep
702
- }
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
  ],
@@ -192,7 +192,7 @@ module.exports = class LogicHookUtils {
192
192
  })
193
193
  return vm.run(script)
194
194
  } catch (err) {
195
- throw new Error(`${err.message || err}`)
195
+ throw new Error(`Script ${key} is not valid - ${err.message || err}`)
196
196
  }
197
197
  } else {
198
198
  throw new Error(`Script "${key}" is not valid - only functions and javascript code accepted`)