wingbot 3.69.7 → 3.69.8

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/jsconfig.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "module": "commonjs",
4
- "lib": ["es6"],
5
- "target": "es2019",
4
+ "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"],
5
+ "target": "ESNext",
6
6
  "allowSyntheticDefaultImports": true,
7
7
  "checkJs": true,
8
8
  "resolveJsonModule": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.69.7",
3
+ "version": "3.69.8",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -61,7 +61,7 @@
61
61
  "graphql": "^16.8.1",
62
62
  "jsonwebtoken": "^9.0.2",
63
63
  "node-fetch": "^2.6.7",
64
- "path-to-regexp": "^6.2.1",
64
+ "path-to-regexp": "^6.3.0",
65
65
  "uuid": "^9.0.1",
66
66
  "webalize": "^0.1.0"
67
67
  },
@@ -77,6 +77,10 @@ const PLUGIN_RESOLVER_NAME = 'botbuild.customCode';
77
77
  * @typedef {Map<string|number,string>} LinksMap
78
78
  */
79
79
 
80
+ /**
81
+ * @typedef {Map<string|number, Block>} BlockMap
82
+ */
83
+
80
84
  /** @type {TransformedRoute} */
81
85
  const DUMMY_ROUTE = { id: 0, path: null, resolvers: [] };
82
86
 
@@ -136,6 +140,7 @@ const DUMMY_ROUTE = { id: 0, path: null, resolvers: [] };
136
140
 
137
141
  /**
138
142
  * @typedef {object} BotContextExtention
143
+ * @prop {BlockMap} [nestedBlocksByStaticId]
139
144
  * @prop {LinksMap} [linksMap]
140
145
  * @prop {boolean} [isLastIndex]
141
146
  * @prop {boolean} [isLastMessage]
@@ -565,11 +570,10 @@ class BuildRouter extends Router {
565
570
  ...this._resolvedContext, blockName, blockType, isRoot, staticBlockId, BuildRouter
566
571
  };
567
572
 
568
- this._linksMap = this._createLinksMap(block);
569
-
570
- this._setExpectedFromResponderRoutes(block.routes);
573
+ const [linksMap, nestedBlocksByStaticId] = this._createLinksMap(block);
574
+ this._linksMap = linksMap;
571
575
 
572
- this._buildRoutes(block.routes);
576
+ this._buildRoutes(block.routes, nestedBlocksByStaticId);
573
577
 
574
578
  this._configTs = setConfigTimestamp;
575
579
 
@@ -577,74 +581,78 @@ class BuildRouter extends Router {
577
581
  this.emit('rebuild');
578
582
  }
579
583
 
580
- _setExpectedFromResponderRoutes (routes) {
581
- const set = new Set();
582
-
583
- routes.forEach((route) => {
584
- if (!route.isResponder) {
585
- return;
586
- }
587
-
588
- // create the pseudopath ant set to set to corresponding route
589
- const referredRoutePath = this._linksMap.get(route.respondsToRouteId);
590
-
591
- if (!referredRoutePath) {
592
- return;
593
- }
594
-
595
- const expectedPath = `${referredRoutePath}_responder`
596
- .replace(/^\//, '');
597
-
598
- Object.assign(route, { path: expectedPath });
599
-
600
- // set expectedPath to referredRoute
601
-
602
- if (set.has(route.respondsToRouteId)) {
603
- return;
604
- }
605
- set.add(route.respondsToRouteId);
606
-
607
- const referredRoute = routes.find((r) => r.id === route.respondsToRouteId);
608
-
609
- Object.assign(referredRoute, { expectedPath });
610
- });
611
- }
612
-
613
584
  /**
614
585
  *
615
586
  * @param {Block} block
616
- * @returns {LinksMap}
587
+ * @returns {[LinksMap, BlockMap]}
617
588
  */
618
589
  _createLinksMap (block) {
590
+ const { linksMap: prevLinksMap, blocks = [] } = this._resolvedContext;
591
+
619
592
  /** @type {LinksMap} */
620
593
  const linksMap = new Map();
621
594
 
595
+ if (prevLinksMap) {
596
+ for (const [from, to] of prevLinksMap.entries()) {
597
+ linksMap.set(from, `../${to}`); // this._joinPaths('..', to)
598
+ }
599
+ }
600
+
601
+ const expectedFromResponders = new Set();
602
+
603
+ const blocksById = new Map();
604
+
622
605
  block.routes
623
- .filter((route) => !route.isResponder)
624
- .forEach((route) => linksMap.set(route.id, route.path));
606
+ .forEach((route) => {
607
+ if (!route.isResponder) {
608
+ linksMap.set(route.id, route.path);
609
+ }
610
+ blocksById.set(route.id, route);
611
+ });
625
612
 
626
- const { linksMap: prevLinksMap } = this._resolvedContext;
613
+ let { nestedBlocksByStaticId } = this._resolvedContext;
614
+ if (!nestedBlocksByStaticId) {
615
+ nestedBlocksByStaticId = new Map();
627
616
 
628
- if (prevLinksMap) {
629
- for (const [from, to] of prevLinksMap.entries()) {
630
- if (!linksMap.has(from)) {
631
- linksMap.set(from, this._joinPaths('..', to));
617
+ blocks.forEach((b) => {
618
+ if (b.staticBlockId && !b.disabled) {
619
+ nestedBlocksByStaticId.set(b.staticBlockId, b);
632
620
  }
633
- }
621
+ });
622
+
623
+ Object.assign(this._resolvedContext, { nestedBlocksByStaticId });
634
624
  }
635
625
 
636
626
  block.routes.forEach((route) => {
637
- const enabledNestedBlock = this._getBlockById(this._getIncludedBlockId(route));
638
- if (!enabledNestedBlock) {
639
- return;
627
+ const enabledNestedBlock = nestedBlocksByStaticId.get(this._getIncludedBlockId(route));
628
+ if (enabledNestedBlock) {
629
+ const routeConfig = this._getRouteConfig(route);
630
+ if (this._enabledByRouteConfig(routeConfig)) {
631
+ this._findEntryPointsInResolver(linksMap, enabledNestedBlock, route);
632
+ }
640
633
  }
641
- const routeConfig = this._getRouteConfig(route);
642
- if (this._enabledByRouteConfig(routeConfig)) {
643
- this._findEntryPointsInResolver(linksMap, enabledNestedBlock, route);
634
+
635
+ if (route.isResponder) {
636
+ // create the pseudopath ant set to set to corresponding route
637
+ const referredRoutePath = linksMap.get(route.respondsToRouteId);
638
+
639
+ if (referredRoutePath) {
640
+ const expectedPath = `${referredRoutePath}_responder`
641
+ .replace(/^\//, '');
642
+
643
+ Object.assign(route, { path: expectedPath });
644
+
645
+ if (!expectedFromResponders.has(route.respondsToRouteId)) {
646
+ expectedFromResponders.add(route.respondsToRouteId);
647
+
648
+ const referredRoute = blocksById.get(route.respondsToRouteId);
649
+ Object.assign(referredRoute, { expectedPath });
650
+ }
651
+ }
644
652
  }
645
653
  });
646
654
 
647
- return linksMap;
655
+ return [linksMap, nestedBlocksByStaticId];
648
656
  }
649
657
 
650
658
  /**
@@ -682,24 +690,6 @@ class BuildRouter extends Router {
682
690
  : null;
683
691
  }
684
692
 
685
- /**
686
- *
687
- * @param {string} staticBlockId
688
- * @returns {Block|null}
689
- */
690
- _getBlockById (staticBlockId) {
691
- if (!staticBlockId) {
692
- return null;
693
- }
694
- const nestedBlock = (this._resolvedContext.blocks || [])
695
- .find((b) => b.staticBlockId === staticBlockId);
696
-
697
- if (!nestedBlock || nestedBlock.disabled) {
698
- return null;
699
- }
700
- return nestedBlock;
701
- }
702
-
703
693
  /**
704
694
  *
705
695
  * @param {TransformedRoute} route
@@ -811,8 +801,9 @@ class BuildRouter extends Router {
811
801
  /**
812
802
  *
813
803
  * @param {TransformedRoute[]} routes
804
+ * @param {BlockMap} nestedBlocksByStaticId
814
805
  */
815
- _buildRoutes (routes) {
806
+ _buildRoutes (routes, nestedBlocksByStaticId) {
816
807
  routes.forEach((route, i) => {
817
808
  const routeConfig = this._getRouteConfig(route);
818
809
 
@@ -821,7 +812,7 @@ class BuildRouter extends Router {
821
812
  }
822
813
 
823
814
  const includedBlockId = this._getIncludedBlockId(route);
824
- const nestedBlock = this._getBlockById(includedBlockId);
815
+ const nestedBlock = nestedBlocksByStaticId.get(includedBlockId);
825
816
 
826
817
  if (includedBlockId && (!nestedBlock || !this._enabledByRouteConfig(routeConfig))) {
827
818
  return;
@@ -882,9 +873,10 @@ class BuildRouter extends Router {
882
873
  * @param {Resolver[]} resolvers
883
874
  * @param {TransformedRoute} [route]
884
875
  * @param {BuildInfo} [buildInfo]
876
+ * @param {BlockMap} [nestedBlocksByStaticId=null]
885
877
  * @returns {Middleware<S,C>[]}
886
878
  */
887
- buildResolvers (resolvers, route = DUMMY_ROUTE, buildInfo = {}) {
879
+ buildResolvers (resolvers, route = DUMMY_ROUTE, buildInfo = {}, nestedBlocksByStaticId = null) {
888
880
  const {
889
881
  path: ctxPath, isFallback, isResponder, expectedPath, id
890
882
  } = route;
@@ -911,7 +903,8 @@ class BuildRouter extends Router {
911
903
  expectedPath,
912
904
  routeId: id,
913
905
  configuration,
914
- resolverId: resolver.id
906
+ resolverId: resolver.id,
907
+ nestedBlocksByStaticId
915
908
  };
916
909
 
917
910
  const resFn = this._resolverFactory(resolver, context, buildInfo);
package/src/ChatGpt.js CHANGED
@@ -6,11 +6,14 @@
6
6
  const nodeFetch = require('node-fetch').default;
7
7
  const util = require('util');
8
8
  const { PHONE_REGEX, EMAIL_REGEX } = require('./systemEntities/regexps');
9
+ const LLM = require('./LLM');
9
10
 
10
11
  /** @typedef {import('node-fetch').default} Fetch */
11
12
  /** @typedef {import('./Request')} Request */
12
13
  /** @typedef {import('./Responder')} Responder */
13
14
  /** @typedef {import('./Responder').QuickReply} QuickReply */
15
+ /** @typedef {import('./LLM').LLMMessage} LLMMessage */
16
+ /** @typedef {import('./LLM').LLMProviderOptions} LLMProviderOptions */
14
17
 
15
18
  /**
16
19
  * @typedef {object} Transcript
@@ -59,7 +62,7 @@ const { PHONE_REGEX, EMAIL_REGEX } = require('./systemEntities/regexps');
59
62
 
60
63
  /**
61
64
  * @typedef {object} ChatGPTChoice
62
- * @prop {'stop'|'length'|'function_call'|'content_filter'|null} finish_reason
65
+ * @prop {'stop'|'length'|'tool_calls'|'content_filter'|null} finish_reason
63
66
  * @prop {number} index
64
67
  * @prop {Message} message
65
68
  */
@@ -237,16 +240,31 @@ class ChatGpt {
237
240
  }));
238
241
  }
239
242
 
243
+ /**
244
+ * @param {LLMMessage[]} prompt
245
+ * @param {LLMProviderOptions} [options]
246
+ * @returns {Promise<LLMMessage>}
247
+ */
248
+ async requestChat (prompt, options) {
249
+
250
+ const choice = await this._request(prompt, options);
251
+
252
+ const { finish_reason: finishReason, message } = choice;
253
+
254
+ return {
255
+ finishReason,
256
+ ...message
257
+ };
258
+ }
259
+
240
260
  /**
241
261
  *
242
- * @param {string} content
243
- * @param {string} [system]
244
- * @param {Transcript[]} [transcript]
245
- * @param {RequestOptions} [requestOptions]
246
- * @param {string|Request} [user]
262
+ * @param {LLMMessage[]} chat
263
+ * @param {RequestOptions} requestOptions
264
+ * @param {string} user
247
265
  * @returns {Promise<ChatGPTChoice>}
248
266
  */
249
- async request (content, system = null, transcript = [], requestOptions = {}, user = null) {
267
+ async _request (chat, requestOptions, user = null) {
250
268
  const {
251
269
  requestTokens,
252
270
  tokensLimit,
@@ -259,50 +277,51 @@ class ChatGpt {
259
277
  ...requestOptions
260
278
  };
261
279
 
280
+ let messages = chat;
262
281
  const maxTokens = Math.min(requestTokens, tokensLimit);
263
282
 
264
283
  let body;
265
-
266
284
  try {
285
+ let lastUserIndex = 0;
286
+ let totalTokens = messages
287
+ .reduce((total, m, i) => {
288
+ if (m.role === LLM.ROLE_USER) {
289
+ lastUserIndex = i;
290
+ }
291
+ return (m.content ? 0 : m.content.length) + total;
292
+ }, 0);
293
+
294
+ if (totalTokens > tokensLimit) {
295
+ messages = messages.filter((m, i) => {
296
+ if (m.role === LLM.ROLE_SYSTEM
297
+ || i >= lastUserIndex
298
+ || totalTokens <= tokensLimit
299
+ || !m.content) {
300
+
301
+ return true;
302
+ }
303
+ totalTokens -= m.content.length;
304
+ return false;
305
+ });
306
+ }
307
+
267
308
  body = {
268
309
  model,
269
310
  frequency_penalty: 0,
270
311
  presence_penalty: presencePenalty,
271
312
  max_tokens: maxTokens,
272
313
  temperature,
273
- ...(functions.length ? { functions } : {})
314
+ messages
274
315
  };
275
316
 
276
- if (typeof user === 'string') {
317
+ if (user) {
277
318
  Object.assign(body, { user });
278
- } else if (user) {
279
- Object.assign(body, { user: `${user.pageId}|${user.senderId}` });
280
- } else if (this._defaultUser) {
281
- Object.assign(body, { user: this._defaultUser });
282
319
  }
283
320
 
284
- let total = (system ? system.length : 0)
285
- + maxTokens
286
- + content.length;
287
-
288
- const ts = transcript.slice();
289
-
290
- for (let i = ts.length - 1; i >= 0; i--) {
291
- total += ts[i].text.length;
292
- if (total > tokensLimit) {
293
- ts.splice(i, 1);
294
- }
321
+ if (functions.length) {
322
+ Object.assign(body, { functions });
295
323
  }
296
324
 
297
- /** @type {Message[]} */
298
- const messages = [
299
- ...(system ? [{ role: 'system', content: system }] : []),
300
- ...ts.map((t) => ({ role: t.fromBot ? 'assistant' : 'user', content: t.text })),
301
- { role: 'user', content }
302
- ];
303
-
304
- Object.assign(body, { messages });
305
-
306
325
  const apiUrl = `${this._openAiEndpoint}/chat/completions${this._apiKey ? '?api-version=2023-03-15-preview' : ''}`;
307
326
 
308
327
  this._log('#GPT request', body);
@@ -342,6 +361,37 @@ class ChatGpt {
342
361
  }
343
362
  }
344
363
 
364
+ /**
365
+ *
366
+ * @deprecated
367
+ * @param {string} content
368
+ * @param {string} [system]
369
+ * @param {Transcript[]} [transcript]
370
+ * @param {RequestOptions} [requestOptions]
371
+ * @param {string|Request} [user]
372
+ * @returns {Promise<ChatGPTChoice>}
373
+ */
374
+ async request (content, system = null, transcript = [], requestOptions = {}, user = null) {
375
+
376
+ /** @type {Message[]} */
377
+ const messages = [
378
+ ...(system ? [{ role: 'system', content: system }] : []),
379
+ ...transcript.map((t) => ({ role: t.fromBot ? 'assistant' : 'user', content: t.text })),
380
+ { role: 'user', content }
381
+ ];
382
+
383
+ let useUser;
384
+ if (typeof user === 'string') {
385
+ useUser = user;
386
+ } else if (user) {
387
+ useUser = `${user.pageId}|${user.senderId}`;
388
+ } else if (this._defaultUser) {
389
+ useUser = this._defaultUser;
390
+ }
391
+
392
+ return this._request(messages, requestOptions, useUser);
393
+ }
394
+
345
395
  /**
346
396
  *
347
397
  * @param {ChatGPTChoice} choice
package/src/LLM.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * @author David Menger
3
+ */
4
+ 'use strict';
5
+
6
+ const { PHONE_REGEX, EMAIL_REGEX } = require('./systemEntities/regexps');
7
+
8
+ /** @typedef {import('./Responder')} Responder */
9
+ /** @typedef {import('./Responder').Persona} Persona */
10
+ /** @typedef {import('./Router').BaseConfiguration} BaseConfiguration */
11
+ /** @typedef {import('./LLMSession').LLMMessage} LLMMessage */
12
+ /** @typedef {import('./LLMSession').LLMRole} LLMRole */
13
+ /** @typedef {import('./LLMSession')} LLMSession */
14
+ /** @typedef {import('./transcript/transcriptFromHistory').Transcript} Transcript */
15
+
16
+ /**
17
+ * @callback LLMChatProviderPrompt
18
+ * @param {LLMMessage[]} prompt
19
+ * @param {LLMProviderOptions} [options]
20
+ * @returns {Promise<LLMMessage>}
21
+ */
22
+
23
+ /**
24
+ * @typedef {object} LLMProviderOptions
25
+ * @prop {string} [model]
26
+ */
27
+
28
+ /**
29
+ * @typedef {object} LLMChatProvider
30
+ * @prop {LLMChatProviderPrompt} requestChat
31
+ */
32
+
33
+ /** @typedef {import('node-fetch').default} Fetch */
34
+
35
+ /**
36
+ * @typedef {object} LLMConfiguration
37
+ * @prop {LLMChatProvider} provider
38
+ * @prop {string} [model]
39
+ * @prop {number} [transcriptLength=-5]
40
+ * @prop {'gpt'|string} [transcriptFlag]
41
+ * @prop {boolean} [transcriptAnonymize]
42
+ * @prop {Persona|string|null} [persona]
43
+ */
44
+
45
+ /**
46
+ * @typedef {object} AnonymizeRegexp
47
+ * @prop {string} [replacement]
48
+ * @prop {RegExp} regex
49
+ */
50
+
51
+ /**
52
+ * @class LLM
53
+ */
54
+ class LLM {
55
+
56
+ /** @type {LLMRole} */
57
+ static ROLE_USER = 'user';
58
+
59
+ /** @type {LLMRole} */
60
+ static ROLE_ASSISTANT = 'assistant';
61
+
62
+ /** @type {LLMRole} */
63
+ static ROLE_SYSTEM = 'system';
64
+
65
+ static GPT_FLAG = 'gpt';
66
+
67
+ /** @type {AnonymizeRegexp[]} */
68
+ static anonymizeRegexps = [
69
+ { replacement: '@PHONE', regex: new RegExp(PHONE_REGEX.source, 'g') },
70
+ { replacement: '@EMAIL', regex: new RegExp(EMAIL_REGEX.source, 'g') }
71
+ ];
72
+
73
+ /**
74
+ *
75
+ * @param {LLMConfiguration} configuration
76
+ */
77
+ constructor (configuration) {
78
+ const { provider, ...rest } = configuration;
79
+
80
+ this._configuration = {
81
+ transcriptFlag: 'gpt',
82
+ transcriptLength: 5,
83
+ provider: null,
84
+ ...rest
85
+ };
86
+
87
+ /** @type {LLMChatProvider} */
88
+ this._provider = provider;
89
+ }
90
+
91
+ /**
92
+ * @returns {Omit<LLMConfiguration, 'provider'>}
93
+ */
94
+ get configuration () {
95
+ return this._configuration;
96
+ }
97
+
98
+ /**
99
+ *
100
+ * @param {Transcript[]} chat
101
+ * @param {boolean} [transcriptAnonymize]
102
+ * @returns {LLMMessage[]}
103
+ */
104
+ static anonymizeTranscript (chat, transcriptAnonymize) {
105
+ return chat.map((c) => ({
106
+ role: c.fromBot ? LLM.ROLE_ASSISTANT : LLM.ROLE_USER,
107
+ content: transcriptAnonymize
108
+ ? LLM.anonymizeRegexps
109
+ .reduce((text, { replacement, regex }) => {
110
+ const replaced = text.replace(regex, replacement);
111
+ return replaced;
112
+ }, c.text)
113
+ : c.text
114
+ }));
115
+ }
116
+
117
+ /**
118
+ *
119
+ * @param {LLMSession} session
120
+ * @param {LLMProviderOptions} [options={}]
121
+ * @returns {Promise<LLMMessage>}
122
+ */
123
+ async generate (session, options = {}) {
124
+ /** @type {LLMProviderOptions} */
125
+ const opts = {
126
+ model: this._configuration.model,
127
+ ...options
128
+ };
129
+
130
+ const prompt = session.toArray();
131
+ const result = await this._provider.requestChat(prompt, opts);
132
+
133
+ return result;
134
+ }
135
+
136
+ /**
137
+ *
138
+ * @param {LLMMessage} result
139
+ * @returns {LLMMessage[]}
140
+ */
141
+ static toMessages (result) {
142
+ let filtered = result.content
143
+ .replace(/\n\n+/g, '\n')
144
+ .split(/\n+(?!-)/g)
145
+ .map((t) => t.trim())
146
+ .filter((t) => !!t);
147
+
148
+ if (result.finishReason === 'length' && filtered.length <= 0) {
149
+ filtered = filtered.slice(0, filtered.length - 1);
150
+ }
151
+
152
+ return filtered.map((content) => ({
153
+ content,
154
+ role: result.role
155
+ }));
156
+ }
157
+
158
+ }
159
+
160
+ module.exports = LLM;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @author David Menger
3
+ */
4
+ 'use strict';
5
+
6
+ const LLM = require('./LLM');
7
+
8
+ /** @typedef {import('./LLM').LLMChatProvider} LLMChatProvider */
9
+ /** @typedef {import('./LLM').LLMMessage} LLMMessage */
10
+ /** @typedef {import('./LLM').LLMProviderOptions} LLMProviderOptions */
11
+
12
+ /**
13
+ * @class LLMMockProvider
14
+ * @implements {LLMChatProvider}
15
+ */
16
+ class LLMMockProvider {
17
+
18
+ static DEFAULT_MODEL = 'mockmodel';
19
+
20
+ constructor () {
21
+ this._index = 0;
22
+ this._sequence = [
23
+ 'lorem',
24
+ 'ipsum',
25
+ 'dolor',
26
+ 'sit',
27
+ 'amet'
28
+ ];
29
+ }
30
+
31
+ /**
32
+ * @param {LLMMessage[]} prompt
33
+ * @param {LLMProviderOptions} [options]
34
+ * @returns {Promise<LLMMessage>}
35
+ */
36
+ // eslint-disable-next-line no-unused-vars
37
+ async requestChat (prompt, options) {
38
+ if (prompt.length === 0) {
39
+ throw new Error('Empty prompt');
40
+ }
41
+ const stats = prompt.reduce((o, m) => Object.assign(o, {
42
+ [m.role]: (o[m.role] || 0) + 1
43
+ }), { system: 0, assistant: 0, user: 0 });
44
+
45
+ const statsText = JSON.stringify(stats)
46
+ .replace(/"/g, '');
47
+
48
+ const message = this._sequence[this._index];
49
+ this._index = (this._index + 1) % this._sequence.length;
50
+
51
+ return {
52
+ role: LLM.ROLE_ASSISTANT,
53
+ finishReason: 'length',
54
+ content: `${statsText} > ${LLMMockProvider.DEFAULT_MODEL}: ${message}`
55
+ };
56
+ }
57
+
58
+ }
59
+
60
+ module.exports = LLMMockProvider;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * @author David Menger
3
+ */
4
+ 'use strict';
5
+
6
+ const LLM = require('./LLM');
7
+
8
+ /** @typedef {import('./Responder').QuickReply} QuickReply */
9
+ /** @typedef {'user'|'assistant'} LLMChatRole */
10
+ /** @typedef {'system'} LLMSystemRole */
11
+ /** @typedef {LLMChatRole|LLMSystemRole|string} LLMRole */
12
+ /** @typedef {import('./LLM').LLMProviderOptions} LLMProviderOptions */
13
+
14
+ /** @typedef {'stop'|'length'|'tool_calls'|'content_filter'} LLMFinishReason */
15
+
16
+ /**
17
+ * @template {LLMRole} [R=LLMRole]
18
+ * @typedef {object} LLMMessage
19
+ * @prop {R} role
20
+ * @prop {string} content
21
+ * @prop {LLMFinishReason} [finishReason]
22
+ */
23
+
24
+ /**
25
+ * @callback SendCallback
26
+ * @param {LLMMessage[]} messages
27
+ * @param {QuickReply[]} quickReplies
28
+ */
29
+
30
+ /**
31
+ * @class LLMSession
32
+ */
33
+ class LLMSession {
34
+
35
+ /**
36
+ *
37
+ * @param {LLM} llm
38
+ * @param {LLMMessage[]} [chat]
39
+ * @param {SendCallback} [onSend]
40
+ */
41
+ constructor (llm, chat = [], onSend = () => {}) {
42
+ this._llm = llm;
43
+
44
+ this._onSend = onSend;
45
+
46
+ /** @type {LLMMessage[]} */
47
+ this._chat = chat;
48
+
49
+ this._generatedIndex = null;
50
+
51
+ this._sort();
52
+ }
53
+
54
+ _sort (what = this._chat) {
55
+ what.sort((a, z) => {
56
+ if (a.role === z.role
57
+ || (a.role !== LLM.ROLE_SYSTEM && z.role !== LLM.ROLE_SYSTEM)) {
58
+ return 0;
59
+ }
60
+ return a.role === LLM.ROLE_SYSTEM ? -1 : 1;
61
+ });
62
+ return what;
63
+ }
64
+
65
+ _mergeSystem () {
66
+ const sysMessages = [];
67
+
68
+ const otherMessages = this._chat.filter((message) => {
69
+ if (message.role !== LLM.ROLE_SYSTEM) {
70
+ return true;
71
+ }
72
+ sysMessages.push(message);
73
+ return false;
74
+ });
75
+
76
+ if (sysMessages.length === 0) {
77
+ return otherMessages;
78
+ }
79
+
80
+ const promptRegex = /\$\{prompt\(\)\}/g;
81
+
82
+ const content = sysMessages.reduce((reduced, current, i) => {
83
+ if (i === 0) {
84
+ return current.content;
85
+ }
86
+ if (!reduced.match(promptRegex)) {
87
+ return `${reduced}\n\n${current.content}`;
88
+ }
89
+ return reduced.replace(promptRegex, current.content).trim();
90
+ }, '')
91
+ .replace(promptRegex, '')
92
+ .trim();
93
+
94
+ return [
95
+ {
96
+ role: LLM.ROLE_SYSTEM, content
97
+ },
98
+ ...otherMessages
99
+ ];
100
+ }
101
+
102
+ /**
103
+ * @returns {LLMMessage[]}
104
+ */
105
+ toArray () {
106
+ return this._mergeSystem();
107
+ }
108
+
109
+ /**
110
+ *
111
+ * @param {LLMMessage} msg
112
+ * @returns {string}
113
+ */
114
+ _msgPrefix (msg) {
115
+ switch (msg.role) {
116
+ case LLM.ROLE_SYSTEM:
117
+ return '-';
118
+ case LLM.ROLE_ASSISTANT:
119
+ return msg.content ? '<' : '#';
120
+ case LLM.ROLE_USER:
121
+ return '>';
122
+ default:
123
+ return '*';
124
+ }
125
+ }
126
+
127
+ /**
128
+ *
129
+ * @param {LLMMessage[]} [messages]
130
+ * @returns {string}
131
+ */
132
+ toString (messages = this._chat) {
133
+ if (messages.length === 0) {
134
+ return '[<empty>]';
135
+ }
136
+ return messages.map((m) => {
137
+ switch (m.role) {
138
+ case LLM.ROLE_SYSTEM:
139
+ return `--- system ---\n${m.content}\n--------------`;
140
+ default:
141
+ return `${this._msgPrefix(m)} ${m.content}`;
142
+ }
143
+ })
144
+ .join('\n');
145
+ }
146
+
147
+ toJSON () {
148
+ return this.toArray();
149
+ }
150
+
151
+ /**
152
+ *
153
+ * @param {boolean} [needRaw=false]
154
+ * @returns {this}
155
+ */
156
+ debug (needRaw = false) {
157
+ // eslint-disable-next-line no-console
158
+ console.log('LLMSession#debug\n', this.toString(
159
+ needRaw ? this._chat : this.toArray()
160
+ ));
161
+ return this;
162
+ }
163
+
164
+ /**
165
+ *
166
+ * @param {LLMMessage} message
167
+ * @returns {this}
168
+ */
169
+ push (message) {
170
+ // if its system, append it on top
171
+ this._chat.push(message);
172
+ return this;
173
+ }
174
+
175
+ /**
176
+ *
177
+ * @param {string} content
178
+ * @returns {this}
179
+ */
180
+ systemPrompt (content) {
181
+ this.push({
182
+ role: LLM.ROLE_SYSTEM,
183
+ content
184
+ });
185
+ this._sort();
186
+ return this;
187
+ }
188
+
189
+ /**
190
+ *
191
+ * @param {LLMProviderOptions} [options={}]
192
+ * @returns {Promise<LLMMessage>}
193
+ */
194
+ async generate (options = {}) {
195
+ const result = await this._llm.generate(this, options);
196
+
197
+ this._generatedIndex = this._chat.length;
198
+ this._chat.push(result);
199
+
200
+ return result;
201
+ }
202
+
203
+ /**
204
+ *
205
+ * @param {QuickReply[]} quickReplies
206
+ * @returns {this}
207
+ */
208
+ send (quickReplies = undefined) {
209
+ if (!this._generatedIndex) {
210
+ // eslint-disable-next-line no-console
211
+ console.log('LLMSession', this.toString());
212
+ throw new Error('LLMSession: no message to send');
213
+ }
214
+
215
+ let messages = this._chat.splice(this._generatedIndex);
216
+ messages = messages.flatMap((msg) => LLM.toMessages(msg));
217
+ this._generatedIndex = null;
218
+ this._chat.push(...messages);
219
+
220
+ this._onSend(messages, quickReplies);
221
+
222
+ return this;
223
+ }
224
+
225
+ }
226
+
227
+ module.exports = LLMSession;
package/src/Processor.js CHANGED
@@ -11,6 +11,8 @@ const Request = require('./Request');
11
11
  const Ai = require('./Ai');
12
12
  const ReturnSender = require('./ReturnSender');
13
13
  const { prepareState, mergeState, isUserInteraction } = require('./utils/stateVariables');
14
+ const LLM = require('./LLM');
15
+ const LLMMockProvider = require('./LLMMockProvider');
14
16
 
15
17
  /** @typedef {import('./wingbot/CustomEntityDetectionModel').Intent} Intent */
16
18
  /** @typedef {import('./ReducerWrapper')} ReducerWrapper */
@@ -19,6 +21,7 @@ const { prepareState, mergeState, isUserInteraction } = require('./utils/stateVa
19
21
  /** @typedef {import('./analytics/consts').TrackingCategory} TrackingCategory */
20
22
  /** @typedef {import('./analytics/consts').TrackingType} TrackingType */
21
23
  /** @typedef {import('./analytics/consts').ResponseFlag} ResponseFlag */
24
+ /** @typedef {import('./LLM').LLMConfiguration} LLMConfiguration */
22
25
 
23
26
  /**
24
27
  * @typedef {object} AutoTypingConfig
@@ -110,6 +113,7 @@ const { prepareState, mergeState, isUserInteraction } = require('./utils/stateVa
110
113
  * @prop {string} [apiUrl] - Url for calling orchestrator API
111
114
  * @prop {Function} [fetch] - Fetch function for calling orchestrator API
112
115
  * @prop {number} [sessionDuration] - Session duration for analytic purposes
116
+ * @prop {LLMConfiguration} [llm] - LLM model configuration
113
117
  * @prop {Preloader<R>} [preloader]
114
118
  */
115
119
 
@@ -401,14 +405,19 @@ class Processor extends EventEmitter {
401
405
  return { status: 304 };
402
406
  }
403
407
 
408
+ const llm = new LLM({
409
+ provider: new LLMMockProvider(),
410
+ ...this.options.llm
411
+ });
412
+
404
413
  const result = await this
405
- ._dispatch(message, pageId, messageSender, responderData, preloadPromise);
414
+ ._dispatch(message, pageId, messageSender, responderData, preloadPromise, llm);
406
415
 
407
416
  messageSender.defer(preloadPromise, this.options.log);
408
417
  return result;
409
418
  }
410
419
 
411
- async _dispatch (message, pageId, messageSender, responderData, preloadPromise) {
420
+ async _dispatch (message, pageId, messageSender, responderData, preloadPromise, llm) {
412
421
  let req;
413
422
  let res;
414
423
  let state;
@@ -418,7 +427,14 @@ class Processor extends EventEmitter {
418
427
  ({
419
428
  req, res, data, state
420
429
  } = await this
421
- ._processMessage(message, pageId, messageSender, responderData, preloadPromise));
430
+ ._processMessage(
431
+ message,
432
+ pageId,
433
+ messageSender,
434
+ responderData,
435
+ llm,
436
+ preloadPromise
437
+ ));
422
438
 
423
439
  messageSender.defer(this._emitInteractionEvent(req, res, messageSender, state, data));
424
440
 
@@ -548,6 +564,7 @@ class Processor extends EventEmitter {
548
564
  pageId,
549
565
  messageSender,
550
566
  responderData,
567
+ llm,
551
568
  preloadPromise = null,
552
569
  senderMeta = null
553
570
  ) {
@@ -664,13 +681,13 @@ class Processor extends EventEmitter {
664
681
  _sts: sessionTs,
665
682
  _snew: sessionCreated
666
683
  });
667
- } else {
684
+ } /* else {
668
685
  Object.assign(state, {
669
686
  _snew: false
670
687
  });
671
- }
688
+ } */
672
689
 
673
- prepareState(state, fromEvent, state._snew);
690
+ prepareState(state, fromEvent, state._snew && fromEvent);
674
691
 
675
692
  const features = [
676
693
  ...(this.options.features || []),
@@ -690,7 +707,8 @@ class Processor extends EventEmitter {
690
707
  options,
691
708
  responderData,
692
709
  configuration,
693
- senderMeta
710
+ senderMeta,
711
+ llm
694
712
  );
695
713
  const postBack = this._createPostBack(postbackAcumulator, req, res, features);
696
714
 
@@ -852,6 +870,7 @@ class Processor extends EventEmitter {
852
870
  pageId,
853
871
  messageSender,
854
872
  responderData,
873
+ llm,
855
874
  res.senderMeta
856
875
  );
857
876
 
@@ -949,6 +968,7 @@ class Processor extends EventEmitter {
949
968
  pageId,
950
969
  messageSender,
951
970
  responderData,
971
+ llm,
952
972
  senderMeta
953
973
  ) {
954
974
  return postbackAcumulator.reduce((promise, postback) => promise
@@ -966,6 +986,7 @@ class Processor extends EventEmitter {
966
986
  pageId,
967
987
  messageSender,
968
988
  responderData,
989
+ llm,
969
990
  null,
970
991
  senderMeta
971
992
  );
package/src/Responder.js CHANGED
@@ -17,6 +17,8 @@ const {
17
17
  FEATURE_PHRASES
18
18
  } = require('./features');
19
19
  const transcriptFromHistory = require('./transcript/transcriptFromHistory');
20
+ const LLM = require('./LLM');
21
+ const LLMSession = require('./LLMSession');
20
22
 
21
23
  const TYPE_RESPONSE = 'RESPONSE';
22
24
  const TYPE_UPDATE = 'UPDATE';
@@ -28,8 +30,11 @@ const EXCEPTION_HOPCOUNT_THRESHOLD = 5;
28
30
  /** @typedef {import('./ReturnSender').SendOptions} SendOptions */
29
31
  /** @typedef {import('./ReturnSender').TextFilter} TextFilter */
30
32
  /** @typedef {import('./analytics/consts').TrackingCategory} TrackingCategory */
31
- /** @typedef {import('./analytics/consts').TrackingType} TrackingType */
32
33
  /** @typedef {import('./transcript/transcriptFromHistory').Transcript} Transcript */
34
+ /** @typedef {import('./analytics/consts').TrackingType} TrackingType */
35
+
36
+ /** @typedef {import('./LLM').LLMConfiguration} LLMConfiguration */
37
+ /** @typedef {import('./LLMSession').LLMMessage} LLMMessage */
33
38
 
34
39
  /**
35
40
  * @enum {string} ExpectedInput
@@ -114,12 +119,14 @@ class Responder {
114
119
  options = {},
115
120
  data = {},
116
121
  configuration = {},
117
- senderMeta = null
122
+ senderMeta = null,
123
+ llm = null
118
124
  ) {
119
125
  this._messageSender = messageSender;
120
126
  this._senderId = senderId;
121
127
  this._pageId = options.pageId;
122
128
  this.token = token;
129
+
123
130
  this._configuration = configuration;
124
131
 
125
132
  /**
@@ -214,20 +221,124 @@ class Responder {
214
221
 
215
222
  /** @type {SendOptions} */
216
223
  this._nextMessageSendOptions = null;
224
+
225
+ /** @type {LLM} */
226
+ this.llm = llm;
227
+
228
+ this.LLM_CTX_DEFAULT = 'default';
229
+
230
+ /** @type {Map<string,string[]>} */
231
+ this._llmContext = new Map([
232
+ [this.LLM_CTX_DEFAULT, []]
233
+ ]);
234
+ }
235
+
236
+ /**
237
+ *
238
+ * @param {string} systemPrompt
239
+ * @param {string} contextType
240
+ * @returns {this}
241
+ */
242
+ llmAddSystemPrompt (systemPrompt, contextType = this.LLM_CTX_DEFAULT) {
243
+ if (!systemPrompt || !systemPrompt.trim()) {
244
+ return this;
245
+ }
246
+ if (!this._llmContext.has(contextType)) {
247
+ // @todo make it array of messages / maybe keep it in a single array
248
+ this._llmContext.set(contextType, []);
249
+ }
250
+ this._llmContext.get(contextType).push(systemPrompt.trim());
251
+ return this;
252
+ }
253
+
254
+ llmSession (contextType = this.LLM_CTX_DEFAULT) {
255
+ const chat = this._getSystemContentForType(contextType)
256
+ .map((content) => ({ role: LLM.ROLE_SYSTEM, content }));
257
+
258
+ return new LLMSession(this.llm, chat, this._llmSend.bind(this));
259
+ }
260
+
261
+ /**
262
+ *
263
+ * @param {string} contextType
264
+ * @param {string[]} [callStack]
265
+ * @returns {string[]}
266
+ */
267
+ _getSystemContentForType (contextType, callStack = []) {
268
+ if (new Set(callStack).size < callStack.length) {
269
+ throw new Error(`Circular reference detected: contextType -> ${callStack}`);
270
+ }
271
+
272
+ if (!this._llmContext.has(contextType)) {
273
+ return [];
274
+ }
275
+
276
+ return this._llmContext.get(contextType)
277
+ .map((c) => c.replace(/\$\{([a-zA-Z0-9\s]+)\}/g, (str, requestType) => this
278
+ ._getSystemContentForType(requestType, [...callStack, contextType])
279
+ .join('\n\n')).trim());
280
+ }
281
+
282
+ async llmSessionWithHistory (contextType = this.LLM_CTX_DEFAULT) {
283
+ const {
284
+ transcriptAnonymize,
285
+ transcriptFlag,
286
+ transcriptLength
287
+ } = this.llm.configuration;
288
+
289
+ const systems = this._getSystemContentForType(contextType);
290
+ const transcript = await this.getTranscript(transcriptLength, transcriptFlag);
291
+
292
+ const chat = [
293
+ ...systems.map((content) => ({ role: LLM.ROLE_SYSTEM, content })),
294
+ ...LLM.anonymizeTranscript(transcript, transcriptAnonymize)
295
+ ];
296
+
297
+ return new LLMSession(this.llm, chat, this._llmSend.bind(this));
298
+ }
299
+
300
+ /**
301
+ *
302
+ * @param {LLMMessage[]} messages
303
+ * @param {QuickReply[]} quickReplies
304
+ */
305
+ _llmSend (messages, quickReplies) {
306
+ this.setFlag(LLM.GPT_FLAG);
307
+
308
+ const { persona } = this.llm.configuration;
309
+
310
+ if (typeof persona === 'string') {
311
+ this.setPersona({ name: persona });
312
+ } else if (persona) {
313
+ this.setPersona(persona);
314
+ }
315
+
316
+ messages.forEach((m, i) => {
317
+ const addQuickReply = i === (messages.length - 1);
318
+ this.text(m.content, addQuickReply ? quickReplies : null);
319
+ });
320
+
321
+ if (persona) {
322
+ this.setPersona({ name: null });
323
+ }
217
324
  }
218
325
 
219
326
  _findPersonaConfiguration (name) {
327
+ // @ts-ignore
220
328
  if (!name || !this._configuration.persona) {
221
329
  return null;
222
330
  }
331
+ // @ts-ignore
223
332
  if (!this._configuration._cachedPersonas) {
224
- // eslint-disable-next-line no-param-reassign
333
+ // @ts-ignore
225
334
  this._configuration._cachedPersonas = new Map(
335
+ // @ts-ignore
226
336
  Object.entries(this._configuration.persona)
227
337
  .map(([k, v]) => [k === PERSONA_DEFAULT ? k : tokenize(k), v])
228
338
  );
229
339
  }
230
340
  const nameKey = name === PERSONA_DEFAULT ? PERSONA_DEFAULT : tokenize(name);
341
+ // @ts-ignore
231
342
  return this._configuration._cachedPersonas.get(nameKey);
232
343
  }
233
344
 
@@ -252,16 +363,16 @@ class Responder {
252
363
  */
253
364
  async getTranscript (limit = 10, onlyFlag = null, skipThisTurnaround = false) {
254
365
  const { chatLogStorage, timestamp } = this._messageSender;
255
- if (!chatLogStorage) {
256
- return [];
366
+ let transcript = [];
367
+ if (chatLogStorage) {
368
+ transcript = await transcriptFromHistory(
369
+ chatLogStorage,
370
+ this._senderId,
371
+ this._pageId,
372
+ limit,
373
+ onlyFlag
374
+ );
257
375
  }
258
- const transcript = await transcriptFromHistory(
259
- chatLogStorage,
260
- this._senderId,
261
- this._pageId,
262
- limit,
263
- onlyFlag
264
- );
265
376
  if (!skipThisTurnaround) {
266
377
  const { responseTexts = [], requestTexts = [] } = this._messageSender;
267
378
  transcript.push(...requestTexts.map((text) => ({
@@ -11,6 +11,7 @@ const extractText = require('./transcript/extractText');
11
11
  /** @typedef {import('./Request')} Request */
12
12
  /** @typedef {import('./Responder')} Responder */
13
13
  /** @typedef {import('./Processor').TrackingObject} TrackingObject */
14
+ /** @typedef {import('./LLMContext').LLMMessage} LLMMessage */
14
15
 
15
16
  /**
16
17
  * @callback GetInteractions
package/src/Tester.js CHANGED
@@ -18,6 +18,7 @@ const ResponseAssert = require('./testTools/ResponseAssert');
18
18
  const Router = require('./Router'); // eslint-disable-line no-unused-vars
19
19
  const ReducerWrapper = require('./ReducerWrapper'); // eslint-disable-line no-unused-vars
20
20
  const { FEATURE_TEXT } = require('./features');
21
+ const LLMMockProvider = require('./LLMMockProvider');
21
22
 
22
23
  /** @typedef {import('./Processor').ProcessorOptions<Router>} ProcessorOptions */
23
24
 
@@ -100,6 +101,10 @@ class Tester {
100
101
  log,
101
102
  // @ts-ignore
102
103
  loadUsers: false,
104
+ llm: {
105
+ provider: new LLMMockProvider(),
106
+ ...processorOptions.llm
107
+ },
103
108
  ...processorOptions
104
109
  }));
105
110
 
@@ -28,8 +28,8 @@ function button (params, context) {
28
28
  buttons = [],
29
29
  text = null
30
30
  } = params;
31
- const compiledText = cachedTranslatedCompilator(text);
32
31
 
32
+ const compiledText = cachedTranslatedCompilator(text);
33
33
  const condition = getCondition(params, context, 'button');
34
34
 
35
35
  const ret = isLastIndex ? Router.END : Router.CONTINUE;
@@ -4,8 +4,9 @@
4
4
  'use strict';
5
5
 
6
6
  function include (params, context, plugins) {
7
- const includedRouter = context.blocks
8
- .find((block) => block.staticBlockId === params.staticBlockId);
7
+ const includedRouter = context.nestedBlocksByStaticId
8
+ ? context.nestedBlocksByStaticId.get(params.staticBlockId)
9
+ : context.blocks.find((block) => block.staticBlockId === params.staticBlockId);
9
10
 
10
11
  if (!includedRouter) {
11
12
  throw new Error(`Block ${params.staticBlockId} not found!`);
@@ -289,15 +289,13 @@ function message (params, context = {}) {
289
289
  }
290
290
 
291
291
  // parse quick replies
292
- let quickReplies = null;
292
+ let quickReplies;
293
293
  if (params.replies && !Array.isArray(params.replies)) {
294
294
  throw new Error('Replies should be an array');
295
- } else if (params.replies && params.replies.length > 0) {
296
- quickReplies = parseReplies(params.replies, linksMap, context);
297
295
  }
298
296
 
299
297
  // compile condition
300
- const condition = getCondition(params, context, 'Message condition');
298
+ let condition;
301
299
 
302
300
  const ret = isLastIndex ? Router.END : Router.CONTINUE;
303
301
 
@@ -306,6 +304,17 @@ function message (params, context = {}) {
306
304
  * @param {Responder} res
307
305
  */
308
306
  return (req, res) => {
307
+ if (condition === undefined) {
308
+ condition = getCondition(params, context, 'Message condition');
309
+ }
310
+ if (quickReplies === undefined) {
311
+ if (params.replies && params.replies.length > 0) {
312
+ quickReplies = parseReplies(params.replies, linksMap, context);
313
+ } else {
314
+ quickReplies = null;
315
+ }
316
+ }
317
+
309
318
  const data = stateData(req, res, configuration);
310
319
 
311
320
  // filter supported messages
@@ -32,11 +32,15 @@ function postback (params, context) {
32
32
  }
33
33
  }
34
34
 
35
- const condition = getCondition(params, context, 'postback');
35
+ let condition;
36
36
 
37
37
  const ret = isLastIndex ? Router.END : Router.CONTINUE;
38
38
 
39
39
  return (req, res, postBack) => {
40
+ if (condition === undefined) {
41
+ condition = getCondition(params, context, 'postback');
42
+ }
43
+
40
44
  if (!action || !shouldExecuteResolver(req, params)) {
41
45
  return ret;
42
46
  }
@@ -20,16 +20,21 @@ const { dateToISO8601String, zeroHourDate } = require('./datetime');
20
20
 
21
21
  /**
22
22
  *
23
- * @param {IStateRequest} req
24
- * @param {Responder} res
25
- * @param {object} configuration
23
+ * @param {IStateRequest} [req]
24
+ * @param {Responder} [res]
25
+ * @param {object} [configuration]
26
26
  * @param {object} [stateOverride]
27
27
  * @returns {object}
28
28
  */
29
- module.exports = function stateData (req, res = null, configuration = null, stateOverride = {}) {
30
- const c = configuration || req.configuration;
29
+ module.exports = function stateData (
30
+ req = null,
31
+ res = null,
32
+ configuration = null,
33
+ stateOverride = {}
34
+ ) {
35
+ const c = configuration || (req && req.configuration) || (res && res._configuration);
31
36
 
32
- const $this = req.text();
37
+ const $this = req ? req.text() : '';
33
38
 
34
39
  const now = new Date();
35
40
 
@@ -42,15 +47,20 @@ module.exports = function stateData (req, res = null, configuration = null, stat
42
47
 
43
48
  const {
44
49
  senderId,
45
- pageId
46
- } = req;
50
+ pageId,
51
+ state
52
+ } = req || {
53
+ senderId: res._senderId,
54
+ pageId: res._pageId,
55
+ state: res.options.state
56
+ };
47
57
 
48
58
  return {
49
59
  senderId,
50
60
  pageId,
51
61
  c,
52
62
  configuration: c,
53
- ...req.state,
63
+ ...state,
54
64
  ...(res ? res.newState : {}),
55
65
  ...stateOverride,
56
66
  $this,
@@ -59,7 +69,7 @@ module.exports = function stateData (req, res = null, configuration = null, stat
59
69
  $tomorrow,
60
70
  $yesterday,
61
71
  // yes - res because of circular dependency
62
- ...(res && req.actionData()),
72
+ ...(res && req && req.actionData()),
63
73
  ...(res ? res.data : {}),
64
74
  $input: $this
65
75
  };