wingbot 3.39.0-alpha.1 → 3.39.0-alpha.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.39.0-alpha.1",
3
+ "version": "3.39.0-alpha.3",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,44 @@
1
+ const { StepState, StepType, getNextStep } = require('../../src/utils/slots');
2
+ const { vars } = require('../../src/utils/stateVariables');
3
+
4
+ /** @typedef {import('../../src/Router').Resolver} Resolver */
5
+ /** @typedef {import('../../src/Responder')} Responder */
6
+ /** @typedef {import('../../src/utils/slots').SlotBotState} SlotBotState */
7
+ /** @typedef {import('../../src/utils/slots').SlotsResolver} SlotsResolver */
8
+ /** @typedef {import('../../src/utils/slots').SlotsRequest} SlotsRequest */
9
+
10
+ /**
11
+ * @returns {SlotsResolver}
12
+ */
13
+ function slotContinue () {
14
+
15
+ /**
16
+ * @param {SlotsRequest} req
17
+ * @param {Responder} res
18
+ * @param {Function} postBack
19
+ * @returns {Promise}
20
+ */
21
+ async function slotContinuePlugin (req, res, postBack) {
22
+ const state = { ...req.state, ...res.newState };
23
+
24
+ const { _slotStep: step } = state;
25
+ let { _slotState: slotState } = state;
26
+
27
+ if (!step || !slotState) {
28
+ const msg = 'ERROR: slot filling was not initialized (use the "continue" plugin after the "initialize")';
29
+ res.text(msg);
30
+ throw new Error(msg);
31
+ }
32
+ slotState = slotState.map((s) => (s.e === step.entity
33
+ ? { ...s, s: StepState.FILLED }
34
+ : s));
35
+
36
+ res.setState({ _slotState: slotState });
37
+
38
+ return getNextStep(req, res, postBack);
39
+ }
40
+
41
+ return slotContinuePlugin;
42
+ }
43
+
44
+ module.exports = slotContinue;
@@ -0,0 +1,89 @@
1
+ const { webalize } = require('webalize');
2
+ const { ai } = require('../../src/Ai');
3
+ const Router = require('../../src/Router');
4
+ const { StepState, StepType, getNextStep } = require('../../src/utils/slots');
5
+ const { vars } = require('../../src/utils/stateVariables');
6
+
7
+ /** @typedef {import('../../src/utils/slots').SlotsResolver} SlotsResolver */
8
+ /** @typedef {import('../../src/utils/slots').SlotPluginConfig} SlotPluginConfig */
9
+ /** @typedef {import('../../src/utils/slots').SlotBotState} SlotBotState */
10
+ /** @typedef {import('../../src/utils/slots').SlotState} SlotState */
11
+
12
+ /**
13
+ *
14
+ * @param {SlotPluginConfig} params
15
+ * @returns {Router<SlotBotState>}
16
+ */
17
+ function slotsRegister ({
18
+ steps = [],
19
+ doneAction,
20
+ intents = ''
21
+ }) {
22
+
23
+ const useIntents = intents.split(',')
24
+ .map((i) => i.trim());
25
+
26
+ /** @type {Router<SlotBotState>} */
27
+ const bot = new Router();
28
+
29
+ const setEntities = [];
30
+
31
+ /** @type {SlotsResolver} */
32
+ const handler = async (req, res, postBack) => {
33
+ // offer rooms or (does'nt matter)
34
+
35
+ /** @type {SlotState[]} */
36
+ const slotState = steps.map((step) => {
37
+ const entity = step.entity.replace(/^@/, '');
38
+ const entities = req.entities.filter((e) => e.entity === entity)
39
+ .map((e) => e.value);
40
+
41
+ if (entities.length === 0) {
42
+ return {
43
+ e: step.entity,
44
+ s: step.type === StepType.ADDITIONAL
45
+ ? StepState.FILLED
46
+ : StepState.INITIALIZED
47
+ };
48
+ }
49
+
50
+ if (step.type === StepType.MULTI) {
51
+ setEntities.push(vars.dialogContext(`+${entity}`, entities, true));
52
+ } else {
53
+ setEntities.push(vars.dialogContext(step.entity, entities[0], true));
54
+ }
55
+
56
+ return {
57
+ e: step.entity,
58
+ s: step.validateAction ? StepState.FILLED : StepState.VALID
59
+ };
60
+ });
61
+
62
+ res.setState({
63
+ _slotState: slotState,
64
+ _slotSteps: steps,
65
+ _slotDone: doneAction,
66
+ ...Object.fromEntries(
67
+ steps.map(({ entity, type }) => (type === StepType.MULTI
68
+ ? [entity.replace(/^@/, '+'), []]
69
+ : [entity, null]))
70
+ ),
71
+ ...setEntities.reduce((o, r) => Object.assign(o, r), {})
72
+ });
73
+
74
+ return getNextStep(req, res, postBack);
75
+ };
76
+
77
+ bot.use(ai.global('/', useIntents), handler);
78
+
79
+ for (const step of steps) {
80
+ bot.use(
81
+ ai.global(webalize(step.entity), [...useIntents, step.entity]),
82
+ handler
83
+ );
84
+ }
85
+
86
+ return bot;
87
+ }
88
+
89
+ module.exports = slotsRegister;
@@ -375,6 +375,81 @@
375
375
  "label": "Text to test (keep empty to use user input)"
376
376
  }
377
377
  ]
378
+ },
379
+ {
380
+ "id": "ai.wingbot.slotsRegister",
381
+ "name": "Slot filling: register",
382
+ "description": "Sets up slot filling conversation for a list of entities",
383
+ "availableSince": 3.39,
384
+ "editable": false,
385
+ "isFactory": true,
386
+ "category": "conversation",
387
+ "inputs": [
388
+ {
389
+ "type": "text",
390
+ "name": "intents",
391
+ "label": "Intent list (comma separated)",
392
+ "validations": [
393
+ { "type": "regexp", "value": "^[^\\s]+$", "message": "intent for slot filling should not be empty" }
394
+ ]
395
+ },
396
+ {
397
+ "type": "postback",
398
+ "name": "doneAction",
399
+ "label": "Continue here, when finished"
400
+ },
401
+ {
402
+ "type": "array",
403
+ "name": "steps",
404
+ "label": "Slot",
405
+ "keyPropertyName": "entity",
406
+ "inputs": [
407
+ {
408
+ "type": "text",
409
+ "name": "entity",
410
+ "label": "Entity (with @)",
411
+ "validations": [
412
+ { "type": "regexp", "value": "^@[a-zA-Z0-9-]+$", "message": "the entity for the slot filling should be valid" }
413
+ ]
414
+ },
415
+ {
416
+ "type": "select",
417
+ "name": "type",
418
+ "label": "Question",
419
+ "options": [
420
+ { "label": "Required", "value": "req" },
421
+ { "label": "Multi value", "value": "mul" },
422
+ { "label": "Additional", "value": "add" }
423
+ ]
424
+ },
425
+ {
426
+ "type": "postback",
427
+ "name": "askAction",
428
+ "label": "Question"
429
+ },
430
+ {
431
+ "type": "postback",
432
+ "optional": true,
433
+ "name": "validateAction",
434
+ "label": "Validation"
435
+ }
436
+ ]
437
+ }
438
+ ],
439
+ "items": [
440
+ ]
441
+ },
442
+ {
443
+ "id": "ai.wingbot.slotsContinue",
444
+ "name": "Slot filling: continue",
445
+ "description": "move the slot filling to the next step",
446
+ "availableSince": 3.39,
447
+ "editable": false,
448
+ "isFactory": true,
449
+ "inputs": [
450
+ ],
451
+ "items": [
452
+ ]
378
453
  }
379
454
  ],
380
455
  "categories": [
package/src/AiMatching.js CHANGED
@@ -191,7 +191,7 @@ class AiMatching {
191
191
  return returnIfEmpty;
192
192
  }
193
193
 
194
- _hbsOrFn (value, cb) {
194
+ _hbsOrFn (value) {
195
195
  if (typeof value === 'string') {
196
196
  let useValue = value;
197
197
  if (useValue.match(/^\$[a-zA-Z0-9_-]+$/)) {
@@ -199,13 +199,12 @@ class AiMatching {
199
199
  }
200
200
  if (useValue.match(/\{\{.+\}\}/)) {
201
201
  const compiler = handlebars.compile(useValue);
202
- return (data) => {
203
- const res = compiler(data);
204
- return cb(res);
205
- };
202
+ // @ts-ignore
203
+ compiler.template = useValue;
204
+ return compiler;
206
205
  }
207
206
  }
208
- return cb(value);
207
+ return value;
209
208
  }
210
209
 
211
210
  _normalizeComparisonArray (compare, op) {
@@ -220,7 +219,7 @@ class AiMatching {
220
219
  const [val] = arr;
221
220
 
222
221
  return [
223
- this._hbsOrFn(val, (r) => this._normalizeToNumber(r, 0))
222
+ this._hbsOrFn(val)
224
223
  ];
225
224
  }
226
225
 
@@ -228,12 +227,12 @@ class AiMatching {
228
227
  const [min, max] = arr;
229
228
 
230
229
  return [
231
- this._hbsOrFn(min, (r) => this._normalizeToNumber(r, -Infinity)),
232
- this._hbsOrFn(max, (r) => this._normalizeToNumber(r, Infinity))
230
+ this._hbsOrFn(min),
231
+ this._hbsOrFn(max)
233
232
  ];
234
233
  }
235
234
 
236
- return arr.map((cmp) => this._hbsOrFn(cmp, (r) => `${r}`));
235
+ return arr.map((cmp) => this._hbsOrFn(cmp));
237
236
  }
238
237
 
239
238
  _stringOpToOperation (op) {
@@ -285,6 +284,7 @@ class AiMatching {
285
284
  */
286
285
  getSetStateForEntityRules ({ entities }) {
287
286
  return entities.reduce((o, rule) => {
287
+
288
288
  if (rule instanceof RegExp) {
289
289
  return o;
290
290
  }
@@ -297,13 +297,13 @@ class AiMatching {
297
297
 
298
298
  if (rule.op === COMPARE.EQUAL
299
299
  && rule.compare
300
- && rule.compare.length === 1
301
- && typeof rule.compare[0] !== 'function') {
300
+ && rule.compare.length === 1) {
302
301
 
303
302
  const key = `@${rule.entity}`;
304
303
  const value = rule.compare[0];
305
304
 
306
- return Object.assign(o, vars.dialogContext(key, value));
305
+ // @ts-ignore
306
+ return vars.dialogContext(key, value && (value.template || value));
307
307
  }
308
308
  return o;
309
309
  }, {});
@@ -732,11 +732,15 @@ class AiMatching {
732
732
  : false;
733
733
  }
734
734
 
735
- const useCmp = (compare || [])
735
+ let useCmp = (compare || [])
736
736
  .map((c) => (typeof c === 'function'
737
737
  ? c(requestState)
738
738
  : c));
739
739
 
740
+ if ([COMPARE.EQUAL, COMPARE.NOT_EQUAL].includes(operation)) {
741
+ useCmp = useCmp.map((c) => (typeof c === 'string' ? c : `${c}`));
742
+ }
743
+
740
744
  switch (operation) {
741
745
  case COMPARE.EQUAL:
742
746
  return useCmp.length === 0 || useCmp.includes(`${value}`);
@@ -748,7 +752,8 @@ class AiMatching {
748
752
  if (normalized === null) {
749
753
  return false;
750
754
  }
751
- return normalized >= min && normalized <= max;
755
+ return normalized >= this._normalizeToNumber(min, -Infinity)
756
+ && normalized <= this._normalizeToNumber(max, Infinity);
752
757
  }
753
758
  case COMPARE.GT:
754
759
  case COMPARE.LT:
@@ -759,7 +764,7 @@ class AiMatching {
759
764
  if (normalized === null) {
760
765
  return false;
761
766
  }
762
- return this._numberComparison(op, cmp, normalized);
767
+ return this._numberComparison(op, this._normalizeToNumber(cmp, 0), normalized);
763
768
  }
764
769
  default:
765
770
  return true;
package/src/Processor.js CHANGED
@@ -1,4 +1,4 @@
1
- /*
1
+ /**
2
2
  * @author David Menger
3
3
  */
4
4
  'use strict';
package/src/Request.js CHANGED
@@ -1137,7 +1137,15 @@ class Request {
1137
1137
  let { setState = {} } = res;
1138
1138
  for (const gi of this.globalIntents.values()) {
1139
1139
  if (gi.action === res.action) {
1140
- ({ entitiesSetState } = gi);
1140
+ entitiesSetState = { ...gi.entitiesSetState };
1141
+
1142
+ const values = Array.from(Object.values(entitiesSetState));
1143
+
1144
+ for (const value of values) {
1145
+ if (typeof value === 'function') {
1146
+ Object.assign(entitiesSetState, value(stateData(this)));
1147
+ }
1148
+ }
1141
1149
  }
1142
1150
  }
1143
1151
  const newState = {
package/src/Tester.js CHANGED
@@ -290,6 +290,14 @@ class Tester {
290
290
  || (!botAction.match(/\*/) && actionMatches(botAction, path));
291
291
  }
292
292
 
293
+ _actionsDebug (matchRequestActions = false) {
294
+ const set = new Set();
295
+ return this.actions
296
+ .filter((a) => !a.isReqAction || matchRequestActions)
297
+ .map((a) => (a.doNotTrack ? `(system interaction) ${a.action}` : a.action))
298
+ .filter((a) => !set.has(a) && set.add(a));
299
+ }
300
+
293
301
  /**
294
302
  * Checks, that request passed an interaction
295
303
  *
@@ -305,11 +313,7 @@ class Tester {
305
313
  && this._actionMatches(action.action, path));
306
314
  let actual;
307
315
  if (!ok) {
308
- const set = new Set();
309
- actual = this.actions
310
- .filter((a) => !a.isReqAction || matchRequestActions)
311
- .map((a) => (a.doNotTrack ? `(system interaction) ${a.action}` : a.action))
312
- .filter((a) => !set.has(a) && set.add(a));
316
+ actual = this._actionsDebug(matchRequestActions);
313
317
  assert.fail(asserts.ex('Interaction was not passed', path, actual));
314
318
  }
315
319
  return this;
@@ -419,6 +423,22 @@ class Tester {
419
423
  .intentWithEntity(this.senderId, text, intent, entity, value, score));
420
424
  }
421
425
 
426
+ /**
427
+ * Makes recognised AI request with entity
428
+ *
429
+ * @param {string} entity
430
+ * @param {string} [value]
431
+ * @param {string} [text]
432
+ * @param {number} [score]
433
+ * @returns {Promise}
434
+ *
435
+ * @memberOf Tester
436
+ */
437
+ entity (entity, value = entity, text = value, score = 1) {
438
+ return this.processMessage(Request
439
+ .intentWithEntity(this.senderId, text, `random-${Date.now()}`, entity, value, score));
440
+ }
441
+
422
442
  /**
423
443
  * Make optin call
424
444
  *
@@ -523,6 +543,27 @@ class Tester {
523
543
  .postBack(this.senderId, action, data, refAction, refData, null));
524
544
  }
525
545
 
546
+ /**
547
+ * Prints last conversation turnaround
548
+ *
549
+ * @param {boolean} [showPrivateKeys]
550
+ */
551
+ debug (showPrivateKeys = false) {
552
+ // eslint-disable-next-line no-console
553
+ console.log(
554
+ '\n===== actions =====\n',
555
+ this._actionsDebug(true),
556
+ '\n---- responses ----\n',
557
+ this.responses.map(({ messaging_type: m, recipient, ...o }) => o),
558
+ '\n------ state ------\n',
559
+ Object.fromEntries(
560
+ Object.entries(this.getState().state)
561
+ .filter((e) => showPrivateKeys || !e[0].startsWith('_'))
562
+ ),
563
+ '\n===================\n'
564
+ );
565
+ }
566
+
526
567
  }
527
568
 
528
569
  module.exports = Tester;
@@ -286,6 +286,10 @@ function message (params, context = {}) {
286
286
 
287
287
  res.text(text, sendReplies, voiceControl);
288
288
 
289
+ if (isLastMessage && !req.actionData()._resolverTag) {
290
+ res.finalMessageSent = true;
291
+ }
292
+
289
293
  return ret;
290
294
  };
291
295
  }
@@ -30,8 +30,8 @@ function m (text, actual = null, expected = null) {
30
30
 
31
31
  function ex (message, expected, actual) {
32
32
  const actuals = Array.isArray(actual) ? actual : [actual];
33
- return `${message}\n + expected: "${expected}"\n - actual: ${actuals
34
- .map((a) => `"${a}"`).join('\n ')}`;
33
+ return `${message}\n + expected: "${expected}"\n - actual: ${actuals
34
+ .map((a) => `"${a}"`).join('\n ')}`;
35
35
  }
36
36
 
37
37
  function getText (response) {
@@ -13,7 +13,7 @@ try {
13
13
  handlebars = { compile: (text) => () => text };
14
14
  }
15
15
 
16
- const ATTR_REGEX = /^([^.+]*)\.?(.+)?$/;
16
+ const ATTR_REGEX = /^([^.]*)\.?(.+)?$/;
17
17
  const SCALAR_TYPES = ['string', 'number', 'boolean'];
18
18
  const SUBSCRIBE = '_$subscribe';
19
19
  const UNSUBSCRIBE = '_$unsubscribe';
@@ -208,6 +208,7 @@ function makeQuickReplies (replies, path = '', translate = (w) => w, quickReplyC
208
208
  }
209
209
  return o;
210
210
  }, {});
211
+
211
212
  setState = {
212
213
  ...entitiesSetState,
213
214
  ...setState
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @author {David Menger}
3
+ */
4
+ 'use strict';
5
+
6
+ const Router = require('../Router');
7
+ const Request = require('../Request'); // eslint-disable-line no-unused-vars
8
+
9
+ /** @typedef {import('../Responder')} Responder */
10
+
11
+ /**
12
+ * @readonly
13
+ * @enum {string}
14
+ */
15
+ const StepType = {
16
+ REQUIRED: 'req',
17
+ MULTI: 'mul',
18
+ ADDITIONAL: 'add'
19
+ };
20
+
21
+ /**
22
+ * @readonly
23
+ * @enum {string}
24
+ */
25
+ const StepState = {
26
+ INITIALIZED: 'i',
27
+ FILLED: 'f',
28
+ VALID: 'v'
29
+ };
30
+
31
+ /**
32
+ * @typedef {object} SlotStep
33
+ * @prop {string} entity
34
+ * @prop {StepType} type
35
+ * @prop {string} [validateAction]
36
+ * @prop {string} askAction
37
+ */
38
+
39
+ /**
40
+ * @typedef {object} SlotState
41
+ * @prop {string} e
42
+ * @prop {StepState} s
43
+ */
44
+
45
+ /**
46
+ * @typedef {object} SlotBotState
47
+ * @prop {SlotState[]} _slotState
48
+ * @prop {SlotStep} [_slotStep]
49
+ * @prop {SlotStep[]} [_slotSteps]
50
+ * @prop {string} [_slotDone]
51
+ */
52
+
53
+ /**
54
+ * @typedef {object} SlotPluginConfig
55
+ * @prop {SlotStep[]} steps
56
+ * @prop {string} doneAction
57
+ * @prop {string} intents
58
+ */
59
+
60
+ /** @typedef {import('../Router').Resolver<SlotBotState>} SlotsResolver */
61
+ /** @typedef {Request<SlotBotState>} SlotsRequest */
62
+
63
+ /**
64
+ * @param {Request<SlotBotState>} req
65
+ * @param {Responder} res
66
+ * @param {Function} postBack
67
+ * @returns {Promise}
68
+ */
69
+ async function getNextStep (req, res, postBack) {
70
+ let invalid = null;
71
+ const {
72
+ _slotState: slotState, _slotSteps: steps, _slotDone: doneAction
73
+ } = { ...req.state, ...res.newState };
74
+
75
+ for (const slot of slotState) {
76
+ const step = steps.find((s) => s.entity === slot.e);
77
+ if (slot.s === StepState.FILLED && step && step.validateAction) {
78
+ // eslint-disable-next-line
79
+ await postBack(step.validateAction, {}, true);
80
+ if (res.finalMessageSent) {
81
+ invalid = step;
82
+ break;
83
+ }
84
+ slot.s = StepState.VALID;
85
+ } else if (slot.s === StepState.FILLED) {
86
+ slot.s = StepState.VALID;
87
+ }
88
+ }
89
+
90
+ res.setState({ _slotState: slotState });
91
+
92
+ if (invalid) {
93
+ res.setState({ _slotStep: invalid });
94
+ return Router.END;
95
+ }
96
+
97
+ const nextStep = slotState.find((s) => s.s !== StepState.VALID);
98
+
99
+ if (!nextStep) {
100
+ postBack(doneAction);
101
+ return Router.END;
102
+ }
103
+ const stepConfig = steps.find((s) => s.entity === nextStep.e);
104
+ res.setState({ _slotStep: stepConfig });
105
+ postBack(stepConfig.askAction);
106
+ return Router.END;
107
+ }
108
+
109
+ module.exports = {
110
+ getNextStep,
111
+ StepState,
112
+ StepType
113
+ };
@@ -26,15 +26,19 @@ const vars = {
26
26
  *
27
27
  * @param {string} key
28
28
  * @param {*} value
29
+ * @param {string} [path]
29
30
  * @returns {object}
30
31
  * @example
31
32
  * const { vars } = require('wingbot');
32
33
  * res.setState(vars.dialogContext('myKey', 'foovalue'))
33
34
  */
34
- dialogContext (key, value) {
35
+ dialogContext (key, value, path = null) {
35
36
  return {
36
37
  [key]: value,
37
- [`_~${key}`]: { t: DIALOG_CONTEXT }
38
+ [`_~${key}`]: {
39
+ t: DIALOG_CONTEXT,
40
+ ...(path && { p: path })
41
+ }
38
42
  };
39
43
  },
40
44
 
@@ -166,6 +170,17 @@ function mergeState (previousState, req, res, senderStateUpdate, firstInTurnover
166
170
  case DIALOG_CONTEXT: {
167
171
  // compare state
168
172
 
173
+ if (!lastInTurnover || previousState._lastVisitedPath === undefined) {
174
+ break;
175
+ }
176
+
177
+ if (value.p === true) {
178
+ state[key].p = res.newState._lastVisitedPath;
179
+ } else if (value.p !== res.newState._lastVisitedPath) {
180
+ delete state[key];
181
+ delete state[referencedKey];
182
+ }
183
+
169
184
  if (lastInTurnover
170
185
  && previousState._lastVisitedPath !== undefined
171
186
  && value.p !== res.newState._lastVisitedPath) {