wingbot 3.52.5 → 3.53.0-alpha.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.52.5",
3
+ "version": "3.53.0-alpha.1",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1 @@
1
+ > This plugin erases state of conversational sequences
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @param {import('../../src/Request')} req
3
+ * @param {import('../../src/Responder')} res
4
+ */
5
+ module.exports = async (req, res) => {
6
+ [...Object.keys(req.state), ...Object.keys(res.newState)]
7
+ .forEach((key) => {
8
+ const match = key.match(/^(_~_R_|_R_)/);
9
+ if (match) {
10
+ res.setState({ [key]: null });
11
+ }
12
+ });
13
+ };
@@ -1,9 +1 @@
1
- > This plugin helps you to drive user back to previous interaction.
2
-
3
- If the previous interaction exists and it's not a fallback or the same interaction, where the plugin is, the bot will go back and stops executing this interaction.
4
-
5
- If not, the bot will continue in execution of this interaction, so you can handle situation.
6
-
7
- It's useful to it use **Show when there is a way back** condition, so you don't have to worry about handling the situation, when there's no way to go back.
8
-
9
- ![go back condition](https://github.com/wingbotai/wingbot/raw/master/plugins/ai.wingbot.conditionIfGoBackPossible/back.png)
1
+ > This plugin detects reqular expressions
@@ -446,6 +446,7 @@
446
446
  "availableSince": 3.39,
447
447
  "editable": false,
448
448
  "isFactory": true,
449
+ "category": "conversation",
449
450
  "inputs": [
450
451
  {
451
452
  "name": "skip",
@@ -470,6 +471,19 @@
470
471
  ],
471
472
  "items": [
472
473
  ]
474
+ },
475
+ {
476
+ "id": "ai.wingbot.clearMessageSequences",
477
+ "name": "Clear message sequences state",
478
+ "description": "Put it in a first interaction of the process, where would you like to have clear states of messages sequences",
479
+ "availableSince": 3.53,
480
+ "editable": true,
481
+ "isFactory": false,
482
+ "category": "conversation",
483
+ "inputs": [
484
+ ],
485
+ "items": [
486
+ ]
473
487
  }
474
488
  ],
475
489
  "categories": [
@@ -26,6 +26,7 @@ const MESSAGE_RESOLVER_NAME = 'botbuild.message';
26
26
 
27
27
  /**
28
28
  * @typedef {object} Resolver
29
+ * @prop {string|number} [id] - only for text messages with random characters
29
30
  * @prop {string} type
30
31
  * @prop {object} params
31
32
  * @prop {string} [params.staticBlockId]
@@ -132,6 +133,7 @@ const DUMMY_ROUTE = { id: 0, path: null, resolvers: [] };
132
133
  * @prop {string} [staticBlockId]
133
134
  * @prop {Block[]} [blocks]
134
135
  * @prop {object} [BuildRouter]
136
+ * @prop {string} [resolverId] - only for text messages with random characters
135
137
  */
136
138
 
137
139
  /**
@@ -827,7 +829,8 @@ class BuildRouter extends Router {
827
829
  isResponder,
828
830
  expectedPath,
829
831
  routeId: id,
830
- configuration: this._configuration
832
+ configuration: this._configuration,
833
+ resolverId: resolver.id
831
834
  };
832
835
 
833
836
  const resFn = this._resolverFactory(resolver, context, buildInfo);
package/src/Plugins.js CHANGED
@@ -76,6 +76,7 @@ class Plugins {
76
76
  * @param {object} [context]
77
77
  * @param {boolean} [context.isLastIndex]
78
78
  * @param {Router} [context.router]
79
+ * @param {object} [context.configuration]
79
80
  * @example
80
81
  *
81
82
  * const { Router } = require('wingbot');
@@ -99,7 +100,7 @@ class Plugins {
99
100
  name,
100
101
  paramsData = {},
101
102
  items = new Map(),
102
- context = { isLastIndex: true }
103
+ context = { isLastIndex: true, configuration: {} }
103
104
  ) {
104
105
  let useItems = items;
105
106
 
@@ -119,7 +120,7 @@ class Plugins {
119
120
  .map(([k, e]) => ({ [k]: e }))
120
121
  .reduce(Object.assign, {});
121
122
 
122
- const customFn = this.getPluginFactory(name, cleanParams);
123
+ const customFn = this.getPluginFactory(name, cleanParams, context.configuration);
123
124
  if (typeof customFn === 'object') {
124
125
  // this is an attached router
125
126
 
package/src/Processor.js CHANGED
@@ -10,7 +10,7 @@ const Responder = require('./Responder');
10
10
  const Request = require('./Request');
11
11
  const Ai = require('./Ai');
12
12
  const ReturnSender = require('./ReturnSender');
13
- const { mergeState, isUserInteraction } = require('./utils/stateVariables');
13
+ const { prepareState, mergeState, isUserInteraction } = require('./utils/stateVariables');
14
14
 
15
15
  /** @typedef {import('./wingbot/CustomEntityDetectionModel').Intent} Intent */
16
16
  /** @typedef {import('./ReducerWrapper')} ReducerWrapper */
@@ -637,6 +637,8 @@ class Processor extends EventEmitter {
637
637
  });
638
638
  }
639
639
 
640
+ prepareState(state, fromEvent, state._snew);
641
+
640
642
  const features = [
641
643
  ...(this.options.features || []),
642
644
  ...req.features
package/src/Tester.js CHANGED
@@ -18,6 +18,8 @@ const Router = require('./Router'); // eslint-disable-line no-unused-vars
18
18
  const ReducerWrapper = require('./ReducerWrapper'); // eslint-disable-line no-unused-vars
19
19
  const { FEATURE_TEXT } = require('./features');
20
20
 
21
+ /** @typedef {import('./Processor').ProcessorOptions} ProcessorOptions */
22
+
21
23
  /**
22
24
  * Utility for testing requests
23
25
  *
@@ -31,7 +33,7 @@ class Tester {
31
33
  * @param {Router|ReducerWrapper} reducer
32
34
  * @param {string} [senderId=null]
33
35
  * @param {string} [pageId=null]
34
- * @param {object} [processorOptions={}] - options for Processor
36
+ * @param {ProcessorOptions} [processorOptions={}] - options for Processor
35
37
  * @param {MemoryStateStorage} [storage] - place to override the storage
36
38
  *
37
39
  * @memberOf Tester
@@ -95,6 +97,7 @@ class Tester {
95
97
  this.processor = new Processor(reducer, ({
96
98
  stateStorage: this.storage,
97
99
  log,
100
+ // @ts-ignore
98
101
  loadUsers: false,
99
102
  ...processorOptions
100
103
  }));
@@ -168,10 +171,12 @@ class Tester {
168
171
  /**
169
172
  * Enable tester to expand random texts
170
173
  * It joins them into a single sting
174
+ *
175
+ * @param {boolean} [fixedIndex]
171
176
  */
172
- setExpandRandomTexts () {
177
+ setExpandRandomTexts (fixedIndex) {
173
178
  Object.assign(this.testData, {
174
- _expandRandomTexts: true
179
+ _expandRandomTexts: fixedIndex ? 1 : true
175
180
  });
176
181
  }
177
182
 
@@ -381,9 +386,20 @@ class Tester {
381
386
  stateContains (object, deep = false) {
382
387
  const { state } = this.getState();
383
388
 
389
+ const clean = Object.fromEntries(
390
+ Object.entries(object)
391
+ .filter(([k, v]) => {
392
+ if (v === null || v === undefined) {
393
+ assert.ok(state[k] === null || state[k] === undefined, `Expected state key '${k}' to be empty. Actual: ${JSON.stringify(state[k])}'`);
394
+ return false;
395
+ }
396
+ return true;
397
+ })
398
+ );
399
+
384
400
  assert.deepEqual(
385
401
  state,
386
- deep ? deepExtend({}, state, object) : { ...state, ...object },
402
+ deep ? deepExtend({}, state, clean) : { ...state, ...clean },
387
403
  'Conversation state equals'
388
404
  );
389
405
  }
@@ -13,11 +13,13 @@ const {
13
13
  getLanguageText,
14
14
  getLanguageTextObjects,
15
15
  randomizedCompiler,
16
- cachedTranslatedCompilator
16
+ cachedTranslatedCompilator,
17
+ renderMessageText
17
18
  } = require('./utils');
18
19
  const {
19
20
  FEATURE_SSML, FEATURE_TEXT, FEATURE_VOICE
20
21
  } = require('../features');
22
+ const { vars, VAR_TYPES } = require('../utils/stateVariables');
21
23
 
22
24
  /** @typedef {import('../Responder').VoiceControl} VoiceControl */
23
25
  /** @typedef {import('./utils').Translations} Translations */
@@ -180,6 +182,89 @@ function findSupportedMessages (text, features, lang = null) {
180
182
  };
181
183
  }
182
184
 
185
+ const MODE_RANDOM = 'r';
186
+ const MODE_SEQUENCE = 's';
187
+ const MODE_RANDOM_START_SEQUENCE = 'rs';
188
+
189
+ const ALL_MODES = [MODE_RANDOM, MODE_SEQUENCE, MODE_RANDOM_START_SEQUENCE];
190
+
191
+ function selectTranslation (resolverId, params, texts, data) {
192
+ const key = `_R_${resolverId}`;
193
+ let { lang, [key]: seqState } = data;
194
+
195
+ if (texts.length === 1) {
196
+ return [
197
+ renderMessageText(texts[0].t, data),
198
+ seqState && resolverId ? { [key]: null } : {}
199
+ ];
200
+ }
201
+
202
+ const { mode, persist = null } = params;
203
+
204
+ if ((!mode || mode === MODE_RANDOM) && data._expandRandomTexts === true) {
205
+ return [
206
+ texts.map((x) => renderMessageText(x.t, data))
207
+ .join('\n'),
208
+ {}
209
+ ];
210
+ }
211
+
212
+ if (!lang) {
213
+ const [firstText = { l: null }] = getLanguageTextObjects(params.text);
214
+ lang = firstText.l || 'X';
215
+ }
216
+
217
+ if (mode === MODE_RANDOM || !ALL_MODES.includes(mode)) {
218
+ const index = data._expandRandomTexts
219
+ ? 1
220
+ : Math.floor(Math.random() * texts.length);
221
+ return [
222
+ renderMessageText(texts[index].t, data),
223
+ seqState ? vars.clear(key) : {}
224
+ ];
225
+ }
226
+
227
+ if (!seqState
228
+ || seqState.l !== lang
229
+ || seqState.n > texts.length
230
+ || seqState.i >= texts.length) {
231
+
232
+ seqState = {
233
+ l: lang,
234
+ n: texts.length,
235
+ i: mode === MODE_SEQUENCE ? 0 : Math.floor(Math.random() * texts.length)
236
+ };
237
+
238
+ if (mode === MODE_RANDOM_START_SEQUENCE && data._expandRandomTexts) {
239
+ seqState.i = 1;
240
+ }
241
+ } else {
242
+ seqState = {
243
+ ...seqState,
244
+ i: (seqState.i + 1) % texts.length
245
+ };
246
+ }
247
+
248
+ let setState;
249
+
250
+ if (persist === VAR_TYPES.SESSION_CONTEXT) {
251
+ setState = vars.sessionContext(key, seqState);
252
+ } else if (persist === VAR_TYPES.SESSION_DIALOGUE_CONTEXT) {
253
+ setState = vars.sessionContext(key, seqState, null); // same as interaction
254
+ } else if (persist === VAR_TYPES.DIALOG_CONTEXT) {
255
+ setState = vars.dialogContext(key, seqState);
256
+ } else {
257
+ setState = {
258
+ [key]: seqState
259
+ };
260
+ }
261
+
262
+ return [
263
+ renderMessageText(texts[seqState.i].t, data),
264
+ setState
265
+ ];
266
+ }
267
+
183
268
  /** @typedef {import('../BuildRouter').BotContext} BotContext */
184
269
  /** @typedef {import('../Router').Resolver} Resolver */
185
270
 
@@ -192,7 +277,7 @@ function findSupportedMessages (text, features, lang = null) {
192
277
  function message (params, context = {}) {
193
278
  const {
194
279
  // @ts-ignore
195
- isLastIndex, isLastMessage, linksMap, configuration
280
+ isLastIndex, isLastMessage, linksMap, configuration, resolverId
196
281
  } = context;
197
282
  if (typeof params.text !== 'string' && !Array.isArray(params.text)) {
198
283
  throw new Error('Message should be a text!');
@@ -225,15 +310,14 @@ function message (params, context = {}) {
225
310
  data.lang
226
311
  );
227
312
 
228
- // find random alternative
229
- const textTemplate = randomizedCompiler([{
230
- l: data.lang,
231
- t: supportedText.translations.map((x) => x.t)
232
- }]);
233
-
234
- const text = textTemplate(data)
235
- .trim();
313
+ const [text, seqState] = selectTranslation(
314
+ resolverId,
315
+ params,
316
+ supportedText.translations,
317
+ data
318
+ );
236
319
 
320
+ res.setState(seqState);
237
321
  res.setData({ $this: text });
238
322
  if (condition && !condition(req, res)) {
239
323
  res.setData({ $this: null });
@@ -145,6 +145,11 @@ function getLanguageTextObjects (translations, lang = null) {
145
145
  }));
146
146
  }
147
147
 
148
+ function renderMessageText (fn, data) {
149
+ const renderer = fn === 'function' ? fn : hbs.compile(fn);
150
+ return renderer(data).trim();
151
+ }
152
+
148
153
  function randomizedCompiler (text, lang) {
149
154
  const texts = getLanguageText(text, lang);
150
155
 
@@ -307,6 +312,7 @@ module.exports = {
307
312
  randomizedCompiler,
308
313
  getText,
309
314
  stateData,
315
+ renderMessageText,
310
316
 
311
317
  ASPECT_SQUARE,
312
318
  ASPECT_HORISONTAL,
@@ -3,6 +3,8 @@
3
3
  */
4
4
  'use strict';
5
5
 
6
+ const SESSION_DIALOGUE_CONTEXT = 'sd';
7
+ const SESSION_CONTEXT = 's';
6
8
  const DIALOG_CONTEXT = 'd';
7
9
  const EXPIRES_AFTER = 't';
8
10
 
@@ -11,11 +13,27 @@ const EXPIRES_AFTER = 't';
11
13
 
12
14
  const VAR_TYPES = {
13
15
  DIALOG_CONTEXT,
14
- EXPIRES_AFTER
16
+ EXPIRES_AFTER,
17
+ SESSION_DIALOGUE_CONTEXT,
18
+ SESSION_CONTEXT
15
19
  };
16
20
 
21
+ const DIALOGUE_CONTEXT_TYPES = [DIALOG_CONTEXT, SESSION_DIALOGUE_CONTEXT];
22
+
17
23
  const vars = {
18
24
 
25
+ /**
26
+ * Clears variable with it's metadata
27
+ *
28
+ * @param {string} key
29
+ * @returns {object}
30
+ */
31
+ clear (key) {
32
+ return {
33
+ [key]: null
34
+ };
35
+ },
36
+
19
37
  /**
20
38
  * Sets variable, which will be removed, when user leaves the dialogue.
21
39
  * **Variable will be available at first interaction of next dialogue.**
@@ -39,6 +57,29 @@ const vars = {
39
57
  };
40
58
  },
41
59
 
60
+ /**
61
+ * Sets variable, which will be removed, when new session is created.
62
+ * **When `dialoguePath` argument is provided, the variable will NOT expire,
63
+ * when the new session will continue in the same dialogue**
64
+ *
65
+ * @param {string} key
66
+ * @param {*} value
67
+ * @param {string|boolean} [dialoguePath] - keeps context also within a dialogue
68
+ * @returns {object}
69
+ * @example
70
+ * const { vars } = require('wingbot');
71
+ * res.setState(vars.sessionContext('myKey', 'foovalue'))
72
+ */
73
+ sessionContext (key, value, dialoguePath = false) {
74
+ return {
75
+ [key]: value,
76
+ [`_~${key}`]: {
77
+ t: dialoguePath === false ? SESSION_CONTEXT : SESSION_DIALOGUE_CONTEXT,
78
+ ...(dialoguePath && { p: dialoguePath })
79
+ }
80
+ };
81
+ },
82
+
42
83
  /**
43
84
  * Sets variable, which will be removed after specified number of conversation turonovers
44
85
  *
@@ -98,7 +139,6 @@ function checkSetState (setState, newState) {
98
139
  delete newState[metaKey]; // eslint-disable-line no-param-reassign
99
140
  }
100
141
  }
101
-
102
142
  }
103
143
  }
104
144
 
@@ -116,6 +156,42 @@ function isUserInteraction (req) {
116
156
  || req.isTextOrIntent());
117
157
  }
118
158
 
159
+ function prepareState (state, firstInTurnover, sessionCreated) {
160
+ if (!sessionCreated) {
161
+ return;
162
+ }
163
+
164
+ // set right path, when path was set
165
+ // eslint-disable-next-line guard-for-in
166
+ for (const key in state) { // eslint-disable-line no-restricted-syntax
167
+ const match = key.match(/^_~(.+)$/);
168
+ if (!match) {
169
+ continue;
170
+ }
171
+ const value = state[key];
172
+ const [, referencedKey] = match;
173
+
174
+ if (value.t === SESSION_CONTEXT) {
175
+ delete state[key]; // eslint-disable-line no-param-reassign
176
+ delete state[referencedKey]; // eslint-disable-line no-param-reassign
177
+ }
178
+
179
+ if (value.t === SESSION_DIALOGUE_CONTEXT) {
180
+ if (value.p === state._lastVisitedPath) {
181
+ // context still matches - make it just DIALOGUE_CONTEXT
182
+ state[key] = { // eslint-disable-line no-param-reassign
183
+ t: DIALOG_CONTEXT,
184
+ p: value.p
185
+ };
186
+ } else {
187
+ delete state[key]; // eslint-disable-line no-param-reassign
188
+ delete state[referencedKey]; // eslint-disable-line no-param-reassign
189
+ }
190
+ }
191
+ }
192
+
193
+ }
194
+
119
195
  /**
120
196
  *
121
197
  * @private
@@ -149,7 +225,7 @@ function mergeState (previousState, req, res, senderStateUpdate, firstInTurnover
149
225
  continue;
150
226
  }
151
227
  const value = res.newState[key];
152
- if (value.t === DIALOG_CONTEXT && typeof value.p === 'undefined') {
228
+ if (DIALOGUE_CONTEXT_TYPES.includes(value.t) && typeof value.p === 'undefined') {
153
229
  Object.assign(value, { p: res.newState._lastVisitedPath });
154
230
  }
155
231
  }
@@ -176,6 +252,15 @@ function mergeState (previousState, req, res, senderStateUpdate, firstInTurnover
176
252
  continue;
177
253
  }
178
254
  switch (value.t) {
255
+ case SESSION_DIALOGUE_CONTEXT: {
256
+ if (!lastInTurnover || previousState._lastVisitedPath === undefined) {
257
+ break;
258
+ }
259
+ if (value.p === true) {
260
+ state[key].p = res.newState._lastVisitedPath;
261
+ }
262
+ break;
263
+ }
179
264
  case DIALOG_CONTEXT: {
180
265
  // compare state
181
266
 
@@ -190,13 +275,13 @@ function mergeState (previousState, req, res, senderStateUpdate, firstInTurnover
190
275
  delete state[referencedKey];
191
276
  }
192
277
 
193
- if (lastInTurnover
194
- && previousState._lastVisitedPath !== undefined
195
- && value.p !== res.newState._lastVisitedPath) {
278
+ // if (lastInTurnover
279
+ // && previousState._lastVisitedPath !== undefined
280
+ // && value.p !== res.newState._lastVisitedPath) {
196
281
 
197
- delete state[key];
198
- delete state[referencedKey];
199
- }
282
+ // delete state[key];
283
+ // delete state[referencedKey];
284
+ // }
200
285
 
201
286
  break;
202
287
  }
@@ -234,6 +319,7 @@ function mergeState (previousState, req, res, senderStateUpdate, firstInTurnover
234
319
 
235
320
  module.exports = {
236
321
  VAR_TYPES,
322
+ prepareState,
237
323
  mergeState,
238
324
  vars,
239
325
  checkSetState,