wingbot 3.71.6 → 3.72.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.
@@ -15,7 +15,7 @@ jobs:
15
15
 
16
16
  strategy:
17
17
  matrix:
18
- node-version: [16.x]
18
+ node-version: [20.x]
19
19
 
20
20
  steps:
21
21
 
@@ -23,7 +23,7 @@ jobs:
23
23
  uses: actions/checkout@v2
24
24
 
25
25
  - name: Node.js ${{ matrix.node-version }} setup
26
- uses: actions/setup-node@v2
26
+ uses: actions/setup-node@v3
27
27
  with:
28
28
  node-version: ${{ matrix.node-version }}
29
29
  cache: 'npm'
@@ -11,7 +11,7 @@ jobs:
11
11
 
12
12
  strategy:
13
13
  matrix:
14
- node-version: [16.x]
14
+ node-version: [20.x]
15
15
 
16
16
  steps:
17
17
 
@@ -19,7 +19,7 @@ jobs:
19
19
  uses: actions/checkout@v2
20
20
 
21
21
  - name: Node.js ${{ matrix.node-version }} setup
22
- uses: actions/setup-node@v2
22
+ uses: actions/setup-node@v3
23
23
  with:
24
24
  node-version: ${{ matrix.node-version }}
25
25
  cache: 'npm'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.71.6",
3
+ "version": "3.72.1",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -58,7 +58,9 @@
58
58
  "compress-json": "^3.0.0",
59
59
  "deep-extend": "^0.6.0",
60
60
  "form-data": "^4.0.0",
61
- "graphql": "^16.8.1",
61
+ "graphql": "^16.11.0",
62
+ "graphql-depth-limit": "^1.1.0",
63
+ "graphql-query-complexity": "^1.1.0",
62
64
  "jsonwebtoken": "^9.0.2",
63
65
  "node-fetch": "^2.6.7",
64
66
  "path-to-regexp": "^6.3.0",
package/src/Ai.js CHANGED
@@ -41,8 +41,10 @@ let uq = 1;
41
41
 
42
42
  /** @typedef {import('./Request').IntentAction} IntentAction */
43
43
  /** @typedef {import('./Request')} Request */
44
+ /** @typedef {import('./Request').TextAlternative} TextAlternative */
44
45
  /** @typedef {import('./Responder')} Responder */
45
46
  /** @typedef {import('./Router').Resolver} Resolver */
47
+ /** @typedef {import('./utils/stateData').IStateRequest} IStateRequest */
46
48
  /** @typedef {import('./wingbot/CachedModel').Result} Result */
47
49
  /** @typedef {import('./wingbot/CustomEntityDetectionModel').Phrases} Phrases */
48
50
  /** @typedef {import('./wingbot/CustomEntityDetectionModel').EntityDetector} EntityDetector */
@@ -137,7 +139,7 @@ class Ai {
137
139
  * The prefix translator - for request-specific prefixes
138
140
  *
139
141
  * @param {string} defaultModel
140
- * @param {Request} req
142
+ * @param {IStateRequest} req
141
143
  */
142
144
  this.getPrefix = (defaultModel, req) => req.state.lang || defaultModel; // eslint-disable-line
143
145
 
@@ -857,11 +859,22 @@ class Ai {
857
859
  .filter((alt) => alt.score >= this.sttScoreThreshold)
858
860
  .slice(0, this.sttMaxAlternatives);
859
861
 
860
- const altMax = Math.max(0, ...texts.map((t) => t.score));
862
+ return this._queryModelWithTexts(model, texts, req);
863
+ }
864
+
865
+ /**
866
+ *
867
+ * @param {CustomEntityDetectionModel} model
868
+ * @param {TextAlternative[]} texts
869
+ * @param {Request} [req]
870
+ * @returns {Promise<Result>}
871
+ */
872
+ async _queryModelWithTexts (model, texts, req = null) {
861
873
  const altKoef = (1 - this.confidence);
874
+ const altMax = Math.max(0, ...texts.map((t) => t.score));
862
875
 
863
876
  const results = await Promise.all(
864
- texts.map(({ text, score }) => model
877
+ texts.map(({ text, score = 1 }) => model
865
878
  .resolve(this.textFilter(text), req)
866
879
  .then((res) => ({
867
880
  ...res,
@@ -910,6 +923,34 @@ class Ai {
910
923
  };
911
924
  }
912
925
 
926
+ /**
927
+ *
928
+ * @param {string} text
929
+ * @param {string|IStateRequest} langOrReq
930
+ * @returns {Promise<Result>}
931
+ */
932
+ async queryModel (text, langOrReq = this.DEFAULT_PREFIX) {
933
+ let model;
934
+
935
+ if (typeof langOrReq === 'string') {
936
+ model = this._keyworders.has(langOrReq)
937
+ ? this._keyworders.get(langOrReq)
938
+ : this._keyworders.get(this.DEFAULT_PREFIX);
939
+ } else {
940
+ model = this._getModelForRequest(langOrReq);
941
+ }
942
+
943
+ if (!model) {
944
+ return {
945
+ text,
946
+ intents: [],
947
+ entities: []
948
+ };
949
+ }
950
+
951
+ return this._queryModelWithTexts(model, [{ text, score: 1 }]);
952
+ }
953
+
913
954
  /**
914
955
  *
915
956
  * @param {IntentAction[]} aiActions
package/src/AiMatching.js CHANGED
@@ -3,11 +3,12 @@
3
3
  */
4
4
  'use strict';
5
5
 
6
- const { replaceDiacritics } = require('./utils/tokenizer');
6
+ const { replaceDiacritics, tokenize } = require('./utils/tokenizer');
7
7
  const { vars } = require('./utils/stateVariables');
8
8
  const stateData = require('./utils/stateData');
9
9
 
10
10
  /** @typedef {import('handlebars')} Handlebars */
11
+ /** @typedef {import('./Ai').Result} Result */
11
12
 
12
13
  /** @type {Handlebars} */
13
14
  let handlebars;
@@ -66,16 +67,18 @@ const COMPARE = {
66
67
 
67
68
  /**
68
69
  * @typedef {object} Entity
69
- * @param {string} entity
70
- * @param {string} value
71
- * @param {number} score
70
+ * @prop {string} entity
71
+ * @prop {string} value
72
+ * @prop {number} score
73
+ * @prop {number} [start]
74
+ * @prop {number} [end]
72
75
  */
73
76
 
74
77
  /**
75
78
  * @typedef {object} Intent
76
- * @param {string} [intent]
77
- * @param {number} score
78
- * @param {Entity[]} [entities]
79
+ * @prop {string} [intent]
80
+ * @prop {number} score
81
+ * @prop {Entity[]} [entities]
79
82
  */
80
83
 
81
84
  /**
@@ -461,35 +464,35 @@ class AiMatching {
461
464
  }
462
465
 
463
466
  /**
464
- * Calculate a matching score of preprocessed rule against the request
465
467
  *
466
- * @param {AIRequest} req
468
+ * @param {string} text
467
469
  * @param {PreprocessorOutput} rule
468
- * @param {boolean} [stateless]
469
- * @param {Entity[]} [reqEntities]
470
- * @param {boolean} [noEntityThreshold]
470
+ * @param {Result} nlpResult
471
+ * @param {{}} state
471
472
  * @returns {Intent|null}
472
473
  */
473
- match (req, rule, stateless = false, reqEntities = req.entities, noEntityThreshold = false) {
474
- const { regexps, intents, entities } = rule;
474
+ matchText (text, rule, nlpResult, state = {}) {
475
+ return this._match(text, rule, state, nlpResult, true);
476
+ }
475
477
 
476
- const noIntentHandicap = req.intents.length === 0 ? 0 : this.redundantIntentHandicap;
477
- const regexpScore = this._matchRegexp(req, regexps, noIntentHandicap);
478
- const textLength = req.text().trim().length;
478
+ _match (text, rule, useState, nlpResult, stateless = false, noEntityThreshold = false) {
479
+ let state = useState;
479
480
 
480
- let useState;
481
- if (stateless || intents.length === 0) {
482
- useState = Object.entries(stateData(req))
483
- .reduce((o, [k, v]) => {
484
- if (k.startsWith('@')) {
485
- return o;
486
- }
487
- return Object.assign(o, { [k]: v });
488
- }, {});
489
- } else {
490
- useState = stateData(req);
481
+ if (stateless) {
482
+ state = Object.fromEntries(
483
+ Object.entries(state)
484
+ .filter(([k]) => !k.startsWith('@'))
485
+ );
491
486
  }
492
487
 
488
+ const { regexps, intents, entities } = rule;
489
+ const { entities: reqEntities = [], intents: reqIntents = [] } = nlpResult;
490
+ const tokenized = tokenize(text) || text.trim();
491
+
492
+ const noIntentHandicap = reqIntents.length === 0 ? 0 : this.redundantIntentHandicap;
493
+ const regexpScore = this._matchRegexp(text, tokenized, regexps, noIntentHandicap);
494
+ const textLength = text.length;
495
+
493
496
  if (regexpScore !== 0 || (intents.length === 0 && regexps.length === 0)) {
494
497
 
495
498
  if (entities.length === 0) {
@@ -511,7 +514,7 @@ class AiMatching {
511
514
  textLength,
512
515
  entities,
513
516
  reqEntities,
514
- useState,
517
+ state,
515
518
  undefined,
516
519
  undefined,
517
520
  noEntityThreshold
@@ -563,7 +566,7 @@ class AiMatching {
563
566
  };
564
567
  }
565
568
 
566
- if (!req.intents || req.intents.length === 0) {
569
+ if (reqIntents.length === 0) {
567
570
  return null;
568
571
  }
569
572
 
@@ -572,15 +575,15 @@ class AiMatching {
572
575
  intents
573
576
  .reduce((total, wanted) => {
574
577
  let max = total;
575
- for (const requestIntent of req.intents) {
578
+ for (const requestIntent of reqIntents) {
576
579
  const { score, entities: matchedEntities } = this
577
580
  ._intentMatchingScore(
578
581
  textLength,
579
582
  wanted,
580
583
  requestIntent,
581
584
  entities,
582
- req,
583
- useState,
585
+ reqEntities,
586
+ state,
584
587
  noEntityThreshold
585
588
  );
586
589
 
@@ -602,6 +605,27 @@ class AiMatching {
602
605
  return winningIntent;
603
606
  }
604
607
 
608
+ /**
609
+ * Calculate a matching score of preprocessed rule against the request
610
+ *
611
+ * @param {AIRequest} req
612
+ * @param {PreprocessorOutput} rule
613
+ * @param {boolean} [stateless]
614
+ * @param {Entity[]} [reqEntities]
615
+ * @param {boolean} [noEntityThreshold]
616
+ * @returns {Intent|null}
617
+ */
618
+ match (req, rule, stateless = false, reqEntities = req.entities, noEntityThreshold = false) {
619
+ const { intents } = rule;
620
+
621
+ const state = stateData(req);
622
+
623
+ return this._match(req.text(), rule, state, {
624
+ intents: req.intents,
625
+ entities: reqEntities
626
+ }, stateless || intents.length === 0, noEntityThreshold);
627
+ }
628
+
605
629
  _getMultiMatchGain (entitiesScore, matchedCount, fromState = 0) {
606
630
  return (this.multiMatchGain * entitiesScore) ** Math.max(matchedCount - fromState, 0);
607
631
  }
@@ -613,7 +637,7 @@ class AiMatching {
613
637
  * @param {string} wantedIntent
614
638
  * @param {Intent} requestIntent
615
639
  * @param {EntityExpression[]} wantedEntities
616
- * @param {AIRequest} req
640
+ * @param {Entity[]} reqEntities
617
641
  * @param {object} useState
618
642
  * @param {boolean} [noEntityThreshold]
619
643
  * @returns {{score:number,entities:Entity[]}}
@@ -623,7 +647,7 @@ class AiMatching {
623
647
  wantedIntent,
624
648
  requestIntent,
625
649
  wantedEntities,
626
- req,
650
+ reqEntities,
627
651
  useState,
628
652
  noEntityThreshold = false
629
653
  ) {
@@ -631,7 +655,7 @@ class AiMatching {
631
655
  return { score: 0, entities: [] };
632
656
  }
633
657
 
634
- const useEntities = requestIntent.entities || req.entities;
658
+ const useEntities = requestIntent.entities || reqEntities;
635
659
 
636
660
  if (wantedEntities.length === 0) {
637
661
  return {
@@ -651,7 +675,7 @@ class AiMatching {
651
675
  requestIntent.entities
652
676
  ? (x) => Math.atan((x - 0.76) * 40) / Math.atan((1 - 0.76) * 40)
653
677
  : (x) => x,
654
- req.entities,
678
+ reqEntities,
655
679
  noEntityThreshold
656
680
  );
657
681
 
@@ -941,18 +965,20 @@ class AiMatching {
941
965
 
942
966
  /**
943
967
  *
944
- * @param {AIRequest} req
968
+ * @param {string} text
969
+ * @param {string} tokenized
945
970
  * @param {RegexpComparator[]} regexps
946
971
  * @param {number} noIntentHandicap
947
972
  * @returns {number}
948
973
  */
949
- _matchRegexp (req, regexps, noIntentHandicap) {
974
+ _matchRegexp (text, tokenized, regexps, noIntentHandicap) {
950
975
  if (regexps.length === 0) {
951
976
  return 0;
952
977
  }
953
978
 
954
979
  const scores = regexps.map(({ r, t, f }) => {
955
- const m = req.text(t).match(r);
980
+ const txt = t ? tokenized : text;
981
+ const m = txt.match(r);
956
982
 
957
983
  if (!m) {
958
984
  return 0;
@@ -41,12 +41,12 @@ class BotAppSender extends ReturnSender {
41
41
  /**
42
42
  *
43
43
  * @param {SenderOptions} options
44
- * @param {string} userId
44
+ * @param {string} senderId
45
45
  * @param {object} incommingMessage
46
46
  * @param {ChatLogStorage} logger - console like logger
47
47
  */
48
- constructor (options, userId, incommingMessage, logger = null) {
49
- super(options, userId, incommingMessage, logger);
48
+ constructor (options, senderId, incommingMessage, logger = null) {
49
+ super(options, senderId, incommingMessage, logger);
50
50
 
51
51
  this.waits = true;
52
52
 
@@ -98,14 +98,16 @@ class BotAppSender extends ReturnSender {
98
98
  * @param {Buffer} data
99
99
  * @param {string} contentType
100
100
  * @param {string} fileName
101
+ * @param {string} [senderId]
101
102
  * @returns {Promise<UploadResult>}
102
103
  */
103
- async upload (data, contentType, fileName) {
104
+ async upload (data, contentType, fileName, senderId = this._senderId) {
104
105
  const formData = new FormData();
105
106
 
106
107
  const nonce = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36).padEnd(11, '0');
107
108
 
108
109
  formData.append('nonce', nonce);
110
+ formData.append('senderId', senderId || '');
109
111
  formData.append('f0', data, { filename: fileName, contentType });
110
112
 
111
113
  const [token, agent] = await Promise.all([
@@ -162,6 +162,7 @@ const DUMMY_ROUTE = { id: 0, path: null, resolvers: [] };
162
162
  * @prop {Block[]} [blocks]
163
163
  * @prop {NestedLinksMapFactory} [nestedLinksMapFactory]
164
164
  * @prop {object} [BuildRouter]
165
+ * @prop {Ai} [ai]
165
166
  * @prop {string|number} [resolverId] - only for text messages with random characters
166
167
  */
167
168
 
@@ -928,6 +929,7 @@ class BuildRouter extends Router {
928
929
  return resolvers.map((resolver, i) => {
929
930
 
930
931
  const context = {
932
+ ai: Ai.ai,
931
933
  ...this._resolvedContext,
932
934
  isLastIndex: lastIndex === i && !buildInfo.expectedToAddResolver,
933
935
  isLastMessage: lastMessageIndex === i && !buildInfo.notLastMessage,
package/src/LLM.js CHANGED
@@ -3,16 +3,72 @@
3
3
  */
4
4
  'use strict';
5
5
 
6
+ const { getSetState } = require('./utils/getUpdate');
6
7
  const { PHONE_REGEX, EMAIL_REGEX } = require('./systemEntities/regexps');
8
+ const getCondition = require('./utils/getCondition');
9
+ const stateData = require('./utils/stateData');
10
+ const Ai = require('./Ai');
11
+ // const getCondition = require('./utils/getCondition');
7
12
 
8
13
  /** @typedef {import('./Responder')} Responder */
14
+ /** @typedef {import('./AiMatching').PreprocessorOutput} PreprocessorOutput */
15
+ /** @typedef {import('./Request')} Request */
9
16
  /** @typedef {import('./Responder').Persona} Persona */
10
17
  /** @typedef {import('./Router').BaseConfiguration} BaseConfiguration */
11
18
  /** @typedef {import('./LLMSession').LLMMessage<any>} LLMMessage */
12
19
  /** @typedef {import('./LLMSession').ToolCall} ToolCall */
13
20
  /** @typedef {import('./LLMSession').LLMRole} LLMRole */
21
+ /** @typedef {import('./LLMSession').FilterScope} FilterScope */
14
22
  /** @typedef {import('./LLMSession')} LLMSession */
15
23
  /** @typedef {import('./transcript/transcriptFromHistory').Transcript} Transcript */
24
+ /** @typedef {import('./utils/getCondition').ConditionDefinition} ConditionDefinition */
25
+ /** @typedef {import('./utils/getCondition').ConditionContext} ConditionContext */
26
+ /** @typedef {import('./utils/stateData').IStateRequest} IStateRequest */
27
+
28
+ /** @typedef {string|'_DISCARD'} EvaluationRuleAction */
29
+
30
+ /**
31
+ * @typedef {object} EvaluationRuleData
32
+ * @prop {EvaluationRuleAction} [action]
33
+ * @prop {object} [setState]
34
+ */
35
+
36
+ /**
37
+ * @typedef {object} RuleDefinitionData
38
+ * @prop {string[]} aiTags
39
+ * @prop {EvaluationRuleAction} [targetRouteId]
40
+ */
41
+
42
+ /**
43
+ * @typedef {EvaluationRuleData & RuleDefinitionData & ConditionDefinition} EvaluationRule
44
+ */
45
+
46
+ /**
47
+ * @typedef {object} PrepocessedRuleData
48
+ * @prop {Function} condition
49
+ * @prop {PreprocessorOutput} rule
50
+ */
51
+
52
+ /**
53
+ * @typedef {EvaluationRuleData & PrepocessedRuleData} PreprocessedRule
54
+ */
55
+
56
+ /**
57
+ * @typedef {object} RuleScore
58
+ * @prop {number} score
59
+ */
60
+
61
+ /**
62
+ * @typedef {RuleScore & PreprocessedRule} RuleWithScore
63
+ */
64
+
65
+ /**
66
+ * @typedef {object} EvaluationResult
67
+ * @prop {string} action
68
+ * @prop {boolean} discard
69
+ * @prop {RuleWithScore[]} results
70
+ * @prop {object} setState
71
+ */
16
72
 
17
73
  /**
18
74
  * @callback LLMChatProviderPrompt
@@ -82,6 +138,13 @@ class LLM {
82
138
 
83
139
  static GPT_FLAG = 'gpt';
84
140
 
141
+ /** @type {FilterScope} */
142
+ static FILTER_SCOPE_CONVERSATION = 'conversation';
143
+
144
+ static EVALUATION_ACTIONS = {
145
+ DISCARD: '_DISCARD'
146
+ };
147
+
85
148
  /** @type {AnonymizeRegexp[]} */
86
149
  static anonymizeRegexps = [
87
150
  { replacement: '@PHONE', regex: new RegExp(PHONE_REGEX.source, 'g') },
@@ -91,8 +154,9 @@ class LLM {
91
154
  /**
92
155
  *
93
156
  * @param {LLMConfiguration} configuration
157
+ * @param {Ai} ai
94
158
  */
95
- constructor (configuration) {
159
+ constructor (configuration, ai) {
96
160
  const { provider, ...rest } = configuration;
97
161
 
98
162
  this._configuration = {
@@ -107,6 +171,25 @@ class LLM {
107
171
 
108
172
  /** @type {LLMChatProvider} */
109
173
  this._provider = provider;
174
+
175
+ this._ai = ai;
176
+
177
+ /** @type {LLMMessage} */
178
+ this._lastResult = null;
179
+ }
180
+
181
+ /**
182
+ * @returns {Ai}
183
+ */
184
+ get ai () {
185
+ return this._ai;
186
+ }
187
+
188
+ /**
189
+ * @returns {LLMMessage}
190
+ */
191
+ get lastResult () {
192
+ return this._lastResult;
110
193
  }
111
194
 
112
195
  /**
@@ -148,7 +231,7 @@ class LLM {
148
231
  ...options
149
232
  };
150
233
 
151
- const prompt = session.toArray();
234
+ const prompt = session.toArray(true);
152
235
  const result = await this._provider.requestChat(prompt, opts);
153
236
  this._logPrompt(prompt, result);
154
237
  return result;
@@ -160,6 +243,7 @@ class LLM {
160
243
  * @param {LLMMessage} result
161
244
  */
162
245
  _logPrompt (prompt, result) {
246
+ this._lastResult = result;
163
247
  this._configuration.logger.logPrompt({
164
248
  prompt, result
165
249
  });
@@ -188,6 +272,103 @@ class LLM {
188
272
  }));
189
273
  }
190
274
 
275
+ /**
276
+ *
277
+ * @param {EvaluationRule[]} rules
278
+ * @param {ConditionContext} [context]
279
+ * @returns {PreprocessedRule[]}
280
+ */
281
+ static preprocessEvaluationRules (rules, context = {}) {
282
+ const {
283
+ linksMap = new Map(),
284
+ ai = Ai.ai
285
+ } = context;
286
+
287
+ return rules.map((evalRule) => {
288
+ const { aiTags, targetRouteId, ...rest } = evalRule;
289
+
290
+ const condition = getCondition(rest, context);
291
+ const rule = ai.matcher.preprocessRule(aiTags);
292
+
293
+ let { action = null } = evalRule;
294
+
295
+ if (!action && targetRouteId && linksMap.has(targetRouteId)) {
296
+ action = linksMap.get(targetRouteId);
297
+ }
298
+
299
+ return {
300
+ ...rest,
301
+ condition,
302
+ rule
303
+ };
304
+ });
305
+ }
306
+
307
+ /**
308
+ * Returns all actions, which has been recognized
309
+ * with higher score than threshold, but
310
+ *
311
+ * - _DISCARD action discards any other rules (will return all relevant _DISCARD actions)
312
+ * - only the TOP ranked "interaction" action will be returned
313
+ * - actions will come in THE SAME order, so the "setState" will be applied in the same order
314
+ *
315
+ *
316
+ * @param {LLMMessage|string} result
317
+ * @param {PreprocessedRule[]} rules
318
+ * @param {IStateRequest} req
319
+ * @param {Responder} res
320
+ * @returns {Promise<EvaluationResult>}
321
+ */
322
+ async evaluateResultWithRules (result, rules, req, res) {
323
+ const text = typeof result === 'string' ? result : result.content;
324
+ const nlpResult = await this._ai.queryModel(text, req);
325
+ const state = stateData(req, res);
326
+
327
+ let topRankedAction = null;
328
+ let topActionScore = 0;
329
+ let discard = false;
330
+ const setState = {};
331
+
332
+ const sAct = Object.values(LLM.EVALUATION_ACTIONS);
333
+
334
+ const results = rules
335
+ .filter((rule) => rule.condition(req, res))
336
+ .map((rule) => {
337
+ const matched = this._ai.matcher
338
+ .matchText(text, rule.rule, nlpResult, state);
339
+
340
+ if (!matched || matched.score < this._ai.threshold) {
341
+ return null;
342
+ }
343
+
344
+ if (rule.action === LLM.EVALUATION_ACTIONS.DISCARD) {
345
+ discard = true;
346
+ } else if (rule.action && topActionScore < matched.score) {
347
+ topRankedAction = rule.action;
348
+ topActionScore = matched.score;
349
+ }
350
+
351
+ return {
352
+ ...rule,
353
+ score: matched.score
354
+ };
355
+ })
356
+ .filter((rule) => rule !== null
357
+ && (!discard || rule.action === LLM.EVALUATION_ACTIONS.DISCARD)
358
+ && (!rule.action || rule.action === topRankedAction || sAct.includes(rule.action)));
359
+
360
+ results.forEach((rule) => {
361
+ Object.assign(setState, getSetState(rule.setState, req, res, setState));
362
+ });
363
+
364
+ return {
365
+ setState,
366
+ results,
367
+ discard,
368
+ action: discard ? null : topRankedAction
369
+ };
370
+ }
371
+
191
372
  }
192
373
 
193
374
  module.exports = LLM;