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