wingbot 3.39.0-alpha.2 → 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.2",
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/Processor.js CHANGED
@@ -1,4 +1,4 @@
1
- /*
1
+ /**
2
2
  * @author David Menger
3
3
  */
4
4
  'use strict';
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';
@@ -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) {