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 +1 -1
- package/plugins/ai.wingbot.slotsContinue/plugin.js +44 -0
- package/plugins/ai.wingbot.slotsRegister/plugin.js +89 -0
- package/plugins/plugins.json +75 -0
- package/src/Processor.js +1 -1
- package/src/Tester.js +46 -5
- package/src/resolvers/message.js +4 -0
- package/src/testTools/asserts.js +2 -2
- package/src/utils/getUpdate.js +1 -1
- package/src/utils/slots.js +113 -0
- package/src/utils/stateVariables.js +17 -2
package/package.json
CHANGED
|
@@ -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;
|
package/plugins/plugins.json
CHANGED
|
@@ -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
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
|
-
|
|
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;
|
package/src/resolvers/message.js
CHANGED
package/src/testTools/asserts.js
CHANGED
|
@@ -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:
|
|
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) {
|
package/src/utils/getUpdate.js
CHANGED
|
@@ -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}`]: {
|
|
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) {
|