botium-core 1.12.5 → 1.13.1
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 +277 -171
- package/dist/botium-cjs.js.map +1 -1
- package/dist/botium-es.js +296 -190
- package/dist/botium-es.js.map +1 -1
- package/package.json +24 -24
- package/src/Capabilities.js +7 -0
- package/src/helpers/RetryHelper.js +13 -7
- package/src/scripting/CompilerCsv.js +150 -102
- package/src/scripting/Convo.js +21 -1
- package/src/scripting/ScriptingProvider.js +5 -1
- package/src/scripting/helper.js +19 -12
- package/src/scripting/logichook/LogicHookUtils.js +1 -1
- package/src/scripting/logichook/asserter/ButtonsAsserter.js +4 -2
- package/src/scripting/logichook/asserter/CardsAsserter.js +4 -2
- package/test/compiler/compilercsv.spec.js +363 -12
- package/test/compiler/compilertxt.spec.js +13 -0
- package/test/compiler/convos/csv/utterances_liveperson.csv +108 -0
- package/test/compiler/convos/csv/utterances_multicolumn3col.csv +3 -0
- package/test/compiler/convos/csv/utterances_multicolumn5col.csv +3 -0
- package/test/compiler/convos/csv/utterances_singlecolumn.csv +3 -0
- package/test/compiler/convos/csv/utterances_variable_row_len.csv +3 -0
- package/test/compiler/convos/txt/convos_args_escape.convo.txt +2 -0
- package/test/convo/convos/continuefailing_timeout.convo.txt +16 -0
- package/test/convo/retryconvo.spec.js +134 -0
- package/test/convo/transcript.spec.js +18 -1
- package/test/logichooks/hookfromsrc.spec.js +1 -1
- package/test/scripting/asserters/buttonsAsserter.spec.js +47 -0
- package/test/scripting/asserters/cardsAsserter.spec.js +39 -0
- package/test/scripting/txt/decompile.spec.js +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botium-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.1",
|
|
4
4
|
"description": "The Selenium for Chatbots",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "dist/botium-es.js",
|
|
@@ -32,25 +32,25 @@
|
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://www.botium.ai",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@babel/runtime": "^7.
|
|
36
|
-
"async": "^3.2.
|
|
35
|
+
"@babel/runtime": "^7.18.6",
|
|
36
|
+
"async": "^3.2.4",
|
|
37
37
|
"body-parser": "^1.20.0",
|
|
38
38
|
"boolean": "^3.2.0",
|
|
39
39
|
"bottleneck": "^2.19.5",
|
|
40
|
-
"csv-parse": "^5.0
|
|
40
|
+
"csv-parse": "^5.3.0",
|
|
41
41
|
"debug": "^4.3.4",
|
|
42
42
|
"esprima": "^4.0.1",
|
|
43
|
-
"express": "^4.
|
|
43
|
+
"express": "^4.18.1",
|
|
44
44
|
"globby": "11.0.4",
|
|
45
|
-
"ioredis": "^5.0
|
|
45
|
+
"ioredis": "^5.1.0",
|
|
46
46
|
"is-class": "^0.0.9",
|
|
47
47
|
"is-json": "^2.0.1",
|
|
48
48
|
"jsonpath": "^1.1.1",
|
|
49
49
|
"lodash": "^4.17.21",
|
|
50
|
-
"markdown-it": "^
|
|
50
|
+
"markdown-it": "^13.0.1",
|
|
51
51
|
"mime-types": "^2.1.35",
|
|
52
52
|
"mkdirp": "^1.0.4",
|
|
53
|
-
"moment": "^2.29.
|
|
53
|
+
"moment": "^2.29.4",
|
|
54
54
|
"mustache": "^4.2.0",
|
|
55
55
|
"promise-retry": "^2.0.1",
|
|
56
56
|
"promise.allsettled": "^1.0.5",
|
|
@@ -59,39 +59,39 @@
|
|
|
59
59
|
"rimraf": "^3.0.2",
|
|
60
60
|
"sanitize-filename": "^1.6.3",
|
|
61
61
|
"slugify": "^1.6.5",
|
|
62
|
-
"socket.io": "^4.
|
|
63
|
-
"socket.io-client": "^4.
|
|
62
|
+
"socket.io": "^4.5.1",
|
|
63
|
+
"socket.io-client": "^4.5.1",
|
|
64
64
|
"socketio-auth": "^0.1.1",
|
|
65
65
|
"swagger-jsdoc": "^6.2.1",
|
|
66
|
-
"swagger-ui-express": "^4.
|
|
66
|
+
"swagger-ui-express": "^4.4.0",
|
|
67
67
|
"uuid": "^8.3.2",
|
|
68
|
-
"vm2": "^3.9.
|
|
68
|
+
"vm2": "^3.9.10",
|
|
69
69
|
"write-yaml": "^1.0.0",
|
|
70
70
|
"xlsx": "^0.18.5",
|
|
71
|
-
"xregexp": "^5.1.
|
|
72
|
-
"yaml": "^2.
|
|
71
|
+
"xregexp": "^5.1.1",
|
|
72
|
+
"yaml": "^2.1.1"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@babel/core": "^7.
|
|
76
|
-
"@babel/node": "^7.
|
|
77
|
-
"@babel/plugin-transform-runtime": "^7.
|
|
78
|
-
"@babel/preset-env": "^7.
|
|
75
|
+
"@babel/core": "^7.18.6",
|
|
76
|
+
"@babel/node": "^7.18.6",
|
|
77
|
+
"@babel/plugin-transform-runtime": "^7.18.6",
|
|
78
|
+
"@babel/preset-env": "^7.18.6",
|
|
79
79
|
"chai": "^4.3.6",
|
|
80
80
|
"chai-as-promised": "^7.1.1",
|
|
81
81
|
"cross-env": "^7.0.3",
|
|
82
|
-
"eslint": "^8.
|
|
82
|
+
"eslint": "^8.19.0",
|
|
83
83
|
"eslint-config-standard": "^17.0.0",
|
|
84
84
|
"eslint-plugin-import": "^2.26.0",
|
|
85
|
-
"eslint-plugin-n": "^15.
|
|
85
|
+
"eslint-plugin-n": "^15.2.4",
|
|
86
86
|
"eslint-plugin-promise": "^6.0.0",
|
|
87
87
|
"eslint-plugin-standard": "^4.1.0",
|
|
88
88
|
"license-checker": "^25.0.1",
|
|
89
89
|
"license-compatibility-checker": "^0.3.5",
|
|
90
|
-
"mocha": "^
|
|
91
|
-
"nock": "^13.2.
|
|
92
|
-
"npm-check-updates": "^
|
|
90
|
+
"mocha": "^10.0.0",
|
|
91
|
+
"nock": "^13.2.8",
|
|
92
|
+
"npm-check-updates": "^15.2.6",
|
|
93
93
|
"nyc": "^15.1.0",
|
|
94
|
-
"rollup": "^2.
|
|
94
|
+
"rollup": "^2.76.0",
|
|
95
95
|
"rollup-plugin-babel": "^4.4.0",
|
|
96
96
|
"rollup-plugin-commonjs": "^10.1.0",
|
|
97
97
|
"rollup-plugin-json": "^4.0.0",
|
package/src/Capabilities.js
CHANGED
|
@@ -105,6 +105,10 @@ module.exports = {
|
|
|
105
105
|
SCRIPTING_XLSX_SHEETNAMES_PCONVOS: 'SCRIPTING_XLSX_SHEETNAMES_PCONVOS',
|
|
106
106
|
SCRIPTING_XLSX_SHEETNAMES_UTTERANCES: 'SCRIPTING_XLSX_SHEETNAMES_UTTERANCES',
|
|
107
107
|
SCRIPTING_XLSX_SHEETNAMES_SCRIPTING_MEMORY: 'SCRIPTING_XLSX_SHEETNAMES_SCRIPTING_MEMORY',
|
|
108
|
+
// hidden capability. All newly in Box created testsets will have this as true. CsvCompiler
|
|
109
|
+
// - throws less error (Box reads csv files as utterances, and convo. Compiler cant throw exception if a file is correct, but box tries to load it with incorrect script type)
|
|
110
|
+
// 4 or more colums are compiled just as utterances.
|
|
111
|
+
SCRIPTING_CSV_LEGACY_MODE_OFF: 'SCRIPTING_CSV_LEGACY_MODE_OFF',
|
|
108
112
|
SCRIPTING_CSV_DELIMITER: 'SCRIPTING_CSV_DELIMITER',
|
|
109
113
|
SCRIPTING_CSV_SKIP_HEADER: 'SCRIPTING_CSV_SKIP_HEADER',
|
|
110
114
|
SCRIPTING_CSV_QUOTE: 'SCRIPTING_CSV_QUOTE',
|
|
@@ -114,6 +118,9 @@ module.exports = {
|
|
|
114
118
|
SCRIPTING_CSV_MULTIROW_COLUMN_TEXT: 'SCRIPTING_CSV_MULTIROW_COLUMN_TEXT',
|
|
115
119
|
SCRIPTING_CSV_QA_COLUMN_QUESTION: 'SCRIPTING_CSV_QA_COLUMN_QUESTION',
|
|
116
120
|
SCRIPTING_CSV_QA_COLUMN_ANSWER: 'SCRIPTING_CSV_QA_COLUMN_ANSWER',
|
|
121
|
+
SCRIPTING_CSV_UTTERANCE_STARTROW: 'SCRIPTING_CSV_UTTERANCE_STARTROW',
|
|
122
|
+
SCRIPTING_CSV_UTTERANCE_STARTROW_HEADER: 'SCRIPTING_CSV_UTTERANCE_STARTROW_HEADER',
|
|
123
|
+
SCRIPTING_CSV_UTTERANCE_STOP_ON_EMPTY: 'SCRIPTING_CSV_UTTERANCE_STOP_ON_EMPTY',
|
|
117
124
|
SCRIPTING_NORMALIZE_TEXT: 'SCRIPTING_NORMALIZE_TEXT',
|
|
118
125
|
SCRIPTING_ENABLE_MEMORY: 'SCRIPTING_ENABLE_MEMORY',
|
|
119
126
|
SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS: 'SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS',
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
const util = require('util')
|
|
2
2
|
const _ = require('lodash')
|
|
3
|
+
const debug = require('debug')('botium-core-RetryHelper')
|
|
3
4
|
|
|
4
5
|
module.exports = class RetryHelper {
|
|
5
|
-
constructor (caps, section) {
|
|
6
|
-
this.retrySettings = {
|
|
7
|
-
retries: caps[`RETRY_${section.toUpperCase()}_NUMRETRIES`] || 1,
|
|
8
|
-
factor: caps[`RETRY_${section.toUpperCase()}_FACTOR`] || 1,
|
|
9
|
-
minTimeout: caps[`RETRY_${section.toUpperCase()}_MINTIMEOUT`] || 1000
|
|
10
|
-
}
|
|
6
|
+
constructor (caps, section, options = {}) {
|
|
11
7
|
this.retryErrorPatterns = []
|
|
12
8
|
const onErrorRegexp = caps[`RETRY_${section.toUpperCase()}_ONERROR_REGEXP`] || []
|
|
13
9
|
if (onErrorRegexp) {
|
|
@@ -22,10 +18,20 @@ module.exports = class RetryHelper {
|
|
|
22
18
|
this.retryErrorPatterns.push(onErrorRegexp)
|
|
23
19
|
}
|
|
24
20
|
}
|
|
21
|
+
|
|
22
|
+
// to turn on retries, NUMRETRIES or ONERROR_REGEXP has to be set
|
|
23
|
+
this.retrySettings = {
|
|
24
|
+
retries: caps[`RETRY_${section.toUpperCase()}_NUMRETRIES`] || (!_.isNil(options.numRetries) ? options.numRetries : (this.retryErrorPatterns.length === 0) ? 0 : 1),
|
|
25
|
+
factor: caps[`RETRY_${section.toUpperCase()}_FACTOR`] || (_.isNil(options.factor) ? 1 : options.factor),
|
|
26
|
+
minTimeout: caps[`RETRY_${section.toUpperCase()}_MINTIMEOUT`] || (_.isNil(options.minTimeout) ? 1000 : options.minTimeout)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
debug(`Retry for ${section} is ${this.retrySettings.retries > 0 ? 'enabled' : 'disabled'}. Settings: ${JSON.stringify(this.retrySettings)} Patterns: ${JSON.stringify(this.retryErrorPatterns.map(r => r.toString()))}`)
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
shouldRetry (err) {
|
|
28
|
-
if (!err
|
|
33
|
+
if (!err) return false
|
|
34
|
+
if (this.retryErrorPatterns.length === 0) return true
|
|
29
35
|
const errString = util.inspect(err)
|
|
30
36
|
for (const re of this.retryErrorPatterns) {
|
|
31
37
|
if (errString.match(re)) return true
|
|
@@ -47,6 +47,7 @@ module.exports = class CompilerCsv extends CompilerBase {
|
|
|
47
47
|
if (scriptData.length === 0) {
|
|
48
48
|
return []
|
|
49
49
|
}
|
|
50
|
+
const legacyModeOn = !this._GetOptionalCapability(Capabilities.SCRIPTING_CSV_LEGACY_MODE_OFF, false)
|
|
50
51
|
|
|
51
52
|
let delimiter = this._GetOptionalCapability(Capabilities.SCRIPTING_CSV_DELIMITER)
|
|
52
53
|
if (!delimiter) {
|
|
@@ -71,7 +72,8 @@ module.exports = class CompilerCsv extends CompilerBase {
|
|
|
71
72
|
delimiter,
|
|
72
73
|
escape: this.caps[Capabilities.SCRIPTING_CSV_ESCAPE],
|
|
73
74
|
quote: this.caps[Capabilities.SCRIPTING_CSV_QUOTE],
|
|
74
|
-
columns: false
|
|
75
|
+
columns: false,
|
|
76
|
+
relax_column_count: true
|
|
75
77
|
})
|
|
76
78
|
} catch (err) {
|
|
77
79
|
throw new Error(`Invalid CSV: ${err.message || err}`)
|
|
@@ -79,123 +81,169 @@ module.exports = class CompilerCsv extends CompilerBase {
|
|
|
79
81
|
if (rows.length === 0) {
|
|
80
82
|
return []
|
|
81
83
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
const columnCount = rows[0].length
|
|
85
|
+
debug(`Legacy mode ${legacyModeOn ? 'on' : 'off'} rows ${rows.length} columns ${columnCount}`)
|
|
86
|
+
|
|
87
|
+
if ((scriptType === Constants.SCRIPTING_TYPE_CONVO || scriptType === Constants.SCRIPTING_TYPE_PCONVO)) {
|
|
88
|
+
if (columnCount === 1 || (!legacyModeOn && columnCount > 3)) {
|
|
89
|
+
debug(`Invalid column count '${columnCount}' in convo mode`)
|
|
90
|
+
return []
|
|
91
|
+
}
|
|
92
|
+
let header = null
|
|
93
|
+
if (rows.length > 0 && this.caps[Capabilities.SCRIPTING_CSV_SKIP_HEADER]) {
|
|
94
|
+
header = rows[0]
|
|
95
|
+
rows = rows.slice(1)
|
|
96
|
+
}
|
|
97
|
+
if (rows.length === 0) {
|
|
98
|
+
debug('Datarows not found in convo mode')
|
|
89
99
|
return []
|
|
90
100
|
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (scriptType !== Constants.SCRIPTING_TYPE_CONVO && scriptType !== Constants.SCRIPTING_TYPE_PCONVO) {
|
|
94
|
-
return []
|
|
95
|
-
}
|
|
96
|
-
let header = null
|
|
97
|
-
if (rows.length > 0 && this.caps[Capabilities.SCRIPTING_CSV_SKIP_HEADER]) {
|
|
98
|
-
header = rows[0]
|
|
99
|
-
rows = rows.slice(1)
|
|
100
|
-
}
|
|
101
|
-
if (rows.length === 0) {
|
|
102
|
-
return []
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const lineNumberBase = this.caps[Capabilities.SCRIPTING_CSV_SKIP_HEADER] ? 2 : 1
|
|
106
|
-
if (rows[0].length === 2) {
|
|
107
|
-
debug('Found 2-column CSV file, treating it as question/answer file')
|
|
108
101
|
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
const lineNumberBase = this.caps[Capabilities.SCRIPTING_CSV_SKIP_HEADER] ? 2 : 1
|
|
103
|
+
if (columnCount === 2) {
|
|
104
|
+
let colQuestion = DEFAULT_QA_COLUMN_QUESTION
|
|
105
|
+
let colAnswer = DEFAULT_QA_COLUMN_ANSWER
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
if (header) {
|
|
108
|
+
if (this.caps[Capabilities.SCRIPTING_CSV_QA_COLUMN_QUESTION] !== undefined) {
|
|
109
|
+
colQuestion = _findColIndex(header, this.caps[Capabilities.SCRIPTING_CSV_QA_COLUMN_QUESTION])
|
|
110
|
+
}
|
|
111
|
+
if (this.caps[Capabilities.SCRIPTING_CSV_QA_COLUMN_ANSWER] !== undefined) {
|
|
112
|
+
colAnswer = _findColIndex(header, this.caps[Capabilities.SCRIPTING_CSV_QA_COLUMN_ANSWER])
|
|
113
|
+
}
|
|
115
114
|
}
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
|
|
116
|
+
const convos = rows.map((row, i) => new Convo(this.context, {
|
|
117
|
+
header: {
|
|
118
|
+
name: `L${i + lineNumberBase}`
|
|
119
|
+
},
|
|
120
|
+
conversation: [
|
|
121
|
+
Object.assign({},
|
|
122
|
+
linesToConvoStep(
|
|
123
|
+
[row[colQuestion]],
|
|
124
|
+
'me',
|
|
125
|
+
this.context,
|
|
126
|
+
undefined,
|
|
127
|
+
true
|
|
128
|
+
), { stepTag: `L${i + lineNumberBase}-Question` }),
|
|
129
|
+
Object.assign({},
|
|
130
|
+
linesToConvoStep(
|
|
131
|
+
[row[colAnswer]],
|
|
132
|
+
'bot',
|
|
133
|
+
this.context,
|
|
134
|
+
undefined,
|
|
135
|
+
true
|
|
136
|
+
), { stepTag: `L${i + lineNumberBase}-Answer` })
|
|
137
|
+
]
|
|
138
|
+
}))
|
|
139
|
+
if (scriptType === Constants.SCRIPTING_TYPE_CONVO) {
|
|
140
|
+
this.context.AddConvos(convos)
|
|
141
|
+
} else if (scriptType === Constants.SCRIPTING_TYPE_PCONVO) {
|
|
142
|
+
this.context.AddPartialConvos(convos)
|
|
118
143
|
}
|
|
144
|
+
debug(`Found 2-column CSV file, treating it as question/answer file, extracted ${convos.length} convos`)
|
|
145
|
+
return convos
|
|
119
146
|
}
|
|
120
147
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
conversation: [
|
|
126
|
-
Object.assign({},
|
|
127
|
-
linesToConvoStep(
|
|
128
|
-
[row[colQuestion]],
|
|
129
|
-
'me',
|
|
130
|
-
this.context,
|
|
131
|
-
undefined,
|
|
132
|
-
true
|
|
133
|
-
), { stepTag: `L${i + lineNumberBase}-Question` }),
|
|
134
|
-
Object.assign({},
|
|
135
|
-
linesToConvoStep(
|
|
136
|
-
[row[colAnswer]],
|
|
137
|
-
'bot',
|
|
138
|
-
this.context,
|
|
139
|
-
undefined,
|
|
140
|
-
true
|
|
141
|
-
), { stepTag: `L${i + lineNumberBase}-Answer` })
|
|
142
|
-
]
|
|
143
|
-
}))
|
|
144
|
-
if (scriptType === Constants.SCRIPTING_TYPE_CONVO) {
|
|
145
|
-
this.context.AddConvos(convos)
|
|
146
|
-
} else if (scriptType === Constants.SCRIPTING_TYPE_PCONVO) {
|
|
147
|
-
this.context.AddPartialConvos(convos)
|
|
148
|
-
}
|
|
149
|
-
return convos
|
|
150
|
-
}
|
|
148
|
+
if (columnCount >= 3) {
|
|
149
|
+
let colConversationId = DEFAULT_MULTIROW_COLUMN_CONVERSATION
|
|
150
|
+
let colSender = DEFAULT_MULTIROW_COLUMN_SENDER
|
|
151
|
+
let colText = DEFAULT_MULTIROW_COLUMN_TEXT
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
if (header) {
|
|
154
|
+
if (this.caps[Capabilities.SCRIPTING_CSV_MULTIROW_COLUMN_CONVERSATION_ID] !== undefined) {
|
|
155
|
+
colConversationId = _findColIndex(header, this.caps[Capabilities.SCRIPTING_CSV_MULTIROW_COLUMN_CONVERSATION_ID])
|
|
156
|
+
}
|
|
157
|
+
if (this.caps[Capabilities.SCRIPTING_CSV_MULTIROW_COLUMN_SENDER] !== undefined) {
|
|
158
|
+
colSender = _findColIndex(header, this.caps[Capabilities.SCRIPTING_CSV_MULTIROW_COLUMN_SENDER])
|
|
159
|
+
}
|
|
160
|
+
if (this.caps[Capabilities.SCRIPTING_CSV_MULTIROW_COLUMN_TEXT] !== undefined) {
|
|
161
|
+
colText = _findColIndex(header, this.caps[Capabilities.SCRIPTING_CSV_MULTIROW_COLUMN_TEXT])
|
|
162
|
+
}
|
|
163
|
+
}
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
const conversationIds = _.uniq(rows.map(r => r[colConversationId]))
|
|
166
|
+
const convos = conversationIds.map(conversationId => {
|
|
167
|
+
const convoRows = rows.map((row, i) => {
|
|
168
|
+
if (row[colConversationId] === conversationId) {
|
|
169
|
+
return Object.assign({},
|
|
170
|
+
linesToConvoStep(
|
|
171
|
+
[row[colText]],
|
|
172
|
+
row[colSender],
|
|
173
|
+
this.context,
|
|
174
|
+
undefined,
|
|
175
|
+
true
|
|
176
|
+
), { stepTag: `L${i + lineNumberBase}` })
|
|
177
|
+
}
|
|
178
|
+
return null
|
|
179
|
+
}).filter(c => c)
|
|
180
|
+
return new Convo(this.context, {
|
|
181
|
+
header: {
|
|
182
|
+
name: conversationId
|
|
183
|
+
},
|
|
184
|
+
conversation: convoRows
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
if (scriptType === Constants.SCRIPTING_TYPE_CONVO) {
|
|
188
|
+
this.context.AddConvos(convos)
|
|
189
|
+
} else if (scriptType === Constants.SCRIPTING_TYPE_PCONVO) {
|
|
190
|
+
this.context.AddPartialConvos(convos)
|
|
191
|
+
}
|
|
192
|
+
debug(`Found 3-column CSV file, treating it as multi-row conversation file, extracted ${convos.length} convos`)
|
|
193
|
+
return convos
|
|
194
|
+
}
|
|
195
|
+
} else if (scriptType === Constants.SCRIPTING_TYPE_UTTERANCES) {
|
|
196
|
+
if (columnCount === 2 || columnCount === 3 || (legacyModeOn && columnCount > 4)) {
|
|
197
|
+
debug(`Invalid column count '${columnCount}' in utterances mode`)
|
|
198
|
+
return []
|
|
199
|
+
}
|
|
200
|
+
const result = []
|
|
201
|
+
const startRow = this._GetOptionalCapability(Capabilities.SCRIPTING_CSV_UTTERANCE_STARTROW, 2) - 1
|
|
202
|
+
const startRowHeader = this._GetOptionalCapability(Capabilities.SCRIPTING_CSV_UTTERANCE_STARTROW_HEADER)
|
|
203
|
+
const stopOnEmpty = this._GetOptionalCapability(Capabilities.SCRIPTING_CSV_UTTERANCE_STOP_ON_EMPTY)
|
|
204
|
+
|
|
205
|
+
for (let col = 0; col < columnCount; col++) {
|
|
206
|
+
const name = rows[0][col]
|
|
207
|
+
if (!name || name.trim().length === 0) {
|
|
208
|
+
debug(`Column ${col + 1} has no header, skipping`)
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
158
211
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
212
|
+
const uttStruct = {
|
|
213
|
+
name,
|
|
214
|
+
utterances: []
|
|
215
|
+
}
|
|
216
|
+
let skip = !!startRowHeader
|
|
217
|
+
const getData = (row) => {
|
|
218
|
+
return rows[row][col] ? rows[row][col].trim() : false
|
|
162
219
|
}
|
|
163
|
-
|
|
164
|
-
|
|
220
|
+
//
|
|
221
|
+
for (let row = startRow; row < rows.length && (skip || !stopOnEmpty || !!getData(row)); row++) { // eslint-disable-line no-unmodified-loop-condition
|
|
222
|
+
const data = getData(row)
|
|
223
|
+
if (!data) {
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
if (!skip) {
|
|
227
|
+
uttStruct.utterances.push(data)
|
|
228
|
+
} else {
|
|
229
|
+
if (startRowHeader === rows[row][col]) {
|
|
230
|
+
skip = false
|
|
231
|
+
}
|
|
232
|
+
}
|
|
165
233
|
}
|
|
166
|
-
if (
|
|
167
|
-
|
|
234
|
+
if (uttStruct.utterances.length === 0) {
|
|
235
|
+
// liveperson, skipping meta intents
|
|
236
|
+
debug(`Column ${col + 1} has no utterances, skipping`)
|
|
237
|
+
continue
|
|
168
238
|
}
|
|
239
|
+
result.push(uttStruct)
|
|
169
240
|
}
|
|
170
241
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
linesToConvoStep(
|
|
177
|
-
[row[colText]],
|
|
178
|
-
row[colSender],
|
|
179
|
-
this.context,
|
|
180
|
-
undefined,
|
|
181
|
-
true
|
|
182
|
-
), { stepTag: `L${i + lineNumberBase}` })
|
|
183
|
-
}
|
|
184
|
-
return null
|
|
185
|
-
}).filter(c => c)
|
|
186
|
-
return new Convo(this.context, {
|
|
187
|
-
header: {
|
|
188
|
-
name: conversationId
|
|
189
|
-
},
|
|
190
|
-
conversation: convoRows
|
|
191
|
-
})
|
|
192
|
-
})
|
|
193
|
-
if (scriptType === Constants.SCRIPTING_TYPE_CONVO) {
|
|
194
|
-
this.context.AddConvos(convos)
|
|
195
|
-
} else if (scriptType === Constants.SCRIPTING_TYPE_PCONVO) {
|
|
196
|
-
this.context.AddPartialConvos(convos)
|
|
197
|
-
}
|
|
198
|
-
return convos
|
|
242
|
+
debug(`Multi-column utterance file, extracted ${result.length} utterances`)
|
|
243
|
+
this.context.AddUtterances(result)
|
|
244
|
+
return result
|
|
245
|
+
} else {
|
|
246
|
+
return []
|
|
199
247
|
}
|
|
200
248
|
}
|
|
201
249
|
}
|
package/src/scripting/Convo.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const util = require('util')
|
|
2
2
|
const _ = require('lodash')
|
|
3
3
|
const debug = require('debug')('botium-core-Convo')
|
|
4
|
+
const promiseRetry = require('promise-retry')
|
|
4
5
|
|
|
5
6
|
const BotiumMockMessage = require('../mocks/BotiumMockMessage')
|
|
6
7
|
const Capabilities = require('../Capabilities')
|
|
@@ -8,6 +9,7 @@ const Events = require('../Events')
|
|
|
8
9
|
const ScriptingMemory = require('./ScriptingMemory')
|
|
9
10
|
const { BotiumError, botiumErrorFromErr, botiumErrorFromList } = require('./BotiumError')
|
|
10
11
|
const { normalizeText, toString, removeBuffers, splitStringInNonEmptyLines } = require('./helper')
|
|
12
|
+
const RetryHelper = require('../helpers/RetryHelper')
|
|
11
13
|
|
|
12
14
|
const { LOGIC_HOOK_INCLUDE } = require('./logichook/LogicHookConsts')
|
|
13
15
|
|
|
@@ -210,6 +212,24 @@ class Convo {
|
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
async Run (container) {
|
|
215
|
+
const retryHelper = new RetryHelper(container.caps, 'CONVO')
|
|
216
|
+
return promiseRetry(async (retry, number) => {
|
|
217
|
+
return this.RunImpl(container).catch(err => {
|
|
218
|
+
const retryRemaining = retryHelper.retrySettings.retries - number + 1
|
|
219
|
+
if (retryHelper.shouldRetry(err)) {
|
|
220
|
+
debug(`Convo failed with error "${err.message || JSON.stringify(err)}". Retry ${retryRemaining > 0 ? 'enabled' : 'disabled'} (remaining #${retryRemaining}/${retryHelper.retrySettings.retries}, criterion matches)`)
|
|
221
|
+
retry(err)
|
|
222
|
+
} else {
|
|
223
|
+
if (retryHelper.retryErrorPatterns.length > 0) {
|
|
224
|
+
debug(`Convo failed with error "${err.message || JSON.stringify(err)}". Retry 'disabled' (remaining (#${retryRemaining}/${retryHelper.retrySettings.retries}), criterion does not match)`)
|
|
225
|
+
}
|
|
226
|
+
throw err
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
}, retryHelper.retrySettings)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async RunImpl (container) {
|
|
213
233
|
const transcript = new Transcript({
|
|
214
234
|
steps: [],
|
|
215
235
|
attachments: [],
|
|
@@ -535,7 +555,7 @@ class Convo {
|
|
|
535
555
|
if (container.caps[Capabilities.SCRIPTING_ENABLE_SKIP_ASSERT_ERRORS]) {
|
|
536
556
|
const transcriptStepErrs = transcript.steps.filter(s => s.err).map(s => s.err)
|
|
537
557
|
if (transcriptStepErrs && transcriptStepErrs.length > 0) {
|
|
538
|
-
transcript.err = botiumErrorFromList([transcriptStepErrs, transcript.err].filter(e => e), {})
|
|
558
|
+
transcript.err = botiumErrorFromList([...transcriptStepErrs.filter(err => err !== transcript.err), transcript.err].filter(e => e), {})
|
|
539
559
|
}
|
|
540
560
|
}
|
|
541
561
|
}
|
|
@@ -20,7 +20,7 @@ const RetryHelper = require('../helpers/RetryHelper')
|
|
|
20
20
|
const { getMatchFunction } = require('./MatchFunctions')
|
|
21
21
|
const precompilers = require('./precompilers')
|
|
22
22
|
|
|
23
|
-
const globPattern = '**/+(*.convo.txt|*.utterances.txt|*.pconvo.txt|*.scriptingmemory.txt|*.xlsx|*.xlsm|*.convo.csv|*.pconvo.csv|*.yaml|*.yml|*.json|*.md|*.markdown)'
|
|
23
|
+
const globPattern = '**/+(*.convo.txt|*.utterances.txt|*.pconvo.txt|*.scriptingmemory.txt|*.xlsx|*.xlsm|*.convo.csv|*.pconvo.csv|*.utterances.csv|*.yaml|*.yml|*.json|*.md|*.markdown)'
|
|
24
24
|
const skipPattern = /^skip[.\-_]/i
|
|
25
25
|
|
|
26
26
|
const p = (retryHelper, fn) => {
|
|
@@ -522,6 +522,10 @@ module.exports = class ScriptingProvider {
|
|
|
522
522
|
result = this.ReadScriptFromBuffer(scriptBuffer, Constants.SCRIPTING_FORMAT_CSV, Constants.SCRIPTING_TYPE_CONVO)
|
|
523
523
|
} else if (filename.endsWith('.pconvo.csv')) {
|
|
524
524
|
result = this.ReadScriptFromBuffer(scriptBuffer, Constants.SCRIPTING_FORMAT_CSV, Constants.SCRIPTING_TYPE_PCONVO)
|
|
525
|
+
} else if (filename.endsWith('.pconvo.csv')) {
|
|
526
|
+
result = this.ReadScriptFromBuffer(scriptBuffer, Constants.SCRIPTING_FORMAT_CSV, Constants.SCRIPTING_TYPE_PCONVO)
|
|
527
|
+
} else if (filename.endsWith('.utterance.csv')) {
|
|
528
|
+
result = this.ReadScriptFromBuffer(scriptBuffer, Constants.SCRIPTING_FORMAT_CSV, Constants.SCRIPTING_TYPE_UTTERANCES)
|
|
525
529
|
} else if (filename.endsWith('.yaml') || filename.endsWith('.yml')) {
|
|
526
530
|
result = this.ReadScriptFromBuffer(scriptBuffer, Constants.SCRIPTING_FORMAT_YAML, [Constants.SCRIPTING_TYPE_UTTERANCES, Constants.SCRIPTING_TYPE_PCONVO, Constants.SCRIPTING_TYPE_CONVO, Constants.SCRIPTING_TYPE_SCRIPTING_MEMORY])
|
|
527
531
|
} else if (filename.endsWith('.json')) {
|
package/src/scripting/helper.js
CHANGED
|
@@ -77,6 +77,13 @@ const flatString = (str) => {
|
|
|
77
77
|
return str ? str.split('\n').map(s => s.trim()).join(' ') : ''
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
const _formatAppendArgs = (args) => {
|
|
81
|
+
return args ? ` ${args.map(a => _.isString(a) ? a.replace(/\|/g, '\\|') : `${a}`).join('|')}` : ''
|
|
82
|
+
}
|
|
83
|
+
const _parseArgs = (str) => {
|
|
84
|
+
return (str && str.length > 0 && str.replace(/\\\|/g, '###ESCAPESPLIT###').split('|').map(s => s.replace(/###ESCAPESPLIT###/g, '|').trim())) || []
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
const linesToConvoStep = (lines, sender, context, eol, singleLineMode = false) => {
|
|
81
88
|
if (!validateSender(sender)) throw new Error(`Failed to parse conversation. Section "${sender}" unknown.`)
|
|
82
89
|
|
|
@@ -106,14 +113,14 @@ const linesToConvoStep = (lines, sender, context, eol, singleLineMode = false) =
|
|
|
106
113
|
}
|
|
107
114
|
const name = logicLine.split(' ')[0]
|
|
108
115
|
if (sender !== 'me' && context.IsAsserterValid(name)) {
|
|
109
|
-
const args = (logicLine.length > name.length ? logicLine.substr(name.length + 1)
|
|
116
|
+
const args = (logicLine.length > name.length ? _parseArgs(logicLine.substr(name.length + 1)) : [])
|
|
110
117
|
convoStep.asserters.push({ name, args, not, optional })
|
|
111
118
|
} else if (sender === 'me' && context.IsUserInputValid(name)) {
|
|
112
|
-
const args = (logicLine.length > name.length ? logicLine.substr(name.length + 1)
|
|
119
|
+
const args = (logicLine.length > name.length ? _parseArgs(logicLine.substr(name.length + 1)) : [])
|
|
113
120
|
convoStep.userInputs.push({ name, args })
|
|
114
121
|
textLinesAccepted = false
|
|
115
122
|
} else if (context.IsLogicHookValid(name)) {
|
|
116
|
-
const args = (logicLine.length > name.length ? logicLine.substr(name.length + 1)
|
|
123
|
+
const args = (logicLine.length > name.length ? _parseArgs(logicLine.substr(name.length + 1)) : [])
|
|
117
124
|
convoStep.logicHooks.push({ name, args })
|
|
118
125
|
textLinesAccepted = false
|
|
119
126
|
} else {
|
|
@@ -422,7 +429,7 @@ const convoStepToLines = (step) => {
|
|
|
422
429
|
const lines = []
|
|
423
430
|
if (step.sender === 'me') {
|
|
424
431
|
step.forms && step.forms.filter(form => form.value).forEach((form) => {
|
|
425
|
-
lines.push(`FORM
|
|
432
|
+
lines.push(`FORM${_formatAppendArgs([form.name, form.value])}`)
|
|
426
433
|
})
|
|
427
434
|
if (step.buttons && step.buttons.length > 0) {
|
|
428
435
|
lines.push('BUTTON ' + _decompileButton(step.buttons[0]))
|
|
@@ -432,34 +439,34 @@ const convoStepToLines = (step) => {
|
|
|
432
439
|
lines.push(step.messageText)
|
|
433
440
|
}
|
|
434
441
|
step.userInputs && step.userInputs.forEach((userInput) => {
|
|
435
|
-
lines.push(userInput.name + (userInput.args
|
|
442
|
+
lines.push(userInput.name + _formatAppendArgs(userInput.args))
|
|
436
443
|
})
|
|
437
444
|
step.logicHooks && step.logicHooks.forEach((logicHook) => {
|
|
438
|
-
lines.push(logicHook.name + (logicHook.args
|
|
445
|
+
lines.push(logicHook.name + _formatAppendArgs(logicHook.args))
|
|
439
446
|
})
|
|
440
447
|
} else {
|
|
441
448
|
if (step.messageText) {
|
|
442
449
|
lines.push((step.optional ? '?' : '') + (step.not ? '!' : '') + step.messageText)
|
|
443
450
|
}
|
|
444
|
-
if (step.buttons && step.buttons.length > 0) lines.push('BUTTONS
|
|
445
|
-
if (step.media && step.media.length > 0) lines.push('MEDIA
|
|
451
|
+
if (step.buttons && step.buttons.length > 0) lines.push('BUTTONS' + _formatAppendArgs(step.buttons.filter(b => b.text).map(b => flatString(b.text))))
|
|
452
|
+
if (step.media && step.media.length > 0) lines.push('MEDIA' + _formatAppendArgs(step.media.filter(m => !m.buffer && m.mediaUri).map(m => m.mediaUri)))
|
|
446
453
|
if (step.cards && step.cards.length > 0) {
|
|
447
454
|
step.cards.forEach(c => {
|
|
448
455
|
let cardTexts = []
|
|
449
456
|
if (c.text) cardTexts = cardTexts.concat(_.isArray(c.text) ? c.text : [c.text])
|
|
450
457
|
if (c.subtext) cardTexts = cardTexts.concat(_.isArray(c.subtext) ? c.subtext : [c.subtext])
|
|
451
458
|
if (c.content) cardTexts = cardTexts.concat(_.isArray(c.content) ? c.content : [c.content])
|
|
452
|
-
if (cardTexts.length > 0) lines.push('CARDS
|
|
459
|
+
if (cardTexts.length > 0) lines.push('CARDS' + _formatAppendArgs(cardTexts.map(c => flatString(c))))
|
|
453
460
|
|
|
454
|
-
if (c.buttons && c.buttons.length > 0) lines.push('BUTTONS
|
|
461
|
+
if (c.buttons && c.buttons.length > 0) lines.push('BUTTONS' + _formatAppendArgs(c.buttons.filter(b => b.text).map(b => flatString(b.text))))
|
|
455
462
|
if (c.image && !c.image.buffer && c.image.mediaUri) lines.push('MEDIA ' + c.image.mediaUri)
|
|
456
463
|
})
|
|
457
464
|
}
|
|
458
465
|
step.asserters && step.asserters.forEach((asserter) => {
|
|
459
|
-
lines.push((asserter.optional ? '?' : '') + (asserter.not ? '!' : '') + asserter.name + (asserter.args
|
|
466
|
+
lines.push((asserter.optional ? '?' : '') + (asserter.not ? '!' : '') + asserter.name + _formatAppendArgs(asserter.args))
|
|
460
467
|
})
|
|
461
468
|
step.logicHooks && step.logicHooks.forEach((logicHook) => {
|
|
462
|
-
lines.push(logicHook.name + (logicHook.args
|
|
469
|
+
lines.push(logicHook.name + _formatAppendArgs(logicHook.args))
|
|
463
470
|
})
|
|
464
471
|
}
|
|
465
472
|
return lines.map(l => l.trim())
|
|
@@ -192,7 +192,7 @@ module.exports = class LogicHookUtils {
|
|
|
192
192
|
})
|
|
193
193
|
return vm.run(script)
|
|
194
194
|
} catch (err) {
|
|
195
|
-
throw new Error(
|
|
195
|
+
throw new Error(`Script ${key} is not valid - ${err.message || err}`)
|
|
196
196
|
}
|
|
197
197
|
} else {
|
|
198
198
|
throw new Error(`Script "${key}" is not valid - only functions and javascript code accepted`)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
const { SCRIPTING_NORMALIZE_TEXT } = require('../../../Capabilities')
|
|
1
2
|
const { BotiumError } = require('../../BotiumError')
|
|
2
3
|
const { buttonsFromMsg } = require('../helpers')
|
|
4
|
+
const { normalizeText } = require('../../helper')
|
|
3
5
|
|
|
4
6
|
module.exports = class ButtonsAsserter {
|
|
5
7
|
constructor (context, caps = {}) {
|
|
@@ -9,14 +11,14 @@ module.exports = class ButtonsAsserter {
|
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
_evalButtons (args, botMsg) {
|
|
12
|
-
const allButtons = buttonsFromMsg(botMsg, true).map(b => b.text)
|
|
14
|
+
const allButtons = buttonsFromMsg(botMsg, true).map(b => b.text).filter(b => b).map(b => normalizeText(b, !!this.caps[SCRIPTING_NORMALIZE_TEXT]))
|
|
13
15
|
if (!args || args.length === 0) {
|
|
14
16
|
return { allButtons, buttonsNotFound: [], buttonsFound: allButtons }
|
|
15
17
|
}
|
|
16
18
|
const buttonsNotFound = []
|
|
17
19
|
const buttonsFound = []
|
|
18
20
|
for (let i = 0; i < (args || []).length; i++) {
|
|
19
|
-
if (allButtons.findIndex(b => this.context.Match(b, args[i])) < 0) {
|
|
21
|
+
if (allButtons.findIndex(b => this.context.Match(b, normalizeText(args[i], !!this.caps[SCRIPTING_NORMALIZE_TEXT]))) < 0) {
|
|
20
22
|
buttonsNotFound.push(args[i])
|
|
21
23
|
} else {
|
|
22
24
|
buttonsFound.push(args[i])
|