botium-core 1.14.0 → 1.14.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.
- package/dist/botium-cjs.js +302 -94
- package/dist/botium-cjs.js.map +1 -1
- package/dist/botium-es.js +302 -94
- package/dist/botium-es.js.map +1 -1
- package/package.json +19 -19
- package/src/Capabilities.js +2 -0
- package/src/scripting/BotiumError.js +40 -3
- package/src/scripting/Convo.js +139 -28
- package/src/scripting/ScriptingMemory.js +7 -0
- package/src/scripting/ScriptingProvider.js +79 -30
- package/src/scripting/logichook/LogicHookConsts.js +5 -2
- package/src/scripting/logichook/LogicHookUtils.js +8 -6
- package/src/scripting/logichook/logichooks/ConditionalBusinessHoursLogicHook.js +1 -1
- package/src/scripting/logichook/logichooks/ConditionalCapabilityValueBasedLogicHook.js +1 -1
- package/src/scripting/logichook/logichooks/ConditionalJsonPathBasedLogicHook.js +1 -1
- package/src/scripting/logichook/logichooks/ConditionalTimeBasedLogicHook.js +1 -1
- package/src/scripting/logichook/logichooks/ConvoStepParametersLogicHook.js +6 -0
- package/src/scripting/logichook/logichooks/OrderedListToButtonLogicHook.js +37 -0
- package/test/convo/fillAndApplyScriptingMemory.spec.js +11 -0
- package/test/logichooks/orderedListToButton.spec.js +35 -0
- package/test/scripting/asserters/convoStepParameters.spec.js +140 -0
- package/test/scripting/asserters/convos/TEXT_GOOD.convo.txt +6 -0
- package/test/scripting/asserters/convos/convo_step_parameter_matchmode_failed.convo.txt +8 -0
- package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_all_good.convo.txt +9 -0
- package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_botium_timeout.convo.txt +9 -0
- package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_good.convo.txt +9 -0
- package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_good_global.convo.txt +9 -0
- package/test/scripting/asserters/convos/convo_step_parameter_retry_main_and_asserter.convo.txt +10 -0
- package/test/scripting/asserters/convos/convo_step_parameter_retry_main_botium_timeout.convo.txt +9 -0
- package/test/scripting/asserters/convos/convo_step_parameter_retry_main_but_no_button.convo.txt +10 -0
- package/test/scripting/asserters/convos/convo_step_parameter_retry_main_good.convo.txt +9 -0
- package/test/scripting/asserters/convos/convo_step_parameter_retry_main_good_begin.convo.txt +11 -0
- package/test/scripting/logichooks/convos/conditional_steps_multiple_condition_groups_no_assertion.convo.txt +6 -6
- package/test/scripting/logichooks/convos/conditional_steps_multiple_mandatory_condition_groups.convo.txt +20 -0
- package/test/scripting/logichooks/convos/conditional_steps_multiple_optional_condition_groups.convo.txt +20 -0
- package/test/scripting/logichooks/customConditionalStepLogicHook.spec.js +16 -0
- package/test/scripting/matching/matchingmode.spec.js +4 -1
- package/test/scripting/scriptingProvider.spec.js +38 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botium-core",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.3",
|
|
4
4
|
"description": "The Selenium for Chatbots",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "dist/botium-es.js",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
},
|
|
32
32
|
"homepage": "https://www.botium.ai",
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@babel/runtime": "^7.
|
|
35
|
-
"async": "^3.2.
|
|
34
|
+
"@babel/runtime": "^7.23.5",
|
|
35
|
+
"async": "^3.2.5",
|
|
36
36
|
"body-parser": "^1.20.2",
|
|
37
37
|
"boolean": "^3.2.0",
|
|
38
38
|
"bottleneck": "^2.19.5",
|
|
39
|
-
"csv-parse": "^5.5.
|
|
39
|
+
"csv-parse": "^5.5.3",
|
|
40
40
|
"debug": "^4.3.4",
|
|
41
41
|
"express": "^4.18.2",
|
|
42
42
|
"globby": "11.0.4",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"is-json": "^2.0.1",
|
|
46
46
|
"jsonpath": "^1.1.1",
|
|
47
47
|
"lodash": "^4.17.21",
|
|
48
|
-
"markdown-it": "^
|
|
48
|
+
"markdown-it": "^14.0.0",
|
|
49
49
|
"mime-types": "^2.1.35",
|
|
50
50
|
"mkdirp": "^3.0.1",
|
|
51
51
|
"moment": "^2.29.4",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"promise.allsettled": "^1.0.7",
|
|
56
56
|
"randomatic": "^3.1.1",
|
|
57
57
|
"request": "^2.88.2",
|
|
58
|
-
"rimraf": "^5.0.
|
|
58
|
+
"rimraf": "^5.0.5",
|
|
59
59
|
"sanitize-filename": "^1.6.3",
|
|
60
60
|
"slugify": "^1.6.6",
|
|
61
61
|
"socket.io": "^4.7.2",
|
|
@@ -63,31 +63,31 @@
|
|
|
63
63
|
"socketio-auth": "^0.1.1",
|
|
64
64
|
"swagger-jsdoc": "^6.2.8",
|
|
65
65
|
"swagger-ui-express": "^5.0.0",
|
|
66
|
-
"uuid": "^9.0.
|
|
66
|
+
"uuid": "^9.0.1",
|
|
67
67
|
"word-error-rate": "0.0.7",
|
|
68
68
|
"write-yaml": "^1.0.0",
|
|
69
69
|
"xlsx": "^0.18.5",
|
|
70
70
|
"xregexp": "^5.1.1",
|
|
71
|
-
"yaml": "^2.3.
|
|
71
|
+
"yaml": "^2.3.4"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
|
-
"@babel/core": "^7.
|
|
75
|
-
"@babel/node": "^7.22.
|
|
76
|
-
"@babel/plugin-transform-runtime": "^7.
|
|
77
|
-
"@babel/preset-env": "^7.
|
|
78
|
-
"chai": "^4.3.
|
|
74
|
+
"@babel/core": "^7.23.5",
|
|
75
|
+
"@babel/node": "^7.22.19",
|
|
76
|
+
"@babel/plugin-transform-runtime": "^7.23.4",
|
|
77
|
+
"@babel/preset-env": "^7.23.5",
|
|
78
|
+
"chai": "^4.3.10",
|
|
79
79
|
"chai-as-promised": "^7.1.1",
|
|
80
80
|
"cross-env": "^7.0.3",
|
|
81
|
-
"eslint": "^8.
|
|
81
|
+
"eslint": "^8.55.0",
|
|
82
82
|
"eslint-config-standard": "^17.1.0",
|
|
83
|
-
"eslint-plugin-import": "^2.
|
|
84
|
-
"eslint-plugin-mocha": "^10.
|
|
85
|
-
"eslint-plugin-n": "^16.
|
|
83
|
+
"eslint-plugin-import": "^2.29.0",
|
|
84
|
+
"eslint-plugin-mocha": "^10.2.0",
|
|
85
|
+
"eslint-plugin-n": "^16.4.0",
|
|
86
86
|
"eslint-plugin-promise": "^6.1.1",
|
|
87
87
|
"eslint-plugin-standard": "^4.1.0",
|
|
88
88
|
"mocha": "^10.2.0",
|
|
89
|
-
"nock": "^13.
|
|
90
|
-
"npm-check-updates": "^16.
|
|
89
|
+
"nock": "^13.4.0",
|
|
90
|
+
"npm-check-updates": "^16.14.11",
|
|
91
91
|
"nyc": "^15.1.0",
|
|
92
92
|
"rollup": "2.79.1",
|
|
93
93
|
"rollup-plugin-babel": "^4.4.0",
|
package/src/Capabilities.js
CHANGED
|
@@ -157,6 +157,8 @@ module.exports = {
|
|
|
157
157
|
// varnames, testcasenames
|
|
158
158
|
SCRIPTING_MEMORY_COLUMN_MODE: 'SCRIPTING_MEMORY_COLUMN_MODE',
|
|
159
159
|
// Botium Lifecycle Hooks
|
|
160
|
+
SCRIPTING_CONVO_STEP_PARAMETERS: 'SCRIPTING_CONVO_STEP_PARAMETERS',
|
|
161
|
+
// Botium Lifecycle Hooks
|
|
160
162
|
CUSTOMHOOK_ONBUILD: 'CUSTOMHOOK_ONBUILD',
|
|
161
163
|
CUSTOMHOOK_ONSTART: 'CUSTOMHOOK_ONSTART',
|
|
162
164
|
CUSTOMHOOK_ONUSERSAYS: 'CUSTOMHOOK_ONUSERSAYS',
|
|
@@ -90,6 +90,43 @@ const BotiumError = class BotiumError extends Error {
|
|
|
90
90
|
return null
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
hasError ({ type, source }) {
|
|
95
|
+
if (this.context) {
|
|
96
|
+
const errArr = _.isArray(this.context) ? this.context : [this.context]
|
|
97
|
+
for (const err of errArr) {
|
|
98
|
+
if (err.type === 'list') {
|
|
99
|
+
for (const internal of err.errors) {
|
|
100
|
+
if ((!type || internal.type === type) && (!source || internal.source === source)) {
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if ((!type || err.type === type) && (!source || err.source === source)) {
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
toArray () {
|
|
115
|
+
if (this.context) {
|
|
116
|
+
let result = []
|
|
117
|
+
const errArr = _.isArray(this.context) ? this.context : [this.context]
|
|
118
|
+
for (const err of errArr) {
|
|
119
|
+
if (err.type === 'list') {
|
|
120
|
+
result = result.concat(err.errors)
|
|
121
|
+
} else {
|
|
122
|
+
result.push(err)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return result
|
|
126
|
+
} else {
|
|
127
|
+
return []
|
|
128
|
+
}
|
|
129
|
+
}
|
|
93
130
|
}
|
|
94
131
|
|
|
95
132
|
const _getChildErrorsFromContext = (context) => {
|
|
@@ -99,11 +136,11 @@ const _getChildErrorsFromContext = (context) => {
|
|
|
99
136
|
return false
|
|
100
137
|
}
|
|
101
138
|
|
|
102
|
-
const botiumErrorFromErr = (message, err) => {
|
|
139
|
+
const botiumErrorFromErr = (message, err, context = {}) => {
|
|
103
140
|
if (err instanceof BotiumError) {
|
|
104
|
-
return new BotiumError(message, err.context, true)
|
|
141
|
+
return new BotiumError(message, { ...err.context, ...context }, true)
|
|
105
142
|
} else {
|
|
106
|
-
return new BotiumError(message, { err }, true)
|
|
143
|
+
return new BotiumError(message, { err, ...context }, true)
|
|
107
144
|
}
|
|
108
145
|
}
|
|
109
146
|
|
package/src/scripting/Convo.js
CHANGED
|
@@ -275,8 +275,57 @@ class Convo {
|
|
|
275
275
|
let botMsg = null
|
|
276
276
|
let waitForBotSays = true
|
|
277
277
|
let skipTranscriptStep = false
|
|
278
|
-
|
|
278
|
+
let conditionalGroupId = null
|
|
279
|
+
let conditionMetInGroup = false
|
|
280
|
+
let globalConvoStepParameters = container.caps[Capabilities.SCRIPTING_CONVO_STEP_PARAMETERS] || {}
|
|
281
|
+
let retryBotMessageTimeoutEnd = null
|
|
282
|
+
let retryBotMessageConvoId = null
|
|
283
|
+
let retryBotMessageDropBotResponse = false
|
|
284
|
+
for (let i = 0; i < this.conversation.length; i = (retryBotMessageDropBotResponse ? i : i + 1)) {
|
|
285
|
+
retryBotMessageDropBotResponse = false
|
|
279
286
|
const convoStep = this.conversation[i]
|
|
287
|
+
const rawConvoStepParameters = convoStep.logicHooks.find(lh => lh.name === 'CONVO_STEP_PARAMETERS')?.args
|
|
288
|
+
let convoStepParameters = {}
|
|
289
|
+
if (rawConvoStepParameters && rawConvoStepParameters.length) {
|
|
290
|
+
let params
|
|
291
|
+
if (rawConvoStepParameters[0].trim().startsWith('{')) {
|
|
292
|
+
try {
|
|
293
|
+
params = JSON.parse(rawConvoStepParameters[0])
|
|
294
|
+
} catch (e) {
|
|
295
|
+
debug(`${this.header.name}/${convoStep.stepTag}: Failed to parse convo step parameters from JSON ${rawConvoStepParameters[0]}`)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!params || !Object.keys(params).length) {
|
|
299
|
+
params = {}
|
|
300
|
+
for (const param of rawConvoStepParameters) {
|
|
301
|
+
const semicolon = param.indexOf(':')
|
|
302
|
+
if (semicolon) {
|
|
303
|
+
try {
|
|
304
|
+
const name = param.substring(0, semicolon)
|
|
305
|
+
const value = param.substring(semicolon + 1)
|
|
306
|
+
params[name] = value
|
|
307
|
+
} catch (e) {
|
|
308
|
+
debug(`${this.header.name}/${convoStep.stepTag}: Failed to parse convo step parameter from arg ${param}`)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (convoStep.sender === 'begin') {
|
|
315
|
+
globalConvoStepParameters = Object.assign({}, globalConvoStepParameters || {}, params)
|
|
316
|
+
} else {
|
|
317
|
+
convoStepParameters = Object.assign({}, globalConvoStepParameters || {}, params)
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
if (convoStep.sender !== 'begin') {
|
|
321
|
+
convoStepParameters = globalConvoStepParameters
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (Object.keys(convoStepParameters).length) {
|
|
326
|
+
debug(`${this.header.name}: using convo step parameters ${JSON.stringify(convoStepParameters)}`)
|
|
327
|
+
}
|
|
328
|
+
|
|
280
329
|
const currentStepIndex = i
|
|
281
330
|
container.eventEmitter.emit(Events.CONVO_STEP_NEXT, container, convoStep, i)
|
|
282
331
|
skipTranscriptStep = false
|
|
@@ -317,8 +366,8 @@ class Convo {
|
|
|
317
366
|
const coreMsg = _.omit(removeBuffers(meMsg), ['sourceData'])
|
|
318
367
|
debug(`${this.header.name}/${convoStep.stepTag}: user says (cleaned by binary and base64 data and sourceData) ${JSON.stringify(coreMsg, null, 2)}`)
|
|
319
368
|
await new Promise(resolve => {
|
|
320
|
-
if (container.caps.SIMULATE_WRITING_SPEED && meMsg.messageText && meMsg.messageText.length) {
|
|
321
|
-
setTimeout(() => resolve(), container.caps.SIMULATE_WRITING_SPEED * meMsg.messageText.length)
|
|
369
|
+
if (container.caps[Capabilities.SIMULATE_WRITING_SPEED] && meMsg.messageText && meMsg.messageText.length) {
|
|
370
|
+
setTimeout(() => resolve(), container.caps[Capabilities.SIMULATE_WRITING_SPEED] * meMsg.messageText.length)
|
|
322
371
|
} else {
|
|
323
372
|
resolve()
|
|
324
373
|
}
|
|
@@ -356,7 +405,6 @@ class Convo {
|
|
|
356
405
|
throw failErr
|
|
357
406
|
}
|
|
358
407
|
} else if (convoStep.sender === 'bot') {
|
|
359
|
-
const previousWaitForBotSays = waitForBotSays
|
|
360
408
|
if (waitForBotSays) {
|
|
361
409
|
botMsg = null
|
|
362
410
|
} else {
|
|
@@ -406,22 +454,38 @@ class Convo {
|
|
|
406
454
|
}
|
|
407
455
|
|
|
408
456
|
if (convoStep.conditional) {
|
|
457
|
+
waitForBotSays = false
|
|
458
|
+
let endOfConditionalGroup = false
|
|
459
|
+
conditionalGroupId = convoStep.logicHooks.find(lh => lh.name.startsWith('CONDITIONAL_STEP')).args[1]
|
|
409
460
|
const nextConvoStep = this.conversation[i + 1]
|
|
410
461
|
|
|
411
|
-
if (!previousWaitForBotSays) {
|
|
412
|
-
skipTranscriptStep = true
|
|
413
|
-
}
|
|
414
|
-
waitForBotSays = false
|
|
415
462
|
if (!nextConvoStep || nextConvoStep.sender !== 'bot' || !nextConvoStep.logicHooks || !nextConvoStep.logicHooks.some(lh => lh.name.toUpperCase().startsWith('CONDITIONAL_STEP'))) {
|
|
416
|
-
|
|
463
|
+
endOfConditionalGroup = true
|
|
417
464
|
} else {
|
|
418
|
-
const conditionalLogicHook = convoStep.logicHooks.find(lh => lh.name.startsWith('CONDITIONAL_STEP'))
|
|
419
465
|
const nextConditionalLogicHook = nextConvoStep.logicHooks.find(lh => lh.name.startsWith('CONDITIONAL_STEP'))
|
|
420
|
-
|
|
466
|
+
endOfConditionalGroup = conditionalGroupId !== nextConditionalLogicHook.args[1]
|
|
421
467
|
}
|
|
422
468
|
|
|
423
|
-
if (convoStep.conditional.skip) {
|
|
469
|
+
if (convoStep.conditional.skip || conditionMetInGroup) {
|
|
470
|
+
skipTranscriptStep = true
|
|
471
|
+
if (endOfConditionalGroup && !conditionMetInGroup && !convoStep.optional) {
|
|
472
|
+
const failErr = new BotiumError(`${this.header.name}/${convoStep.stepTag}: Non of the conditions are met in ${conditionalGroupId ? `'${conditionalGroupId}' ` : ''}condition group`)
|
|
473
|
+
debug(failErr)
|
|
474
|
+
throw failErr
|
|
475
|
+
}
|
|
476
|
+
if (endOfConditionalGroup) {
|
|
477
|
+
waitForBotSays = !convoStep.optional
|
|
478
|
+
conditionalGroupId = undefined
|
|
479
|
+
conditionMetInGroup = false
|
|
480
|
+
}
|
|
424
481
|
continue
|
|
482
|
+
} else {
|
|
483
|
+
conditionMetInGroup = true
|
|
484
|
+
if (endOfConditionalGroup) {
|
|
485
|
+
waitForBotSays = !convoStep.optional
|
|
486
|
+
conditionalGroupId = undefined
|
|
487
|
+
conditionMetInGroup = false
|
|
488
|
+
}
|
|
425
489
|
}
|
|
426
490
|
}
|
|
427
491
|
|
|
@@ -436,11 +500,32 @@ class Convo {
|
|
|
436
500
|
}
|
|
437
501
|
const isErrorHandledWithOptionConvoStep = (err) => {
|
|
438
502
|
const nextConvoStep = this.conversation[i + 1]
|
|
503
|
+
const retryConfig = convoStepParameters?.ignoreNotMatchedBotResponses
|
|
504
|
+
const retryOn = convoStep.sender === 'bot' && retryConfig && retryConfig.timeout && retryConfig.mainAsserter
|
|
439
505
|
if (convoStep.optional && nextConvoStep && nextConvoStep.sender === 'bot') {
|
|
506
|
+
if (retryOn) {
|
|
507
|
+
debug(`${this.header.name}/${convoStep.stepTag}: Retry failed asserter is ignored on optional convo`)
|
|
508
|
+
}
|
|
440
509
|
waitForBotSays = false
|
|
441
510
|
skipTranscriptStep = true
|
|
442
511
|
return true
|
|
512
|
+
} else if (retryOn) {
|
|
513
|
+
if (!retryBotMessageTimeoutEnd || retryBotMessageConvoId !== convoStep.stepTag) {
|
|
514
|
+
retryBotMessageTimeoutEnd = transcriptStep.stepBegin.getTime() + +retryConfig.timeout
|
|
515
|
+
retryBotMessageConvoId = convoStep.stepTag
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const now = new Date().getTime()
|
|
519
|
+
const timeoutRemaining = retryBotMessageTimeoutEnd - now
|
|
520
|
+
if (timeoutRemaining > 0) {
|
|
521
|
+
debug(`${this.header.name}/${convoStep.stepTag}: Convo step retry on, timeout remaining: ${timeoutRemaining}, error: "${err.message}"`)
|
|
522
|
+
retryBotMessageDropBotResponse = true
|
|
523
|
+
return false
|
|
524
|
+
} else {
|
|
525
|
+
debug(`${this.header.name}/${convoStep.stepTag}: Convo step retry on, but timeout is over. error: "${err.message}"`)
|
|
526
|
+
}
|
|
443
527
|
}
|
|
528
|
+
|
|
444
529
|
if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
|
|
445
530
|
assertErrors.push(err)
|
|
446
531
|
return false
|
|
@@ -457,7 +542,7 @@ class Convo {
|
|
|
457
542
|
const tomatch = this._resolveUtterancesToMatch(container, Object.assign({}, scriptingMemoryUpdate, scriptingMemory), messageText, botMsg)
|
|
458
543
|
if (convoStep.not) {
|
|
459
544
|
try {
|
|
460
|
-
this.scriptingEvents.assertBotNotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep)
|
|
545
|
+
this.scriptingEvents.assertBotNotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep, convoStepParameters)
|
|
461
546
|
} catch (err) {
|
|
462
547
|
if (isErrorHandledWithOptionConvoStep(err)) {
|
|
463
548
|
continue
|
|
@@ -465,7 +550,7 @@ class Convo {
|
|
|
465
550
|
}
|
|
466
551
|
} else {
|
|
467
552
|
try {
|
|
468
|
-
this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep)
|
|
553
|
+
this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep, convoStepParameters)
|
|
469
554
|
} catch (err) {
|
|
470
555
|
if (isErrorHandledWithOptionConvoStep(err)) {
|
|
471
556
|
continue
|
|
@@ -474,7 +559,7 @@ class Convo {
|
|
|
474
559
|
}
|
|
475
560
|
} else if (convoStep.sourceData) {
|
|
476
561
|
try {
|
|
477
|
-
this._compareObject(container, scriptingMemory, convoStep, botMsg.sourceData, convoStep.sourceData, botMsg)
|
|
562
|
+
this._compareObject(container, scriptingMemory, convoStep, botMsg.sourceData, convoStep.sourceData, botMsg, convoStepParameters)
|
|
478
563
|
} catch (err) {
|
|
479
564
|
if (isErrorHandledWithOptionConvoStep(err)) {
|
|
480
565
|
continue
|
|
@@ -492,20 +577,46 @@ class Convo {
|
|
|
492
577
|
skipTranscriptStep = true
|
|
493
578
|
continue
|
|
494
579
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
580
|
+
|
|
581
|
+
const errors = err.toArray ? err.toArray() : []
|
|
582
|
+
const retryConfig = convoStepParameters?.ignoreNotMatchedBotResponses
|
|
583
|
+
const retryOn =
|
|
584
|
+
convoStep.sender === 'bot' &&
|
|
585
|
+
retryConfig &&
|
|
586
|
+
retryConfig.timeout &&
|
|
587
|
+
errors.length &&
|
|
588
|
+
errors.filter(({ type, source, asserter }) => type === 'asserter' && (retryConfig.allAsserters || (retryConfig.asserters && retryConfig.asserters.includes(asserter)))).length
|
|
589
|
+
if (retryOn && (!retryBotMessageTimeoutEnd || retryBotMessageConvoId !== convoStep.stepTag)) {
|
|
590
|
+
retryBotMessageTimeoutEnd = transcriptStep.stepBegin.getTime() + +retryConfig.timeout
|
|
591
|
+
retryBotMessageConvoId = convoStep.stepTag
|
|
500
592
|
}
|
|
501
|
-
|
|
502
|
-
|
|
593
|
+
|
|
594
|
+
const now = new Date().getTime()
|
|
595
|
+
const timeoutRemaining = retryOn && (retryBotMessageTimeoutEnd - now)
|
|
596
|
+
if (retryOn && timeoutRemaining > 0) {
|
|
597
|
+
debug(`${this.header.name}/${convoStep.stepTag}: Convo step retry on, timeout remaining: ${timeoutRemaining}, error: "${err.message}"`)
|
|
598
|
+
retryBotMessageDropBotResponse = true
|
|
503
599
|
} else {
|
|
504
|
-
|
|
600
|
+
if (retryOn && timeoutRemaining <= 0) {
|
|
601
|
+
debug(`${this.header.name}/${convoStep.stepTag}: Convo step retry on, but timeout is over. error: "${err.message}"`)
|
|
602
|
+
}
|
|
603
|
+
const failErr = botiumErrorFromErr(`${this.header.name}/${convoStep.stepTag}: assertion error - ${err.message || err}`, err)
|
|
604
|
+
debug(failErr)
|
|
605
|
+
try {
|
|
606
|
+
this.scriptingEvents.fail && this.scriptingEvents.fail(failErr, lastMeConvoStep)
|
|
607
|
+
} catch (failErr) {
|
|
608
|
+
}
|
|
609
|
+
if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS] && err instanceof BotiumError) {
|
|
610
|
+
assertErrors.push(err)
|
|
611
|
+
} else {
|
|
612
|
+
throw failErr
|
|
613
|
+
}
|
|
505
614
|
}
|
|
506
615
|
}
|
|
507
616
|
if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
|
|
508
617
|
if (assertErrors.length > 0) {
|
|
618
|
+
// this has no effect, but logically it has to be false
|
|
619
|
+
retryBotMessageDropBotResponse = false
|
|
509
620
|
throw botiumErrorFromList(assertErrors, {})
|
|
510
621
|
}
|
|
511
622
|
} else {
|
|
@@ -563,7 +674,7 @@ class Convo {
|
|
|
563
674
|
}
|
|
564
675
|
}
|
|
565
676
|
|
|
566
|
-
_compareObject (container, scriptingMemory, convoStep, result, expected, botMsg) {
|
|
677
|
+
_compareObject (container, scriptingMemory, convoStep, result, expected, botMsg, convoStepParameters) {
|
|
567
678
|
if (expected === null || expected === undefined) return
|
|
568
679
|
|
|
569
680
|
if (_.isArray(expected)) {
|
|
@@ -574,12 +685,12 @@ class Convo {
|
|
|
574
685
|
throw new BotiumError(`${this.header.name}/${convoStep.stepTag}: bot response expected array length ${expected.length}, got ${result.length}`)
|
|
575
686
|
}
|
|
576
687
|
for (let i = 0; i < expected.length; i++) {
|
|
577
|
-
this._compareObject(container, scriptingMemory, convoStep, result[i], expected[i])
|
|
688
|
+
this._compareObject(container, scriptingMemory, convoStep, result[i], expected[i], null, convoStepParameters)
|
|
578
689
|
}
|
|
579
690
|
} else if (_.isObject(expected)) {
|
|
580
691
|
_.forOwn(expected, (value, key) => {
|
|
581
692
|
if (Object.prototype.hasOwnProperty.call(result, key)) {
|
|
582
|
-
this._compareObject(container, scriptingMemory, convoStep, result[key], expected[key])
|
|
693
|
+
this._compareObject(container, scriptingMemory, convoStep, result[key], expected[key], null, convoStepParameters)
|
|
583
694
|
} else {
|
|
584
695
|
throw new BotiumError(`${this.header.name}/${convoStep.stepTag}: bot response "${result}" missing expected property: ${key}`)
|
|
585
696
|
}
|
|
@@ -588,7 +699,7 @@ class Convo {
|
|
|
588
699
|
ScriptingMemory.fill(container, scriptingMemory, result, expected, this.scriptingEvents)
|
|
589
700
|
const response = this._checkNormalizeText(container, result)
|
|
590
701
|
const tomatch = this._resolveUtterancesToMatch(container, scriptingMemory, expected, botMsg)
|
|
591
|
-
this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}
|
|
702
|
+
this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, null, convoStepParameters)
|
|
592
703
|
}
|
|
593
704
|
}
|
|
594
705
|
|
|
@@ -656,7 +767,7 @@ class Convo {
|
|
|
656
767
|
}
|
|
657
768
|
|
|
658
769
|
_checkBotRepliesConsumed (container) {
|
|
659
|
-
if (container.caps.SCRIPTING_FORCE_BOT_CONSUMED) {
|
|
770
|
+
if (container.caps[Capabilities.SCRIPTING_FORCE_BOT_CONSUMED]) {
|
|
660
771
|
const queueLength = container._QueueLength()
|
|
661
772
|
if (queueLength === 1) {
|
|
662
773
|
throw new Error('There is an unread bot reply in queue')
|
|
@@ -261,6 +261,13 @@ const _apply = (scriptingMemory, str, caps, mockMsg) => {
|
|
|
261
261
|
return arg
|
|
262
262
|
}
|
|
263
263
|
})
|
|
264
|
+
args = args.map(arg => {
|
|
265
|
+
const argStr = `${arg}`
|
|
266
|
+
if (argStr.startsWith('$')) {
|
|
267
|
+
return scriptingMemory[argStr.substring(1)] || arg
|
|
268
|
+
}
|
|
269
|
+
return arg
|
|
270
|
+
})
|
|
264
271
|
str = str.replace(match, SCRIPTING_FUNCTIONS[key].handler(caps, ...args, mockMsg))
|
|
265
272
|
} else {
|
|
266
273
|
str = str.replace(match, SCRIPTING_FUNCTIONS[key].handler(caps))
|
|
@@ -92,37 +92,37 @@ module.exports = class ScriptingProvider {
|
|
|
92
92
|
|
|
93
93
|
this.scriptingEvents = {
|
|
94
94
|
onConvoBegin: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
95
|
-
return this._createLogicHookPromises({ hookType: 'onConvoBegin', logicHooks: (convo
|
|
95
|
+
return this._createLogicHookPromises({ hookType: 'onConvoBegin', logicHooks: (convo?.beginLogicHook || []), convo, convoStep, scriptingMemory, ...rest })
|
|
96
96
|
},
|
|
97
97
|
onConvoEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
98
|
-
return this._createLogicHookPromises({ hookType: 'onConvoEnd', logicHooks: (convo
|
|
98
|
+
return this._createLogicHookPromises({ hookType: 'onConvoEnd', logicHooks: (convo?.endLogicHook || []), convo, convoStep, scriptingMemory, ...rest })
|
|
99
99
|
},
|
|
100
100
|
onMeStart: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
101
|
-
return this._createLogicHookPromises({ hookType: 'onMeStart', logicHooks: (convoStep
|
|
101
|
+
return this._createLogicHookPromises({ hookType: 'onMeStart', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
|
|
102
102
|
},
|
|
103
103
|
onMePrepare: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
104
|
-
return this._createLogicHookPromises({ hookType: 'onMePrepare', logicHooks: (convoStep
|
|
104
|
+
return this._createLogicHookPromises({ hookType: 'onMePrepare', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
|
|
105
105
|
},
|
|
106
106
|
onMeEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
107
|
-
return this._createLogicHookPromises({ hookType: 'onMeEnd', logicHooks: (convoStep
|
|
107
|
+
return this._createLogicHookPromises({ hookType: 'onMeEnd', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
|
|
108
108
|
},
|
|
109
109
|
onBotStart: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
110
|
-
return this._createLogicHookPromises({ hookType: 'onBotStart', logicHooks: (convoStep
|
|
110
|
+
return this._createLogicHookPromises({ hookType: 'onBotStart', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
|
|
111
111
|
},
|
|
112
112
|
onBotPrepare: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
113
|
-
return this._createLogicHookPromises({ hookType: 'onBotPrepare', logicHooks: (convoStep
|
|
113
|
+
return this._createLogicHookPromises({ hookType: 'onBotPrepare', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
|
|
114
114
|
},
|
|
115
115
|
onBotEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
116
|
-
return this._createLogicHookPromises({ hookType: 'onBotEnd', logicHooks: (convoStep
|
|
116
|
+
return this._createLogicHookPromises({ hookType: 'onBotEnd', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
|
|
117
117
|
},
|
|
118
118
|
assertConvoBegin: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
119
|
-
return this._createAsserterPromises({ asserterType: 'assertConvoBegin', asserters: (convo
|
|
119
|
+
return this._createAsserterPromises({ asserterType: 'assertConvoBegin', asserters: (convo?.beginAsserter || []), convo, convoStep, scriptingMemory, ...rest })
|
|
120
120
|
},
|
|
121
121
|
assertConvoStep: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
122
|
-
return this._createAsserterPromises({ asserterType: 'assertConvoStep', asserters: (convoStep
|
|
122
|
+
return this._createAsserterPromises({ asserterType: 'assertConvoStep', asserters: (convoStep?.asserters || []), convo, convoStep, scriptingMemory, ...rest })
|
|
123
123
|
},
|
|
124
124
|
assertConvoEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
125
|
-
return this._createAsserterPromises({ asserterType: 'assertConvoEnd', asserters: (convo
|
|
125
|
+
return this._createAsserterPromises({ asserterType: 'assertConvoEnd', asserters: (convo?.endAsserter || []), convo, convoStep, scriptingMemory, ...rest })
|
|
126
126
|
},
|
|
127
127
|
setUserInput: ({ convo, convoStep, scriptingMemory, ...rest }) => {
|
|
128
128
|
return this._createUserInputPromises({ convo, convoStep, scriptingMemory, ...rest })
|
|
@@ -130,12 +130,13 @@ module.exports = class ScriptingProvider {
|
|
|
130
130
|
resolveUtterance: ({ utterance, resolveEmptyIfUnknown }) => {
|
|
131
131
|
return this._resolveUtterance({ utterance, resolveEmptyIfUnknown })
|
|
132
132
|
},
|
|
133
|
-
assertBotResponse: (botresponse, tomatch, stepTag, meMsg) => {
|
|
133
|
+
assertBotResponse: (botresponse, tomatch, stepTag, meMsg, convoStepParameters) => {
|
|
134
134
|
if (!_.isArray(tomatch)) {
|
|
135
135
|
tomatch = [tomatch]
|
|
136
136
|
}
|
|
137
137
|
debug(`assertBotResponse ${stepTag} ${meMsg ? `(${meMsg}) ` : ''}BOT: ${botresponse} = ${tomatch} ...`)
|
|
138
|
-
const
|
|
138
|
+
const matchFn = convoStepParameters.matchingMode ? (getMatchFunction(convoStepParameters.matchingMode) || this.matchFn) : this.matchFn
|
|
139
|
+
const found = _.find(tomatch, (utt) => matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]))
|
|
139
140
|
const asserterType = this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer' ? 'Word Error Rate Asserter' : 'Text Match Asserter'
|
|
140
141
|
if (_.isNil(found)) {
|
|
141
142
|
if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer') {
|
|
@@ -191,12 +192,13 @@ module.exports = class ScriptingProvider {
|
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
},
|
|
194
|
-
assertBotNotResponse: (botresponse, nottomatch, stepTag, meMsg) => {
|
|
195
|
+
assertBotNotResponse: (botresponse, nottomatch, stepTag, meMsg, convoStepParameters) => {
|
|
195
196
|
if (!_.isArray(nottomatch)) {
|
|
196
197
|
nottomatch = [nottomatch]
|
|
197
198
|
}
|
|
198
199
|
debug(`assertBotNotResponse ${stepTag} ${meMsg ? `(${meMsg}) ` : ''}BOT: ${botresponse} != ${nottomatch} ...`)
|
|
199
|
-
const
|
|
200
|
+
const matchFn = convoStepParameters.matchingMode ? (getMatchFunction(convoStepParameters.matchingMode) || this.matchFn) : this.matchFn
|
|
201
|
+
const found = _.find(nottomatch, (utt) => matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]))
|
|
200
202
|
const asserterType = this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer' ? 'Word Error Rate Asserter' : 'Text Match Asserter'
|
|
201
203
|
if (!_.isNil(found)) {
|
|
202
204
|
if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer') {
|
|
@@ -270,6 +272,22 @@ module.exports = class ScriptingProvider {
|
|
|
270
272
|
assertConvoStep: 'assertNotConvoStep',
|
|
271
273
|
assertConvoEnd: 'assertNotConvoEnd'
|
|
272
274
|
}
|
|
275
|
+
const updateExceptionContext = (promise, asserter) => {
|
|
276
|
+
const updateError = (err) => {
|
|
277
|
+
if (err instanceof BotiumError) {
|
|
278
|
+
if (!err.context) {
|
|
279
|
+
err.context = {}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
err.context.asserter = asserter.name
|
|
283
|
+
|
|
284
|
+
throw err
|
|
285
|
+
} else {
|
|
286
|
+
throw botiumErrorFromErr(_.isString(err) ? err : err.message, err, { asserter: asserter.name })
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return promise.catch(err => updateError(err))
|
|
290
|
+
}
|
|
273
291
|
const callAsserter = (asserterSpec, asserter, params) => {
|
|
274
292
|
if (asserterSpec.not) {
|
|
275
293
|
const notAsserterType = mapNot[asserterType]
|
|
@@ -301,18 +319,35 @@ module.exports = class ScriptingProvider {
|
|
|
301
319
|
|
|
302
320
|
const convoAsserter = asserters
|
|
303
321
|
.filter(a => this.asserters[a.name][asserterType])
|
|
304
|
-
.map(a =>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
322
|
+
.map(a => ({
|
|
323
|
+
asserter: a,
|
|
324
|
+
promise: callAsserter(a, this.asserters[a.name], {
|
|
325
|
+
convo,
|
|
326
|
+
convoStep,
|
|
327
|
+
scriptingMemory,
|
|
328
|
+
container,
|
|
329
|
+
args: ScriptingMemory.applyToArgs(a.args, scriptingMemory, container.caps, rest.botMsg),
|
|
330
|
+
isGlobal: false,
|
|
331
|
+
...rest
|
|
332
|
+
})
|
|
312
333
|
}))
|
|
334
|
+
.map(({ promise, asserter }) => updateExceptionContext(promise, asserter))
|
|
335
|
+
|
|
313
336
|
const globalAsserter = Object.values(this.globalAsserter)
|
|
314
337
|
.filter(a => a[asserterType])
|
|
315
|
-
.map(a =>
|
|
338
|
+
.map(a => ({
|
|
339
|
+
asserter: a,
|
|
340
|
+
promise: p(this.retryHelperAsserter, () => a[asserterType]({
|
|
341
|
+
convo,
|
|
342
|
+
convoStep,
|
|
343
|
+
scriptingMemory,
|
|
344
|
+
container,
|
|
345
|
+
args: [],
|
|
346
|
+
isGlobal: true,
|
|
347
|
+
...rest
|
|
348
|
+
}))
|
|
349
|
+
}))
|
|
350
|
+
.map(({ promise, asserter }) => updateExceptionContext(promise, asserter))
|
|
316
351
|
|
|
317
352
|
const allPromises = [...convoAsserter, ...globalAsserter]
|
|
318
353
|
if (this.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
|
|
@@ -335,8 +370,10 @@ module.exports = class ScriptingProvider {
|
|
|
335
370
|
throw Error(`Unknown hookType ${hookType}`)
|
|
336
371
|
}
|
|
337
372
|
|
|
338
|
-
const
|
|
373
|
+
const localHooks = (logicHooks || [])
|
|
339
374
|
.filter(l => this.logicHooks[l.name][hookType])
|
|
375
|
+
|
|
376
|
+
const convoStepPromises = localHooks
|
|
340
377
|
.map(l => p(this.retryHelperLogicHook, () => this.logicHooks[l.name][hookType]({
|
|
341
378
|
convo,
|
|
342
379
|
convoStep,
|
|
@@ -347,17 +384,24 @@ module.exports = class ScriptingProvider {
|
|
|
347
384
|
...rest
|
|
348
385
|
})))
|
|
349
386
|
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
.map(l => p(this.retryHelperLogicHook, () => l[hookType]({ convo, convoStep, scriptingMemory, container, args: [], isGlobal: true, ...rest })))
|
|
387
|
+
const globalHooks = Object.values(this.globalLogicHook).filter(l => l[hookType])
|
|
388
|
+
const globalPromises = globalHooks.map(l => p(this.retryHelperLogicHook, () => l[hookType]({ convo, convoStep, scriptingMemory, container, args: [], isGlobal: true, ...rest })))
|
|
353
389
|
|
|
354
390
|
const allPromises = [...convoStepPromises, ...globalPromises]
|
|
355
|
-
|
|
391
|
+
|
|
392
|
+
if (allPromises.length > 0) {
|
|
393
|
+
return Promise.all(allPromises).then(() => {
|
|
394
|
+
return {
|
|
395
|
+
// just returning some humanreadable
|
|
396
|
+
hooks: [...localHooks, ...globalHooks].map(h => h.name || h.context?.ref || JSON.stringify(h))
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
}
|
|
356
400
|
return Promise.resolve(false)
|
|
357
401
|
}
|
|
358
402
|
|
|
359
403
|
_createUserInputPromises ({ convo, convoStep, scriptingMemory, container, ...rest }) {
|
|
360
|
-
const convoStepPromises = (convoStep
|
|
404
|
+
const convoStepPromises = (convoStep?.userInputs || [])
|
|
361
405
|
.filter(ui => this.userInputs[ui.name])
|
|
362
406
|
.map(ui => p(this.retryHelperUserInput, () => this.userInputs[ui.name].setUserInput({
|
|
363
407
|
convo,
|
|
@@ -424,6 +468,11 @@ module.exports = class ScriptingProvider {
|
|
|
424
468
|
}
|
|
425
469
|
}
|
|
426
470
|
|
|
471
|
+
// Livechat, and crawler using logichooks too. So they need script context
|
|
472
|
+
BuildScriptContext () {
|
|
473
|
+
return this._buildScriptContext()
|
|
474
|
+
}
|
|
475
|
+
|
|
427
476
|
Build () {
|
|
428
477
|
const CompilerXlsx = require('./CompilerXlsx')
|
|
429
478
|
this.compilers[Constants.SCRIPTING_FORMAT_XSLX] = new CompilerXlsx(this._buildScriptContext(), this.caps)
|