wingbot 3.74.7 → 3.75.9-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.
@@ -0,0 +1,508 @@
1
+ /**
2
+ * @author David Menger
3
+ */
4
+ 'use strict';
5
+
6
+ const LLMRouter = require('./LLMRouter');
7
+ const { parseActionPayload } = require('./utils');
8
+ const { quickReplyAction } = require('./utils/quickReplies');
9
+ const stateData = require('./utils/stateData');
10
+ const { checkSetState } = require('./utils/stateVariables');
11
+
12
+ /** @typedef {import('./Request')} Request */
13
+ /** @typedef {import('./Responder')} Responder */
14
+ /** @typedef {import('./GlobalIntents').GlobalIntentsMap} GlobalIntentsMap */
15
+ /** @typedef {import('./GlobalIntents').ResolvedGlobalIntentsMap} ResolvedGlobalIntentsMap */
16
+ /** @typedef {import('./GlobalIntents').GlobalIntent} GlobalIntent */
17
+ /** @typedef {import('./LLMRouter').RouterResolver} RouterResolver */
18
+ /** @typedef {import('./Ai')} Ai */
19
+ /** @typedef {import('./LLM')} LLM */
20
+ /** @typedef {import('./LLMRouter').LLMRouting} LLMRouting */
21
+ /** @typedef {import('./LLMRouter').RoutingAction} RoutingAction */
22
+ /** @typedef {import('./utils/quickReplies').QuickReplyAction} QuickReplyAction */
23
+ /** @typedef {import('./LLMRouter').LLMRoutingCache} LLMRoutingCache */
24
+
25
+ const BASE64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
26
+
27
+ /**
28
+ * @typedef {object} DispatcherRouting
29
+ * @prop {Map<string, GlobalIntent>} giByAction
30
+ * @prop {GlobalIntent[]} nlpMatchers
31
+ */
32
+
33
+ /**
34
+ * @typedef {LLMRouting & DispatcherRouting} Routing
35
+ */
36
+
37
+ /**
38
+ * @typedef {object} ILLMRouter
39
+ * @prop {RouterResolver} resolve
40
+ */
41
+
42
+ /**
43
+ * @typedef {object} Logger
44
+ * @prop {Function} log
45
+ * @prop {Function} error
46
+ */
47
+
48
+ /**
49
+ * @typedef {object} DispatcherOptions
50
+ * @prop {Logger} [log]
51
+ */
52
+
53
+ /**
54
+ * @class LLMDispatcher
55
+ */
56
+ class LLMDispatcher {
57
+
58
+ /**
59
+ *
60
+ * @param {Ai} ai
61
+ * @param {Routing} routing
62
+ * @param {LLM} [llm]
63
+ * @param {DispatcherOptions} options
64
+ */
65
+ constructor (ai, routing = null, llm = null, options = {}) {
66
+ this._ai = ai;
67
+ this._llm = llm;
68
+
69
+ /** @type {Routing} */
70
+ this._routing = routing || {
71
+ router: null,
72
+ nlpMatchers: [],
73
+ giByAction: new Map(),
74
+ globals: {
75
+ actionListString: null,
76
+ actionList: [],
77
+ router: null,
78
+ byAction: new Map()
79
+ },
80
+ locals: new Map()
81
+ };
82
+
83
+ /** @type {QuickReplyAction[]} */
84
+ this._quickReplyActions = null;
85
+
86
+ /** @type {RoutingAction} */
87
+ this._action = null;
88
+
89
+ /** @type {RoutingAction[]} */
90
+ this._aiActions = null;
91
+
92
+ /** @type {Request} */
93
+ this._req = null;
94
+
95
+ this._log = options.log || console;
96
+
97
+ this._dispatched = false;
98
+
99
+ this._overrideAction = null;
100
+ }
101
+
102
+ /**
103
+ *
104
+ * @param {RoutingAction} action
105
+ */
106
+ overrideAction (action) {
107
+ this._overrideAction = action;
108
+ }
109
+
110
+ /**
111
+ *
112
+ * @param {Request} req
113
+ * @param {Responder} [res]
114
+ * @returns {Promise<RoutingAction>}
115
+ */
116
+ async dispatch (req, res) {
117
+ this._req = req;
118
+
119
+ try {
120
+ this._action = await this._postbackAction(res);
121
+ } catch (e) {
122
+ // console.log('DISPATCH FAILED -----\n\n', e);
123
+ this._log.error('LLMDispatch failed', e);
124
+ throw e;
125
+ }
126
+
127
+ // console.log('DISPATCHed -----\n\n');
128
+ this._dispatched = true;
129
+ return this._action;
130
+ }
131
+
132
+ /**
133
+ *
134
+ * @param {Responder} [responder]
135
+ * @returns {Promise<RoutingAction>}
136
+ */
137
+ async _postbackAction (responder = null) {
138
+ let res;
139
+ if (this._req.referral !== null && this._req.referral.ref) {
140
+ res = parseActionPayload({ payload: this._req.referral.ref });
141
+ }
142
+
143
+ if (!res && this._req.postback !== null) {
144
+ res = parseActionPayload(this._req.postback);
145
+ }
146
+
147
+ if (!res && this._req.optin !== null && this._req.optin.ref) {
148
+ res = this._base64Ref(this._req.optin);
149
+ }
150
+
151
+ if (!res && this._req.optin !== null && this._req.optin.payload) {
152
+ res = parseActionPayload(this._req.optin);
153
+ }
154
+
155
+ if (!res && this._req.message !== null && this._req.message.quick_reply) {
156
+ res = parseActionPayload(this._req.message.quick_reply);
157
+ }
158
+
159
+ /**
160
+ * TADY MUSI BYT HOTOVE AI
161
+ * - jsou nasazeny handlery s AI?
162
+ * - nepustíme už rovnou LLM?
163
+ * - AI vždy-s-textem/jen-s-handlery/
164
+ */
165
+
166
+ await this._ai.preloadAi(this._req, responder);
167
+
168
+ if (!res && this._req.state._expectedKeywords) {
169
+ res = this._actionByExpectedKeywords(this._req.state._expected);
170
+ }
171
+
172
+ let bounceAllow = false;
173
+ if (!res && this._req.state._expected) {
174
+ res = parseActionPayload(this._req.state._expected);
175
+ bounceAllow = !!(res
176
+ && this._req.state._expected.bounce === 'allow');
177
+ }
178
+
179
+ this._aiActions = this._resolveAiActions();
180
+
181
+ const routing = this._localLLmRoutingForState(this._req.state);
182
+
183
+ let aiRes;
184
+ let llmRes;
185
+ if ((!res || bounceAllow) && routing?.router) {
186
+ aiRes = await routing.router.resolve(this._llm, this._req, routing);
187
+ }
188
+
189
+ if ((!res || bounceAllow) && !aiRes) {
190
+ llmRes = this._getAiWinnerResAfterResolved();
191
+ }
192
+
193
+ if (aiRes || llmRes) {
194
+ res = aiRes || llmRes;
195
+ }
196
+
197
+ if (!res
198
+ && routing.keepUserInInteractionsWithBounceAllowed
199
+ && this._req.state.lastAction
200
+ && !this._req.state.lastAction.match(/\*$/)) {
201
+
202
+ return {
203
+ action: this._req.state.lastAction
204
+ };
205
+ }
206
+
207
+ return this._applySetStateToDetectedAction(res);
208
+ }
209
+
210
+ _getAiWinnerResAfterResolved () {
211
+ const [winner] = this._aiActions;
212
+
213
+ if (!winner?.winner) {
214
+ return null;
215
+ }
216
+
217
+ const _aiKeys = winner.setState ? Object.keys(winner.setState) : [];
218
+
219
+ return {
220
+ action: winner.action, data: {}, setState: winner.setState, _aiKeys
221
+ };
222
+ }
223
+
224
+ /**
225
+ *
226
+ * @param {*} state
227
+ * @returns {LLMRoutingCache}
228
+ */
229
+ _localLLmRoutingForState (state) {
230
+ const { lastAction } = state;
231
+
232
+ /** @type {LLMRoutingCache} */
233
+ let routing = this._routing.globals;
234
+
235
+ if (lastAction) {
236
+ let normalized = lastAction.replace(/\/$/, '');
237
+
238
+ if (this._routing.locals.has(normalized)) {
239
+ routing = this._routing.locals.get(normalized);
240
+ } else {
241
+ normalized = normalized.replace(/\/[^/]+$/, '');
242
+ if (this._routing.locals.has(normalized)) {
243
+ routing = this._routing.locals.get(normalized);
244
+ }
245
+ }
246
+ }
247
+
248
+ const router = routing?.router || this._routing.router;
249
+
250
+ if (!routing) {
251
+ return null;
252
+ }
253
+
254
+ return {
255
+ ...routing,
256
+ router
257
+ };
258
+ }
259
+
260
+ /**
261
+ *
262
+ * - dokonce i na "expectedKeywords"
263
+ *
264
+ * @param {RoutingAction} res
265
+ * @returns {RoutingAction}
266
+ */
267
+ _applySetStateToDetectedAction (res) {
268
+ // find global intent
269
+ if (!res) {
270
+ return res;
271
+ }
272
+ let { setState = {} } = res;
273
+ let entitiesSetState = {};
274
+ const gi = this._routing.giByAction.get(res.action);
275
+ if (gi && gi.action === res.action) {
276
+ entitiesSetState = { ...gi.entitiesSetState };
277
+
278
+ const values = Array.from(Object.values(entitiesSetState));
279
+
280
+ for (const value of values) {
281
+ if (typeof value === 'function') {
282
+ Object.assign(entitiesSetState, value(stateData(this._req)));
283
+ }
284
+ }
285
+ }
286
+ const newState = {
287
+ ...entitiesSetState,
288
+ ...setState
289
+ };
290
+ checkSetState(setState, newState);
291
+ setState = newState;
292
+ const aiKeysSet = new Set([
293
+ ...(res._aiKeys || []),
294
+ ...Object.keys(entitiesSetState)
295
+ ]);
296
+ const aiKeys = Array.from(aiKeysSet)
297
+ .filter((k) => typeof setState[k] !== 'undefined' && k.startsWith('@'));
298
+
299
+ return {
300
+ ...res,
301
+ setState,
302
+ _aiKeys: aiKeys
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Returns resolved action
308
+ *
309
+ * @returns {RoutingAction}
310
+ */
311
+ action () {
312
+ if (this._overrideAction) {
313
+ return this._overrideAction;
314
+ }
315
+ // if (!this._dispatched) {
316
+ // throw new Error('LLM dispatcher wansn\'t dispatched!');
317
+ // }
318
+ return this._action;
319
+ }
320
+
321
+ /**
322
+ * Returns full detected AI actions
323
+ *
324
+ * @returns {RoutingAction[]}
325
+ */
326
+ aiActions () {
327
+ if (!this._dispatched) {
328
+ return [];
329
+ }
330
+ return this._aiActions;
331
+ }
332
+
333
+ /**
334
+ * Returns full detected AI action
335
+ *
336
+ * @returns {RoutingAction}
337
+ */
338
+ aiActionWinner () {
339
+ // if (!this._dispatched) {
340
+ // throw new Error('LLM dispatcher wansn\'t dispatched!');
341
+ // }
342
+ return this._aiActions[0]?.winner ? this._aiActions[0] : null;
343
+ }
344
+
345
+ /**
346
+ * Yields global intents at the current request path.
347
+ *
348
+ * @returns {Generator<[GlobalIntent, boolean]>}
349
+ */
350
+ * globalIntentsAtPath () {
351
+ // to match the local context intent
352
+ const localRegexToMatch = this._req.getLocalPathRegexp();
353
+
354
+ for (const gi of this._routing.nlpMatchers) {
355
+ const pathMatches = localRegexToMatch && localRegexToMatch.exec(gi.action);
356
+ if (gi.local && !pathMatches) {
357
+ continue;
358
+ }
359
+ yield [gi, !!pathMatches];
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Returns full detected AI action
365
+ *
366
+ * @returns {RoutingAction[]}
367
+ */
368
+ _resolveAiActions () {
369
+ if (!this._req.isTextOrIntent()) {
370
+ return [];
371
+ }
372
+
373
+ /**
374
+ * @typedef {object} ExtendRoutingAction
375
+ * @prop {number} sort
376
+ */
377
+
378
+ /** @type {(RoutingAction & ExtendRoutingAction)[]} */
379
+ const aiActions = [];
380
+
381
+ const iterator = this.globalIntentsAtPath();
382
+
383
+ for (const [gi, pathMatches] of iterator) {
384
+ const intent = gi.matcher(this._req); // , null, true
385
+ if (intent !== null) {
386
+ const sort = intent.score + (pathMatches
387
+ ? this._ai.localEnhancement
388
+ : 0);
389
+ // console.log(sort, wi.intent);
390
+ aiActions.push({
391
+ action: gi.action,
392
+ ...gi,
393
+ intent,
394
+ setState: intent.setState,
395
+ aboveConfidence: intent.aboveConfidence,
396
+ sort,
397
+ winner: false
398
+ });
399
+ }
400
+ }
401
+
402
+ aiActions.sort((l, r) => r.sort - l.sort);
403
+ this._setWinner(aiActions);
404
+ return aiActions;
405
+ }
406
+
407
+ _setWinner (aiActions) {
408
+ if (aiActions.length === 0 || !aiActions[0].aboveConfidence) {
409
+ return null;
410
+ }
411
+
412
+ // there will be no winner, if there are two different intents
413
+ if (this._ai.shouldDisambiguate(aiActions)) {
414
+ return null;
415
+ }
416
+
417
+ if (aiActions[0]) {
418
+ // eslint-disable-next-line no-param-reassign
419
+ aiActions[0].winner = true;
420
+ }
421
+
422
+ return aiActions[0];
423
+ }
424
+
425
+ _actionByExpectedKeywords (expected) {
426
+ // @ts-ignore
427
+ if (!this._req.state._expectedKeywords) {
428
+ return null;
429
+ }
430
+
431
+ const actions = this.resolveQuickReplyActions();
432
+
433
+ // @ts-ignore
434
+ if (expected && this._ai.shouldDisambiguate(actions, true)) {
435
+ return parseActionPayload(expected);
436
+ }
437
+
438
+ const [payload] = actions;
439
+ if (!payload || !payload.aboveConfidence) {
440
+ return null;
441
+ }
442
+
443
+ return parseActionPayload(payload);
444
+ }
445
+
446
+ /**
447
+ *
448
+ * @returns {QuickReplyAction[]}
449
+ */
450
+ resolveQuickReplyActions () {
451
+ if (this._quickReplyActions === null) {
452
+ if (this._req.state._expectedKeywords) {
453
+ this._quickReplyActions = quickReplyAction(
454
+ this._req.state._expectedKeywords,
455
+ this._req,
456
+ this._ai
457
+ );
458
+ } else {
459
+ this._quickReplyActions = [];
460
+ }
461
+ }
462
+ return this._quickReplyActions;
463
+ }
464
+
465
+ _base64Ref (object = {}) {
466
+ let process = {};
467
+
468
+ if (object && object.ref) {
469
+ let payload = object.ref;
470
+
471
+ if (typeof payload === 'string' && payload.match(BASE64_REGEX)) {
472
+ payload = Buffer.from(payload, 'base64').toString('utf8');
473
+ }
474
+
475
+ process = { payload };
476
+ }
477
+
478
+ return parseActionPayload(process);
479
+ }
480
+
481
+ /**
482
+ *
483
+ * @param {ResolvedGlobalIntentsMap} globalIntents
484
+ * @returns {Routing}
485
+ */
486
+ static prepareRouting (globalIntents) {
487
+ /** @type {Map<string,GlobalIntent>} */
488
+ const giByAction = new Map();
489
+ const giMatchersByAction = new Map();
490
+ for (const gi of globalIntents.values()) {
491
+ if (gi.action) {
492
+ giByAction.set(gi.action, gi);
493
+ if (gi.matcher) {
494
+ giMatchersByAction.set(gi.action, gi);
495
+ }
496
+ }
497
+ }
498
+
499
+ return {
500
+ ...LLMRouter.prepareRouting(globalIntents),
501
+ nlpMatchers: Array.from(giMatchersByAction.values()),
502
+ giByAction
503
+ };
504
+ }
505
+
506
+ }
507
+
508
+ module.exports = LLMDispatcher;
@@ -8,6 +8,18 @@ const LLM = require('./LLM');
8
8
  /** @typedef {import('./LLM').LLMChatProvider} LLMChatProvider */
9
9
  /** @typedef {import('./LLM').LLMMessage} LLMMessage */
10
10
  /** @typedef {import('./LLM').LLMProviderOptions} LLMProviderOptions */
11
+ /** @typedef {import('./LLM').LLMPresetName} LLMPresetName */
12
+
13
+ /**
14
+ * @template {object|string} [C=object|string]
15
+ * @typedef {object} LLMMockResponse
16
+ * @prop {LLMPresetName} preset
17
+ * @prop {C} content
18
+ */
19
+
20
+ /**
21
+ * @typedef {LLMMockResponse<{action:string}>} LLMRoutingResponse
22
+ */
11
23
 
12
24
  /**
13
25
  * @class LLMMockProvider
@@ -17,7 +29,12 @@ class LLMMockProvider {
17
29
 
18
30
  static DEFAULT_MODEL = 'mockmodel';
19
31
 
20
- constructor () {
32
+ /**
33
+ *
34
+ * @param {LLMMockResponse[]} [mocks]
35
+ */
36
+ constructor (mocks = []) {
37
+ this._mocks = mocks || [];
21
38
  this._index = 0;
22
39
  this._sequence = [
23
40
  'lorem',
@@ -28,6 +45,26 @@ class LLMMockProvider {
28
45
  ];
29
46
  }
30
47
 
48
+ /**
49
+ *
50
+ * @param {string} preset
51
+ * @returns {LLMMessage}
52
+ */
53
+ _mockResponseForPreset (preset) {
54
+ const found = this._mocks.find((mock) => mock.preset === preset);
55
+
56
+ if (!found) {
57
+ return null;
58
+ }
59
+
60
+ return {
61
+ role: LLM.ROLE_ASSISTANT,
62
+ content: typeof found.content === 'object'
63
+ ? JSON.stringify(found.content)
64
+ : found.content
65
+ };
66
+ }
67
+
31
68
  /**
32
69
  * @param {LLMMessage[]} prompt
33
70
  * @param {LLMProviderOptions} [options]
@@ -65,6 +102,12 @@ class LLMMockProvider {
65
102
  };
66
103
  }
67
104
 
105
+ // simple for now
106
+ const mockResponse = this._mockResponseForPreset(options.preset || 'default');
107
+ if (mockResponse) {
108
+ return mockResponse;
109
+ }
110
+
68
111
  // const stats = prompt.reduce((o, m) => Object.assign(o, {
69
112
  // [m.role]: (o[m.role] || 0) + 1
70
113
  // }), { system: 0, assistant: 0, user: 0 });