botium-core 1.13.16 → 1.13.18
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/README.md +0 -1
- package/dist/botium-cjs.js +283 -172
- package/dist/botium-cjs.js.map +1 -1
- package/dist/botium-es.js +282 -171
- package/dist/botium-es.js.map +1 -1
- package/package.json +23 -23
- package/src/BotDriver.js +2 -4
- package/src/Capabilities.js +7 -3
- package/src/Events.js +1 -0
- package/src/containers/BaseContainer.js +5 -3
- package/src/containers/PluginConnectorContainer.js +0 -4
- package/src/containers/plugins/SimpleRestContainer.js +49 -0
- package/src/scripting/Convo.js +10 -27
- package/src/scripting/ScriptingProvider.js +121 -53
- package/src/scripting/helper.js +9 -2
- package/src/scripting/logichook/asserter/ButtonsAsserter.js +21 -8
- package/src/scripting/logichook/asserter/WerAsserter.js +53 -6
- package/src/scripting/logichook/logichooks/ClearQueueLogicHook.js +0 -1
- package/test/connectors/logicHook.js +0 -1
- package/test/connectors/simplerest.spec.js +79 -4
- package/test/scripting/asserters/buttonsAsserter.spec.js +84 -50
- package/test/scripting/logichooks/convos/custom_embedded_skip.convo.txt +11 -0
- package/test/scripting/logichooks/convos/custom_embedded_skip_followed_by_me.convo.txt +11 -0
- package/test/scripting/logichooks/convos/custom_embedded_skip_followed_by_nothing.convo.txt +8 -0
- package/test/scripting/logichooks/customEmbeddedSkip.json +14 -0
- package/test/scripting/logichooks/customEmbeddedSkip.spec.js +58 -0
- package/test/security/allowUnsafe.spec.js +20 -10
- package/test/security/convos/withscriptingmemoryfunction.convo.txt +1 -0
- package/test/convo/retryconvo.spec.js +0 -134
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botium-core",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.18",
|
|
4
4
|
"description": "The Selenium for Chatbots",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "dist/botium-es.js",
|
|
@@ -32,66 +32,66 @@
|
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://www.botium.ai",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@babel/runtime": "^7.
|
|
35
|
+
"@babel/runtime": "^7.21.5",
|
|
36
36
|
"async": "^3.2.4",
|
|
37
|
-
"body-parser": "^1.20.
|
|
37
|
+
"body-parser": "^1.20.2",
|
|
38
38
|
"boolean": "^3.2.0",
|
|
39
39
|
"bottleneck": "^2.19.5",
|
|
40
|
-
"csv-parse": "^5.3.
|
|
40
|
+
"csv-parse": "^5.3.10",
|
|
41
41
|
"debug": "^4.3.4",
|
|
42
42
|
"esprima": "^4.0.1",
|
|
43
43
|
"express": "^4.18.2",
|
|
44
44
|
"globby": "11.0.4",
|
|
45
|
-
"ioredis": "^5.2
|
|
45
|
+
"ioredis": "^5.3.2",
|
|
46
46
|
"is-class": "^0.0.9",
|
|
47
47
|
"is-json": "^2.0.1",
|
|
48
48
|
"jsonpath": "^1.1.1",
|
|
49
49
|
"lodash": "^4.17.21",
|
|
50
50
|
"markdown-it": "^13.0.1",
|
|
51
51
|
"mime-types": "^2.1.35",
|
|
52
|
-
"mkdirp": "^
|
|
52
|
+
"mkdirp": "^3.0.1",
|
|
53
53
|
"moment": "^2.29.4",
|
|
54
54
|
"mustache": "^4.2.0",
|
|
55
55
|
"promise-retry": "^2.0.1",
|
|
56
56
|
"promise.allsettled": "^1.0.6",
|
|
57
57
|
"randomatic": "^3.1.1",
|
|
58
58
|
"request": "^2.88.2",
|
|
59
|
-
"rimraf": "^
|
|
59
|
+
"rimraf": "^5.0.0",
|
|
60
60
|
"sanitize-filename": "^1.6.3",
|
|
61
|
-
"slugify": "^1.6.
|
|
62
|
-
"socket.io": "^4.
|
|
63
|
-
"socket.io-client": "^4.
|
|
61
|
+
"slugify": "^1.6.6",
|
|
62
|
+
"socket.io": "^4.6.1",
|
|
63
|
+
"socket.io-client": "^4.6.1",
|
|
64
64
|
"socketio-auth": "^0.1.1",
|
|
65
|
-
"swagger-jsdoc": "^6.2.
|
|
66
|
-
"swagger-ui-express": "^4.6.
|
|
65
|
+
"swagger-jsdoc": "^6.2.8",
|
|
66
|
+
"swagger-ui-express": "^4.6.3",
|
|
67
67
|
"uuid": "^9.0.0",
|
|
68
|
-
"vm2": "^3.9.
|
|
68
|
+
"vm2": "^3.9.17",
|
|
69
69
|
"word-error-rate": "0.0.7",
|
|
70
70
|
"write-yaml": "^1.0.0",
|
|
71
71
|
"xlsx": "^0.18.5",
|
|
72
72
|
"xregexp": "^5.1.1",
|
|
73
|
-
"yaml": "^2.
|
|
73
|
+
"yaml": "^2.2.2"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@babel/core": "^7.
|
|
77
|
-
"@babel/node": "^7.20.
|
|
78
|
-
"@babel/plugin-transform-runtime": "^7.
|
|
79
|
-
"@babel/preset-env": "^7.
|
|
76
|
+
"@babel/core": "^7.21.8",
|
|
77
|
+
"@babel/node": "^7.20.7",
|
|
78
|
+
"@babel/plugin-transform-runtime": "^7.21.4",
|
|
79
|
+
"@babel/preset-env": "^7.21.5",
|
|
80
80
|
"chai": "^4.3.7",
|
|
81
81
|
"chai-as-promised": "^7.1.1",
|
|
82
82
|
"cross-env": "^7.0.3",
|
|
83
|
-
"eslint": "^8.
|
|
83
|
+
"eslint": "^8.40.0",
|
|
84
84
|
"eslint-config-standard": "^17.0.0",
|
|
85
|
-
"eslint-plugin-import": "^2.
|
|
85
|
+
"eslint-plugin-import": "^2.27.5",
|
|
86
86
|
"eslint-plugin-mocha": "^10.1.0",
|
|
87
|
-
"eslint-plugin-n": "^15.
|
|
87
|
+
"eslint-plugin-n": "^15.7.0",
|
|
88
88
|
"eslint-plugin-promise": "^6.1.1",
|
|
89
89
|
"eslint-plugin-standard": "^4.1.0",
|
|
90
90
|
"license-checker": "^25.0.1",
|
|
91
91
|
"license-compatibility-checker": "^0.3.5",
|
|
92
92
|
"mocha": "^10.2.0",
|
|
93
|
-
"nock": "^13.
|
|
94
|
-
"npm-check-updates": "^16.
|
|
93
|
+
"nock": "^13.3.1",
|
|
94
|
+
"npm-check-updates": "^16.10.12",
|
|
95
95
|
"nyc": "^15.1.0",
|
|
96
96
|
"rollup": "2.79.1",
|
|
97
97
|
"rollup-plugin-babel": "^4.4.0",
|
package/src/BotDriver.js
CHANGED
|
@@ -2,7 +2,7 @@ const util = require('util')
|
|
|
2
2
|
const fs = require('fs')
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const async = require('async')
|
|
5
|
-
const rimraf = require('rimraf')
|
|
5
|
+
const { rimraf } = require('rimraf')
|
|
6
6
|
const mkdirp = require('mkdirp')
|
|
7
7
|
const sanitize = require('sanitize-filename')
|
|
8
8
|
const moment = require('moment')
|
|
@@ -168,9 +168,7 @@ module.exports = class BotDriver {
|
|
|
168
168
|
debug(`BotDriver Build error: ${err}`)
|
|
169
169
|
this.eventEmitter.emit(Events.CONTAINER_BUILD_ERROR, err)
|
|
170
170
|
if (tempDirectory) {
|
|
171
|
-
rimraf(tempDirectory
|
|
172
|
-
if (err) debug(`Cleanup temp dir ${tempDirectory} failed: ${util.inspect(err)}`)
|
|
173
|
-
})
|
|
171
|
+
rimraf(tempDirectory).catch((err) => debug(`Cleanup temp dir ${tempDirectory} failed: ${util.inspect(err)}`))
|
|
174
172
|
}
|
|
175
173
|
return reject(err)
|
|
176
174
|
}
|
package/src/Capabilities.js
CHANGED
|
@@ -68,6 +68,12 @@ module.exports = {
|
|
|
68
68
|
SIMPLEREST_POLL_INTERVAL: 'SIMPLEREST_POLL_INTERVAL',
|
|
69
69
|
SIMPLEREST_POLL_TIMEOUT: 'SIMPLEREST_PING_TIMEOUT',
|
|
70
70
|
SIMPLEREST_POLL_UPDATE_CONTEXT: 'SIMPLEREST_POLL_UPDATE_CONTEXT',
|
|
71
|
+
SIMPLEREST_CONTEXT_IGNORE_JSONPATH: 'SIMPLEREST_CONTEXT_IGNORE_JSONPATH',
|
|
72
|
+
SIMPLEREST_CONTEXT_IGNORE_MATCH: 'SIMPLEREST_CONTEXT_IGNORE_MATCH',
|
|
73
|
+
SIMPLEREST_CONTEXT_SKIP_JSONPATH: 'SIMPLEREST_CONTEXT_SKIP_JSONPATH',
|
|
74
|
+
SIMPLEREST_CONTEXT_SKIP_MATCH: 'SIMPLEREST_CONTEXT_SKIP_MATCH',
|
|
75
|
+
SIMPLEREST_CONTEXT_CONTINUE_JSONPATH: 'SIMPLEREST_CONTEXT_CONTINUE_JSONPATH',
|
|
76
|
+
SIMPLEREST_CONTEXT_CONTINUE_MATCH: 'SIMPLEREST_CONTEXT_CONTINUE_MATCH',
|
|
71
77
|
SIMPLEREST_BODY_JSONPATH: 'SIMPLEREST_BODY_JSONPATH',
|
|
72
78
|
SIMPLEREST_RESPONSE_JSONPATH: 'SIMPLEREST_RESPONSE_JSONPATH',
|
|
73
79
|
SIMPLEREST_RESPONSE_HOOK: 'SIMPLEREST_RESPONSE_HOOK',
|
|
@@ -165,7 +171,5 @@ module.exports = {
|
|
|
165
171
|
RATELIMIT_USERSAYS_MINTIME: 'RATELIMIT_USERSAYS_MINTIME',
|
|
166
172
|
RATELIMIT_BOTTLENECK_FN: 'RATELIMIT_BOTTLENECK_FN',
|
|
167
173
|
SECURITY_ALLOW_UNSAFE: 'SECURITY_ALLOW_UNSAFE',
|
|
168
|
-
PRECOMPILERS: 'PRECOMPILERS'
|
|
169
|
-
// RETRY
|
|
170
|
-
RETRY_CONVO_ASYNC: 'RETRY_CONVO_ASYNC'
|
|
174
|
+
PRECOMPILERS: 'PRECOMPILERS'
|
|
171
175
|
}
|
package/src/Events.js
CHANGED
|
@@ -13,6 +13,7 @@ module.exports = {
|
|
|
13
13
|
CONTAINER_CLEANED: 'CONTAINER_CLEANED',
|
|
14
14
|
CONTAINER_CLEAN_ERROR: 'CONTAINER_CLEAN_ERROR',
|
|
15
15
|
BOT_CONNECTED: 'BOT_CONNECTED',
|
|
16
|
+
CONVO_STEP_NEXT: 'CONVO_STEP_NEXT',
|
|
16
17
|
// Chatbot Events
|
|
17
18
|
MESSAGE_SENTTOBOT: 'MESSAGE_SENTTOBOT',
|
|
18
19
|
MESSAGE_SENDTOBOT_ERROR: 'MESSAGE_SENDTOBOT_ERROR',
|
|
@@ -173,10 +173,12 @@ module.exports = class BaseContainer {
|
|
|
173
173
|
(rimraffed) => {
|
|
174
174
|
if (this.caps[Capabilities.CLEANUPTEMPDIR]) {
|
|
175
175
|
debug(`Cleanup rimrafing temp dir ${this.tempDirectory}`)
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
try {
|
|
177
|
+
rimraf.sync(this.tempDirectory)
|
|
178
178
|
rimraffed()
|
|
179
|
-
})
|
|
179
|
+
} catch (err) {
|
|
180
|
+
rimraffed(new Error(`Cleanup temp directory ${this.tempDirectory} failed: ${util.inspect(err)}`))
|
|
181
|
+
}
|
|
180
182
|
} else {
|
|
181
183
|
rimraffed()
|
|
182
184
|
}
|
|
@@ -11,16 +11,12 @@ const RetryHelper = require('../helpers/RetryHelper')
|
|
|
11
11
|
module.exports = class PluginConnectorContainer extends BaseContainer {
|
|
12
12
|
async Validate () {
|
|
13
13
|
await super.Validate()
|
|
14
|
-
const setAsync = (isAsync) => {
|
|
15
|
-
this.caps.RETRY_CONVO_ASYNC = isAsync
|
|
16
|
-
}
|
|
17
14
|
this.pluginInstance = tryLoadPlugin(
|
|
18
15
|
this.caps[Capabilities.CONTAINERMODE],
|
|
19
16
|
this.caps[Capabilities.PLUGINMODULEPATH],
|
|
20
17
|
{
|
|
21
18
|
container: this,
|
|
22
19
|
queueBotSays: (msg) => this._QueueBotSays(msg),
|
|
23
|
-
setAsync: (isAsync) => setAsync(isAsync),
|
|
24
20
|
bottleneck: this.bottleneck,
|
|
25
21
|
eventEmitter: this.eventEmitter,
|
|
26
22
|
caps: this.caps,
|
|
@@ -293,6 +293,47 @@ module.exports = class SimpleRestContainer {
|
|
|
293
293
|
debug(`current session context: ${util.inspect(this.view.context)}`)
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
const _isAnyContextJsonPathMatch = (capName, capNameMatch) => {
|
|
297
|
+
const jsonPaths = getAllCapValues(capName, this.caps)
|
|
298
|
+
if (jsonPaths.length > 0) {
|
|
299
|
+
const jsonPathsMatch = getAllCapValues(capNameMatch, this.caps)
|
|
300
|
+
for (const [index, jsonPath] of jsonPaths.entries()) {
|
|
301
|
+
const contextNodes = jp.query(this.view.context, jsonPath)
|
|
302
|
+
if (_.isArray(contextNodes) && contextNodes.length > 0) {
|
|
303
|
+
if (jsonPathsMatch[index]) {
|
|
304
|
+
if (contextNodes[0] === jsonPathsMatch[index]) {
|
|
305
|
+
return {
|
|
306
|
+
jsonPath,
|
|
307
|
+
match: contextNodes[0]
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
return {
|
|
312
|
+
jsonPath
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return null
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const ignoreMatch = _isAnyContextJsonPathMatch(Capabilities.SIMPLEREST_CONTEXT_IGNORE_JSONPATH, Capabilities.SIMPLEREST_CONTEXT_IGNORE_MATCH)
|
|
322
|
+
if (ignoreMatch) {
|
|
323
|
+
if (ignoreMatch.match) debug(`ignoring response for context match: ${ignoreMatch.jsonPath} = ${ignoreMatch.match}`)
|
|
324
|
+
else debug(`ignoring response for context: ${ignoreMatch.jsonPath}`)
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const skipMatch = _isAnyContextJsonPathMatch(Capabilities.SIMPLEREST_CONTEXT_SKIP_JSONPATH, Capabilities.SIMPLEREST_CONTEXT_SKIP_MATCH)
|
|
329
|
+
if (skipMatch) {
|
|
330
|
+
if (skipMatch.match) debug(`skipping response for context match: ${skipMatch.jsonPath} = ${skipMatch.match}`)
|
|
331
|
+
else debug(`skipping response for context: ${skipMatch.jsonPath}`)
|
|
332
|
+
|
|
333
|
+
setTimeout(() => this._doRequest({ messageText: '' }, true, true), 0)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
296
337
|
const result = []
|
|
297
338
|
if (isFromUser) {
|
|
298
339
|
const _extractFrom = (root, jsonPaths) => {
|
|
@@ -442,6 +483,14 @@ module.exports = class SimpleRestContainer {
|
|
|
442
483
|
}
|
|
443
484
|
}
|
|
444
485
|
}
|
|
486
|
+
|
|
487
|
+
const continueMatch = _isAnyContextJsonPathMatch(Capabilities.SIMPLEREST_CONTEXT_CONTINUE_JSONPATH, Capabilities.SIMPLEREST_CONTEXT_CONTINUE_MATCH)
|
|
488
|
+
if (continueMatch) {
|
|
489
|
+
if (continueMatch.match) debug(`continue with next response for context match: ${continueMatch.jsonPath} = ${continueMatch.match}`)
|
|
490
|
+
else debug(`continue with next response for context: ${continueMatch.jsonPath}`)
|
|
491
|
+
|
|
492
|
+
setTimeout(() => this._doRequest({ messageText: '' }, true, true), 0)
|
|
493
|
+
}
|
|
445
494
|
return result
|
|
446
495
|
}
|
|
447
496
|
|
package/src/scripting/Convo.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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')
|
|
5
4
|
|
|
6
5
|
const BotiumMockMessage = require('../mocks/BotiumMockMessage')
|
|
7
6
|
const Capabilities = require('../Capabilities')
|
|
@@ -9,7 +8,6 @@ const Events = require('../Events')
|
|
|
9
8
|
const ScriptingMemory = require('./ScriptingMemory')
|
|
10
9
|
const { BotiumError, botiumErrorFromErr, botiumErrorFromList } = require('./BotiumError')
|
|
11
10
|
const { normalizeText, toString, removeBuffers, splitStringInNonEmptyLines } = require('./helper')
|
|
12
|
-
const RetryHelper = require('../helpers/RetryHelper')
|
|
13
11
|
|
|
14
12
|
const { LOGIC_HOOK_INCLUDE } = require('./logichook/LogicHookConsts')
|
|
15
13
|
|
|
@@ -212,31 +210,6 @@ class Convo {
|
|
|
212
210
|
}
|
|
213
211
|
|
|
214
212
|
async Run (container) {
|
|
215
|
-
if (container.caps.RETRY_CONVO_ASYNC) {
|
|
216
|
-
return this.RunImpl(container).catch(err => {
|
|
217
|
-
debug(`Convo failed with error "${err.message || JSON.stringify(err)}".`)
|
|
218
|
-
throw err
|
|
219
|
-
})
|
|
220
|
-
} else {
|
|
221
|
-
const retryHelper = new RetryHelper(container.caps, 'CONVO')
|
|
222
|
-
return promiseRetry(async (retry, number) => {
|
|
223
|
-
const retryRemaining = retryHelper.retrySettings.retries - number + 1
|
|
224
|
-
return this.RunImpl(container).catch(err => {
|
|
225
|
-
if (retryHelper.shouldRetry(err)) {
|
|
226
|
-
debug(`Convo failed with error "${err.message || JSON.stringify(err)}". Retry ${retryRemaining > 0 ? 'enabled' : 'disabled'} (remaining #${retryRemaining}/${retryHelper.retrySettings.retries}, criterion matches)`)
|
|
227
|
-
retry(err)
|
|
228
|
-
} else {
|
|
229
|
-
if (retryHelper.retryErrorPatterns.length > 0) {
|
|
230
|
-
debug(`Convo failed with error "${err.message || JSON.stringify(err)}". Retry 'disabled' (remaining (#${retryRemaining}/${retryHelper.retrySettings.retries}), criterion does not match)`)
|
|
231
|
-
}
|
|
232
|
-
throw err
|
|
233
|
-
}
|
|
234
|
-
})
|
|
235
|
-
}, retryHelper.retrySettings)
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async RunImpl (container) {
|
|
240
213
|
const transcript = new Transcript({
|
|
241
214
|
steps: [],
|
|
242
215
|
attachments: [],
|
|
@@ -305,6 +278,7 @@ class Convo {
|
|
|
305
278
|
for (let i = 0; i < this.conversation.length; i++) {
|
|
306
279
|
const convoStep = this.conversation[i]
|
|
307
280
|
const currentStepIndex = i
|
|
281
|
+
container.eventEmitter.emit(Events.CONVO_STEP_NEXT, container, convoStep, i)
|
|
308
282
|
skipTranscriptStep = false
|
|
309
283
|
const transcriptStep = new TranscriptStep({
|
|
310
284
|
expected: new BotiumMockMessage(convoStep),
|
|
@@ -430,6 +404,15 @@ class Convo {
|
|
|
430
404
|
throw failErr
|
|
431
405
|
}
|
|
432
406
|
|
|
407
|
+
if (convoStep.skip === true) {
|
|
408
|
+
skipTranscriptStep = true
|
|
409
|
+
const nextConvoStep = this.conversation[i + 1]
|
|
410
|
+
if (nextConvoStep && nextConvoStep.sender === 'bot') {
|
|
411
|
+
waitForBotSays = false
|
|
412
|
+
}
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
|
|
433
416
|
if (!botMsg || (!botMsg.messageText && !botMsg.media && !botMsg.buttons && !botMsg.cards && !botMsg.sourceData && !botMsg.nlp)) {
|
|
434
417
|
const failErr = new BotiumError(`${this.header.name}/${convoStep.stepTag}: bot says nothing`)
|
|
435
418
|
debug(failErr)
|
|
@@ -19,6 +19,7 @@ const { BotiumError, botiumErrorFromList, botiumErrorFromErr } = require('./Boti
|
|
|
19
19
|
const RetryHelper = require('../helpers/RetryHelper')
|
|
20
20
|
const { getMatchFunction } = require('./MatchFunctions')
|
|
21
21
|
const precompilers = require('./precompilers')
|
|
22
|
+
const { calculateWer, toPercent } = require('./helper')
|
|
22
23
|
|
|
23
24
|
const globPattern = '**/+(*.convo.txt|*.utterances.txt|*.pconvo.txt|*.scriptingmemory.txt|*.xlsx|*.xlsm|*.convo.csv|*.pconvo.csv|*.utterances.csv|*.yaml|*.yml|*.json|*.md|*.markdown)'
|
|
24
25
|
const skipPattern = /^skip[.\-_]/i
|
|
@@ -137,32 +138,57 @@ module.exports = class ScriptingProvider {
|
|
|
137
138
|
const found = _.find(tomatch, (utt) => this.matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]))
|
|
138
139
|
const asserterType = this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer' ? 'Word Error Rate Asserter' : 'Text Match Asserter'
|
|
139
140
|
if (_.isNil(found)) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
|
|
162
|
-
args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
|
|
141
|
+
if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer') {
|
|
142
|
+
const wer = calculateWer(botresponse, tomatch[0])
|
|
143
|
+
const werArgs = this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]
|
|
144
|
+
const threshold = ([',', '.'].find(p => `${werArgs[0]}`.includes(p)) ? parseFloat(werArgs[0]) : parseInt(werArgs[0]) / 100)
|
|
145
|
+
const message = `${stepTag}: Word Error Rate (${toPercent(wer)}) higher than accepted (${toPercent(threshold)})`
|
|
146
|
+
throw new BotiumError(
|
|
147
|
+
message,
|
|
148
|
+
{
|
|
149
|
+
type: 'asserter',
|
|
150
|
+
source: asserterType,
|
|
151
|
+
params: {
|
|
152
|
+
matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
|
|
153
|
+
args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
|
|
154
|
+
},
|
|
155
|
+
context: {
|
|
156
|
+
stepTag
|
|
157
|
+
},
|
|
158
|
+
cause: {
|
|
159
|
+
expected: `<=${toPercent(threshold)} (${tomatch})`,
|
|
160
|
+
actual: `${toPercent(wer)} (${botresponse})`
|
|
161
|
+
}
|
|
163
162
|
}
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
)
|
|
164
|
+
} else {
|
|
165
|
+
let message = `${stepTag}: Bot response `
|
|
166
|
+
message += meMsg ? `(on ${meMsg}) ` : ''
|
|
167
|
+
message += botresponse ? ('"' + botresponse + '"') : '<no response>'
|
|
168
|
+
message += ' expected to match '
|
|
169
|
+
message += tomatch && tomatch.length > 1 ? 'one of ' : ''
|
|
170
|
+
message += `${tomatch.map(e => e ? '"' + e + '"' : '<any response>').join(', ')}`
|
|
171
|
+
throw new BotiumError(
|
|
172
|
+
message,
|
|
173
|
+
{
|
|
174
|
+
type: 'asserter',
|
|
175
|
+
source: asserterType,
|
|
176
|
+
params: {
|
|
177
|
+
matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
|
|
178
|
+
args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
|
|
179
|
+
},
|
|
180
|
+
context: {
|
|
181
|
+
stepTag
|
|
182
|
+
},
|
|
183
|
+
cause: {
|
|
184
|
+
expected: tomatch,
|
|
185
|
+
actual: botresponse,
|
|
186
|
+
matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
|
|
187
|
+
args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
}
|
|
166
192
|
}
|
|
167
193
|
},
|
|
168
194
|
assertBotNotResponse: (botresponse, nottomatch, stepTag, meMsg) => {
|
|
@@ -173,33 +199,58 @@ module.exports = class ScriptingProvider {
|
|
|
173
199
|
const found = _.find(nottomatch, (utt) => this.matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]))
|
|
174
200
|
const asserterType = this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer' ? 'Word Error Rate Asserter' : 'Text Match Asserter'
|
|
175
201
|
if (!_.isNil(found)) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
actual: botresponse,
|
|
198
|
-
matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
|
|
199
|
-
args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
|
|
202
|
+
if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer') {
|
|
203
|
+
const wer = calculateWer(botresponse, nottomatch[0])
|
|
204
|
+
const werArgs = this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]
|
|
205
|
+
const threshold = ([',', '.'].find(p => `${werArgs[0]}`.includes(p)) ? parseFloat(werArgs[0]) : parseInt(werArgs[0]) / 100)
|
|
206
|
+
const message = `${stepTag}: Word Error Rate (${toPercent(wer)}) lower than accepted (${toPercent(threshold)})`
|
|
207
|
+
throw new BotiumError(
|
|
208
|
+
message,
|
|
209
|
+
{
|
|
210
|
+
type: 'asserter',
|
|
211
|
+
source: asserterType,
|
|
212
|
+
params: {
|
|
213
|
+
matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
|
|
214
|
+
args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
|
|
215
|
+
},
|
|
216
|
+
context: {
|
|
217
|
+
stepTag
|
|
218
|
+
},
|
|
219
|
+
cause: {
|
|
220
|
+
expected: `>=${toPercent(threshold)} (${nottomatch})`,
|
|
221
|
+
actual: `${toPercent(wer)} (${botresponse})`
|
|
222
|
+
}
|
|
200
223
|
}
|
|
201
|
-
|
|
202
|
-
|
|
224
|
+
)
|
|
225
|
+
} else {
|
|
226
|
+
let message = `${stepTag}: Bot response `
|
|
227
|
+
message += meMsg ? `(on ${meMsg}) ` : ''
|
|
228
|
+
message += botresponse ? ('"' + botresponse + '"') : '<no response>'
|
|
229
|
+
message += ' expected NOT to match '
|
|
230
|
+
message += nottomatch && nottomatch.length > 1 ? 'one of ' : ''
|
|
231
|
+
message += `${nottomatch.map(e => e ? '"' + e + '"' : '<any response>').join(', ')}`
|
|
232
|
+
throw new BotiumError(
|
|
233
|
+
message,
|
|
234
|
+
{
|
|
235
|
+
type: 'asserter',
|
|
236
|
+
source: asserterType,
|
|
237
|
+
params: {
|
|
238
|
+
matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
|
|
239
|
+
args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
|
|
240
|
+
},
|
|
241
|
+
context: {
|
|
242
|
+
stepTag
|
|
243
|
+
},
|
|
244
|
+
cause: {
|
|
245
|
+
not: true,
|
|
246
|
+
expected: nottomatch,
|
|
247
|
+
actual: botresponse,
|
|
248
|
+
matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
|
|
249
|
+
args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
}
|
|
203
254
|
}
|
|
204
255
|
},
|
|
205
256
|
fail: null
|
|
@@ -889,10 +940,16 @@ module.exports = class ScriptingProvider {
|
|
|
889
940
|
convoFilter: null
|
|
890
941
|
}, options)
|
|
891
942
|
const expandedConvos = []
|
|
943
|
+
// The globalContext is going to keep the data even if the Object.assign which happening to create the myContext in _expandConvo function
|
|
944
|
+
const context = {
|
|
945
|
+
globalContext: {
|
|
946
|
+
totalConvoCount: 0
|
|
947
|
+
}
|
|
948
|
+
}
|
|
892
949
|
debug(`ExpandConvos - Using utterances expansion mode: ${this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE]}`)
|
|
893
950
|
this.convos.forEach((convo) => {
|
|
894
951
|
convo.expandPartialConvos()
|
|
895
|
-
for (const expanded of this._expandConvo(convo, options,
|
|
952
|
+
for (const expanded of this._expandConvo(convo, options, context)) {
|
|
896
953
|
expanded.header.assertionCount = this.GetAssertionCount(expanded)
|
|
897
954
|
if (options.justHeader) {
|
|
898
955
|
const ConvoWithOnlyHeader = {
|
|
@@ -908,6 +965,7 @@ module.exports = class ScriptingProvider {
|
|
|
908
965
|
}
|
|
909
966
|
})
|
|
910
967
|
this.convos = expandedConvos
|
|
968
|
+
this.totalConvoCount = context.globalContext.totalConvoCount
|
|
911
969
|
if (!options.justHeader) {
|
|
912
970
|
this._sortConvos()
|
|
913
971
|
} else {
|
|
@@ -920,17 +978,24 @@ module.exports = class ScriptingProvider {
|
|
|
920
978
|
// drop unwanted convos
|
|
921
979
|
convoFilter: null
|
|
922
980
|
}, options)
|
|
981
|
+
// The globalContext is going to keep the data even if the Object.assign which happening to create the myContext in _expandConvo function
|
|
982
|
+
const context = {
|
|
983
|
+
globalContext: {
|
|
984
|
+
totalConvoCount: 0
|
|
985
|
+
}
|
|
986
|
+
}
|
|
923
987
|
debug(`ExpandConvos - Using utterances expansion mode: ${this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE]}`)
|
|
924
988
|
// creating a nested generator, calling the other.
|
|
925
989
|
// We hope this.convos does not changes while this iterator is used
|
|
926
990
|
const _convosIterable = function * (options) {
|
|
927
991
|
for (const convo of this.convos) {
|
|
928
992
|
convo.expandPartialConvos()
|
|
929
|
-
yield * this._expandConvo(convo, options,
|
|
993
|
+
yield * this._expandConvo(convo, options, context)
|
|
930
994
|
}
|
|
931
995
|
}.bind(this)
|
|
932
996
|
|
|
933
997
|
this.convosIterable = _convosIterable(options)
|
|
998
|
+
this.totalConvoCount = context.globalContext.totalConvoCount
|
|
934
999
|
}
|
|
935
1000
|
|
|
936
1001
|
/**
|
|
@@ -1122,6 +1187,9 @@ module.exports = class ScriptingProvider {
|
|
|
1122
1187
|
}
|
|
1123
1188
|
} else {
|
|
1124
1189
|
const expanded = Object.assign(_.cloneDeep(currentConvo), { conversation: _.cloneDeep(convoStepsStack) })
|
|
1190
|
+
if (!_.isNil(_.get(context, 'globalContext.totalConvoCount'))) {
|
|
1191
|
+
context.globalContext.totalConvoCount++
|
|
1192
|
+
}
|
|
1125
1193
|
if (!options.convoFilter || options.convoFilter(expanded)) {
|
|
1126
1194
|
yield expanded
|
|
1127
1195
|
}
|
package/src/scripting/helper.js
CHANGED
|
@@ -553,6 +553,10 @@ const calculateWer = (str, pattern) => {
|
|
|
553
553
|
const botMessageWords = botMessage.split(' ').map(bm => bm.trim())
|
|
554
554
|
const utt = _prepareString(utterance)
|
|
555
555
|
|
|
556
|
+
// if no wildcards, just calculate WER
|
|
557
|
+
if (utt.indexOf('*') === -1) return speechScorer.wordErrorRate(botMessage, utt).toFixed(2)
|
|
558
|
+
|
|
559
|
+
// if there are wildcards, calculate WER for each wildcard part
|
|
556
560
|
const errors = []
|
|
557
561
|
for (let wildcardPart of utt.split('*')) {
|
|
558
562
|
let wer = 1
|
|
@@ -572,7 +576,7 @@ const calculateWer = (str, pattern) => {
|
|
|
572
576
|
}
|
|
573
577
|
}
|
|
574
578
|
if (_.isNil(subsetPhraseFound)) {
|
|
575
|
-
throw new Error('Word Error Asserter:
|
|
579
|
+
throw new Error('Word Error Asserter: When using wild cards, please make sure that the length of the asserter text is smaller than the bot message!')
|
|
576
580
|
}
|
|
577
581
|
errors.push(_getErrors(_getWords(wildcardPart), _getWords(subsetPhraseFound)))
|
|
578
582
|
}
|
|
@@ -586,6 +590,8 @@ const calculateWer = (str, pattern) => {
|
|
|
586
590
|
return (errCount / allCount).toFixed(2)
|
|
587
591
|
}
|
|
588
592
|
|
|
593
|
+
const toPercent = (s) => `${(s * 100).toFixed(0)}%`
|
|
594
|
+
|
|
589
595
|
module.exports = {
|
|
590
596
|
normalizeText,
|
|
591
597
|
splitStringInNonEmptyLines,
|
|
@@ -600,5 +606,6 @@ module.exports = {
|
|
|
600
606
|
validateSender,
|
|
601
607
|
validateConvo,
|
|
602
608
|
linesToScriptingMemories,
|
|
603
|
-
calculateWer
|
|
609
|
+
calculateWer,
|
|
610
|
+
toPercent
|
|
604
611
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const _ = require('lodash')
|
|
1
2
|
const { SCRIPTING_NORMALIZE_TEXT } = require('../../../Capabilities')
|
|
2
3
|
const { BotiumError } = require('../../BotiumError')
|
|
3
4
|
const { buttonsFromMsg } = require('../helpers')
|
|
@@ -11,17 +12,29 @@ module.exports = class ButtonsAsserter {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
_evalButtons (args, botMsg) {
|
|
14
|
-
const allButtons = buttonsFromMsg(botMsg, true).map(b => b.text).filter(b => b).map(b => normalizeText(b, !!this.caps[SCRIPTING_NORMALIZE_TEXT]))
|
|
15
|
+
const allButtons = buttonsFromMsg(botMsg, true).map(b => ({ text: b.text, payload: b.payload })).filter(b => b).map(b => ({ text: normalizeText(b.text, !!this.caps[SCRIPTING_NORMALIZE_TEXT]), payload: b.payload }))
|
|
15
16
|
if (!args || args.length === 0) {
|
|
16
|
-
return { allButtons, buttonsNotFound: [], buttonsFound: allButtons }
|
|
17
|
+
return { allButtons, buttonsNotFound: [], buttonsFound: allButtons.map(b => b.text) }
|
|
17
18
|
}
|
|
18
19
|
const buttonsNotFound = []
|
|
19
20
|
const buttonsFound = []
|
|
21
|
+
const parsePayload = (payload) => {
|
|
22
|
+
if (_.isNil(payload)) {
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(payload)
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return payload
|
|
29
|
+
}
|
|
30
|
+
}
|
|
20
31
|
for (let i = 0; i < (args || []).length; i++) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
const matchByText = allButtons.some(b => this.context.Match(b.text, normalizeText(args[i], !!this.caps[SCRIPTING_NORMALIZE_TEXT])))
|
|
33
|
+
const matchByPayload = allButtons.some(b => _.isEqual(parsePayload(b.payload), parsePayload(args[i])))
|
|
34
|
+
if (matchByText || matchByPayload) {
|
|
24
35
|
buttonsFound.push(args[i])
|
|
36
|
+
} else {
|
|
37
|
+
buttonsNotFound.push(args[i])
|
|
25
38
|
}
|
|
26
39
|
}
|
|
27
40
|
return { allButtons, buttonsNotFound, buttonsFound }
|
|
@@ -42,7 +55,7 @@ module.exports = class ButtonsAsserter {
|
|
|
42
55
|
cause: {
|
|
43
56
|
not: true,
|
|
44
57
|
expected: args,
|
|
45
|
-
actual: allButtons,
|
|
58
|
+
actual: JSON.stringify(allButtons, null, 2),
|
|
46
59
|
diff: buttonsFound
|
|
47
60
|
}
|
|
48
61
|
}
|
|
@@ -67,7 +80,7 @@ module.exports = class ButtonsAsserter {
|
|
|
67
80
|
cause: {
|
|
68
81
|
not: false,
|
|
69
82
|
expected: args,
|
|
70
|
-
actual: allButtons,
|
|
83
|
+
actual: JSON.stringify(allButtons, null, 2),
|
|
71
84
|
diff: buttonsNotFound
|
|
72
85
|
}
|
|
73
86
|
}
|
|
@@ -85,7 +98,7 @@ module.exports = class ButtonsAsserter {
|
|
|
85
98
|
cause: {
|
|
86
99
|
not: false,
|
|
87
100
|
expected: args,
|
|
88
|
-
actual: allButtons,
|
|
101
|
+
actual: JSON.stringify(allButtons, null, 2),
|
|
89
102
|
diff: buttonsNotFound
|
|
90
103
|
}
|
|
91
104
|
}
|