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.
package/src/LLMSession.js CHANGED
@@ -13,6 +13,8 @@ const LLM = require('./LLM');
13
13
  /** @typedef {'tool'} LLMToolRole */
14
14
  /** @typedef {LLMChatRole|LLMSystemRole|LLMToolRole|string} LLMRole */
15
15
 
16
+ /** @typedef {LLMRole|'conversation'} FilterScope */
17
+
16
18
  /** @typedef {'stop'|'length'|'tool_calls'|'content_filter'} LLMFinishReason */
17
19
 
18
20
  /**
@@ -38,6 +40,19 @@ const LLM = require('./LLM');
38
40
  * @param {QuickReply[]} quickReplies
39
41
  */
40
42
 
43
+ /**
44
+ * @callback LLMFilterFn
45
+ * @param {string} text
46
+ * @param {LLMRole} role
47
+ * @returns {boolean|string}
48
+ */
49
+
50
+ /**
51
+ * @typedef {object} LLMFilter
52
+ * @prop {LLMFilterFn} filter
53
+ * @prop {FilterScope} scope
54
+ */
55
+
41
56
  /**
42
57
  * @class LLMSession
43
58
  */
@@ -48,8 +63,9 @@ class LLMSession {
48
63
  * @param {LLM} llm
49
64
  * @param {LLMMessage<any>[]} [chat]
50
65
  * @param {SendCallback} [onSend]
66
+ * @param {LLMFilter[]} [filters=[]]
51
67
  */
52
- constructor (llm, chat = [], onSend = () => {}) {
68
+ constructor (llm, chat = [], onSend = () => {}, filters = []) {
53
69
  this._llm = llm;
54
70
 
55
71
  this._onSend = onSend;
@@ -60,6 +76,13 @@ class LLMSession {
60
76
  this._generatedIndex = null;
61
77
 
62
78
  this._sort();
79
+
80
+ /** @type {LLMFilter[]} */
81
+ this._filters = filters;
82
+
83
+ this._SCOPE_CONVERSATION_ROLES = [
84
+ LLM.ROLE_ASSISTANT, LLM.ROLE_USER
85
+ ];
63
86
  }
64
87
 
65
88
  _sort (what = this._chat) {
@@ -114,10 +137,43 @@ class LLMSession {
114
137
  }
115
138
 
116
139
  /**
140
+ *
141
+ * @param {boolean} [filtered=false]
117
142
  * @returns {LLMMessage[]}
118
143
  */
119
- toArray () {
120
- return this._mergeSystem();
144
+ toArray (filtered = false) {
145
+ const messages = this._mergeSystem();
146
+ if (!filtered || this._filters.length === 0) {
147
+ return messages;
148
+ }
149
+ return messages
150
+ .map((message) => {
151
+ if (!message.content) {
152
+ return message;
153
+ }
154
+ const content = this._filters.reduce((text, filter) => {
155
+ if (!text) {
156
+ return text;
157
+ }
158
+ if (filter.scope !== message.role
159
+ && (filter.scope !== LLM.FILTER_SCOPE_CONVERSATION
160
+ || !this._SCOPE_CONVERSATION_ROLES.includes(message.role))) {
161
+ return text;
162
+ }
163
+ const res = filter.filter(text, message.role);
164
+ return res === true ? text : res;
165
+ }, message.content);
166
+
167
+ if (!content) {
168
+ return null;
169
+ }
170
+
171
+ return {
172
+ ...message,
173
+ content
174
+ };
175
+ })
176
+ .filter((message) => message !== null);
121
177
  }
122
178
 
123
179
  /**
@@ -200,6 +256,20 @@ class LLMSession {
200
256
  return this;
201
257
  }
202
258
 
259
+ /**
260
+ *
261
+ * @param {LLMFilter|LLMFilter[]} filter
262
+ * @returns {this}
263
+ */
264
+ addFilter (filter) {
265
+ if (Array.isArray(filter)) {
266
+ this._filters.push(...filter);
267
+ } else {
268
+ this._filters.push(filter);
269
+ }
270
+ return this;
271
+ }
272
+
203
273
  /**
204
274
  *
205
275
  * @param {LLMProviderOptions} [options={}]
@@ -214,6 +284,23 @@ class LLMSession {
214
284
  return result;
215
285
  }
216
286
 
287
+ /**
288
+ *
289
+ * @returns {string}
290
+ */
291
+ lastResponse () {
292
+ const messages = [];
293
+ for (let i = this._chat.length - 1; i >= 0; i--) {
294
+ const message = this._chat[i];
295
+
296
+ if (message.role !== LLM.ROLE_ASSISTANT || !message.content) {
297
+ break;
298
+ }
299
+ messages.unshift(message.content);
300
+ }
301
+ return messages.join('\n\n');
302
+ }
303
+
217
304
  /**
218
305
  *
219
306
  * @param {boolean} [dontMarkAsSent=false]
package/src/Processor.js CHANGED
@@ -416,7 +416,7 @@ class Processor extends EventEmitter {
416
416
  });
417
417
  }
418
418
 
419
- const llm = new LLM(llmOptions);
419
+ const llm = new LLM(llmOptions, Ai.ai);
420
420
 
421
421
  const result = await this
422
422
  ._dispatch(message, pageId, messageSender, responderData, preloadPromise, llm);
package/src/Responder.js CHANGED
@@ -34,7 +34,14 @@ const EXCEPTION_HOPCOUNT_THRESHOLD = 5;
34
34
  /** @typedef {import('./analytics/consts').TrackingType} TrackingType */
35
35
 
36
36
  /** @typedef {import('./LLM').LLMConfiguration} LLMConfiguration */
37
+ /** @typedef {import('./LLM').PreprocessedRule} PreprocessedRule */
38
+ /** @typedef {import('./LLM').EvaluationRuleAction} EvaluationRuleAction */
39
+ /** @typedef {import('./LLM').EvaluationResult} EvaluationResult */
37
40
  /** @typedef {import('./LLMSession').LLMMessage} LLMMessage */
41
+ /** @typedef {import('./LLMSession').LLMFilterFn} LLMFilterFn */
42
+ /** @typedef {import('./LLMSession').LLMFilter} LLMFilter */
43
+ /** @typedef {import('./LLMSession').FilterScope} FilterScope */
44
+ /** @typedef {import('./utils/stateData').IStateRequest} IStateRequest */
38
45
 
39
46
  /**
40
47
  * @enum {string} ExpectedInput
@@ -243,6 +250,28 @@ class Responder {
243
250
  this._llmContext = new Map([
244
251
  [this.LLM_CTX_DEFAULT, []]
245
252
  ]);
253
+
254
+ /** @type {Map<string,PreprocessedRule[]>} */
255
+ this._llmResultRules = new Map([
256
+ [this.LLM_CTX_DEFAULT, []]
257
+ ]);
258
+
259
+ /** @type {Map<string,LLMFilter[]>} */
260
+ this._llmFilters = new Map([
261
+ [this.LLM_CTX_DEFAULT, []],
262
+ [null, []]
263
+ ]);
264
+ }
265
+
266
+ /**
267
+ *
268
+ * @deprecated use llmAddInstructions() instead
269
+ * @param {PromptSource} systemPrompt
270
+ * @param {string} [contextType]
271
+ * @returns {this}
272
+ */
273
+ llmAddSystemPrompt (systemPrompt, contextType) {
274
+ return this.llmAddInstructions(systemPrompt, contextType);
246
275
  }
247
276
 
248
277
  /**
@@ -251,7 +280,7 @@ class Responder {
251
280
  * @param {string} contextType
252
281
  * @returns {this}
253
282
  */
254
- llmAddSystemPrompt (systemPrompt, contextType = this.LLM_CTX_DEFAULT) {
283
+ llmAddInstructions (systemPrompt, contextType = this.LLM_CTX_DEFAULT) {
255
284
  if (!systemPrompt) {
256
285
  return this;
257
286
  }
@@ -264,12 +293,109 @@ class Responder {
264
293
  return this;
265
294
  }
266
295
 
296
+ /**
297
+ *
298
+ * @param {LLMFilter|LLMFilterFn} filter
299
+ * @param {FilterScope} [scope]
300
+ * @param {string} [contextType]
301
+ * @returns {this}
302
+ */
303
+ llmAddFilter (
304
+ filter,
305
+ scope = LLM.FILTER_SCOPE_CONVERSATION,
306
+ contextType = null
307
+ ) {
308
+ /** @type {LLMFilter} */
309
+ const addFilter = typeof filter === 'function'
310
+ ? {
311
+ filter,
312
+ scope
313
+ }
314
+ : filter;
315
+
316
+ if (!this._llmFilters.has(contextType)) {
317
+ this._llmFilters.set(contextType, []);
318
+ }
319
+ this._llmFilters.get(contextType).push(addFilter);
320
+ return this;
321
+ }
322
+
323
+ /**
324
+ *
325
+ * @param {string[]|PreprocessedRule} rule
326
+ * @param {EvaluationRuleAction} [action]
327
+ * @param {object} [setState]
328
+ * @param {string} [contextType]
329
+ * @returns {this}
330
+ */
331
+ llmAddResultRule (
332
+ rule,
333
+ action = null,
334
+ setState = null,
335
+ contextType = this.LLM_CTX_DEFAULT
336
+ ) {
337
+ let addRule = rule;
338
+
339
+ if (Array.isArray(addRule)) {
340
+ [addRule] = LLM.preprocessEvaluationRules([{
341
+ // @ts-ignore
342
+ aiTags: rule,
343
+ action,
344
+ setState
345
+ }], {
346
+ ai: this.llm.ai
347
+ });
348
+ }
349
+
350
+ if (!this._llmResultRules.has(contextType)) {
351
+ this._llmResultRules.set(contextType, []);
352
+ }
353
+ this._llmResultRules.get(contextType).push(addRule);
354
+ return this;
355
+ }
356
+
267
357
  async llmSession (contextType = this.LLM_CTX_DEFAULT) {
268
358
  const system = await this._getSystemContentForType(contextType);
269
359
 
270
360
  const chat = system.map((content) => ({ role: LLM.ROLE_SYSTEM, content }));
271
361
 
272
- return new LLMSession(this.llm, chat, this._llmSend.bind(this));
362
+ const filters = this._filtersForContext(contextType);
363
+ return new LLMSession(this.llm, chat, this._llmSend.bind(this), filters);
364
+ }
365
+
366
+ /**
367
+ *
368
+ * @param {LLMSession} session
369
+ * @param {string} [contextType]
370
+ * @returns {Promise<EvaluationResult>}
371
+ */
372
+ async llmEvaluate (session, contextType = this.LLM_CTX_DEFAULT) {
373
+ const rules = this._llmResultRules.get(contextType) || [];
374
+ const text = session.lastResponse();
375
+
376
+ if (rules.length === 0 || !text) {
377
+ return {
378
+ action: null,
379
+ setState: {},
380
+ results: [],
381
+ discard: false
382
+ };
383
+ }
384
+
385
+ /** @type {IStateRequest} */
386
+ const req = {
387
+ state: this.options.state,
388
+ text: () => text,
389
+ senderId: this._senderId,
390
+ pageId: this._pageId,
391
+ actionData: () => this._data,
392
+ isConfidentInput: () => false
393
+ };
394
+
395
+ const result = await this.llm.evaluateResultWithRules(text, rules, req, this);
396
+ this.setState(result.setState);
397
+
398
+ return result;
273
399
  }
274
400
 
275
401
  async _replaceAsync (str, regex, asyncFn) {
@@ -339,7 +465,20 @@ class Responder {
339
465
  ...LLM.anonymizeTranscript(transcript, transcriptAnonymize)
340
466
  ];
341
467
 
342
- return new LLMSession(this.llm, chat, this._llmSend.bind(this));
468
+ const filters = this._filtersForContext(contextType);
469
+ return new LLMSession(this.llm, chat, this._llmSend.bind(this), filters);
470
+ }
471
+
472
+ /**
473
+ *
474
+ * @param {string|null} contextType
475
+ * @returns {LLMFilter[]}
476
+ */
477
+ _filtersForContext (contextType) {
478
+ return [
479
+ ...(this._llmFilters.get(contextType) || []),
480
+ ...this._llmFilters.get(null)
481
+ ];
343
482
  }
344
483
 
345
484
  /**
@@ -394,6 +533,7 @@ class Responder {
394
533
  */
395
534
  setFlag (flag) {
396
535
  this._senderMeta.flag = flag;
536
+ // @ts-ignore
397
537
  return this;
398
538
  }
399
539
 
@@ -162,7 +162,7 @@ class ReturnSender {
162
162
  };
163
163
 
164
164
  /** @type {PromptInfo[]} */
165
- this._prompts = [];
165
+ this.prompts = [];
166
166
 
167
167
  this._responseTexts = [];
168
168
 
@@ -199,7 +199,7 @@ class ReturnSender {
199
199
  * @param {PromptInfo} promptInfo
200
200
  */
201
201
  logPrompt (promptInfo) {
202
- this._prompts.push(promptInfo);
202
+ this.prompts.push(promptInfo);
203
203
  }
204
204
 
205
205
  /**
@@ -669,7 +669,7 @@ class ReturnSender {
669
669
  const payload = {};
670
670
  const meta = {
671
671
  actions: this._visitedInteractions.slice(),
672
- prompts: this._prompts
672
+ prompts: this.prompts
673
673
  };
674
674
 
675
675
  if (req) {
@@ -697,7 +697,7 @@ class ReturnSender {
697
697
  _createMeta (req = null, res = null) { // eslint-disable-line no-unused-vars
698
698
  const meta = {
699
699
  visitedInteractions: this._visitedInteractions.slice(),
700
- prompts: this._prompts
700
+ prompts: this.prompts
701
701
  };
702
702
 
703
703
  if (req) {
package/src/Tester.js CHANGED
@@ -19,8 +19,12 @@ 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
21
  const LLMMockProvider = require('./LLMMockProvider');
22
+ const PromptAssert = require('./testTools/PromptAssert');
22
23
 
23
24
  /** @typedef {import('./Processor').ProcessorOptions<Router>} ProcessorOptions */
25
+ /** @typedef {import('./LLM').PromptInfo} PromptInfo */
26
+ /** @typedef {import('./LLM').LLMRole} LLMRole */
27
+ /** @typedef {import('./LLM').LLMMessage} LLMMessage */
24
28
 
25
29
  /**
26
30
  * Utility for testing requests
@@ -128,6 +132,9 @@ class Tester {
128
132
  this.responses = [];
129
133
  this.actions = [];
130
134
 
135
+ /** @type {PromptInfo[]} */
136
+ this.prompts = [];
137
+
131
138
  /**
132
139
  * @prop {object} predefined test data to use
133
140
  */
@@ -201,6 +208,7 @@ class Tester {
201
208
  this._actionsCollector = [];
202
209
  this._pluginBlocksCollector = [];
203
210
  this._responsesMock = [];
211
+ this.prompts = [];
204
212
  }
205
213
 
206
214
  /**
@@ -259,6 +267,7 @@ class Tester {
259
267
  throw Object.assign(new Error(`Processor failed with status ${res.status}`), { code: res.status });
260
268
  }
261
269
  this.responses = messageSender.responses;
270
+ this.prompts = messageSender.prompts;
262
271
  this.pluginBlocks = this._pluginBlocksCollector;
263
272
  this.actions = this._actionsCollector;
264
273
  this._actionsCollector = [];
@@ -388,6 +397,36 @@ class Tester {
388
397
  this.storage.saveState(stateObj);
389
398
  }
390
399
 
400
+ /**
401
+ *
402
+ * @returns {PromptAssert}
403
+ */
404
+ anyPrompt () {
405
+ return new PromptAssert(this.prompts);
406
+ }
407
+
408
+ /**
409
+ *
410
+ * @returns {PromptAssert}
411
+ */
412
+ lastPrompt () {
413
+ return new PromptAssert(
414
+ this.prompts.length
415
+ ? [this.prompts[this.prompts.length - 1]]
416
+ : []
417
+ );
418
+ }
419
+
420
+ /**
421
+ *
422
+ * @returns {LLMMessage}
423
+ */
424
+ getLastPromptResult () {
425
+ return this.prompts.length
426
+ ? this.prompts[this.prompts.length - 1].result
427
+ : null;
428
+ }
429
+
391
430
  /**
392
431
  * Assert, that state contains a subset of provided value
393
432
  *
@@ -606,9 +645,10 @@ class Tester {
606
645
  /**
607
646
  * Prints last conversation turnaround
608
647
  *
609
- * @param {boolean} [showPrivateKeys]
648
+ * @param {boolean} [full=false]
649
+ * @param {boolean} [showPrivateKeys=false]
610
650
  */
611
- debug (showPrivateKeys = false) {
651
+ debug (full = false, showPrivateKeys = false) {
612
652
  // eslint-disable-next-line no-console
613
653
  console.log(
614
654
  '\n===== actions =====\n',
@@ -625,6 +665,8 @@ class Tester {
625
665
  Object.entries(this.getState().state)
626
666
  .filter((e) => showPrivateKeys || !e[0].startsWith('_'))
627
667
  ),
668
+ '\n------- LLM -------\n',
669
+ ...PromptAssert.debug(this.prompts, full, true),
628
670
  '\n===================\n'
629
671
  );
630
672
  }
@@ -8,6 +8,7 @@ const WingbotApiConnector = require('./WingbotApiConnector');
8
8
  // @ts-ignore
9
9
  const packageJson = require('../../package.json');
10
10
  const headersToAuditMeta = require('../utils/headersToAuditMeta');
11
+ const gqlRules = require('./gqlRules');
11
12
 
12
13
  const DEFAULT_GROUPS = ['botEditor', 'botAdmin', 'appToken'];
13
14
  const KEYS_URL = 'https://api.wingbot.ai/keys';
@@ -27,6 +28,12 @@ const DEFAULT_CACHE = 86400000; // 24 hours
27
28
  /** @typedef {import('../CallbackAuditLog')} AuditLog */
28
29
  /** @typedef {import('graphql')} GqlLib */
29
30
 
31
+ /**
32
+ * @typedef {object} Logger
33
+ * @prop {Function} log
34
+ * @prop {Function} error
35
+ */
36
+
30
37
  /**
31
38
  * Experimental chatbot API
32
39
  */
@@ -41,8 +48,11 @@ class GraphApi {
41
48
  * @param {string[]} [options.groups] - list of allowed bot groups
42
49
  * @param {boolean} [options.useBundledGql] - uses library bundled graphql definition
43
50
  * @param {AuditLog} [options.auditLog]
51
+ * @param {boolean} [options.isProduction]
52
+ * @param {boolean} [options.hideVerboseErrors]
53
+ * @param {Logger} [log=console]
44
54
  */
45
- constructor (apis, options) {
55
+ constructor (apis, options, log = console) {
46
56
  this._root = {
47
57
  version () {
48
58
  return packageJson.version;
@@ -65,6 +75,13 @@ class GraphApi {
65
75
  }
66
76
  };
67
77
 
78
+ this._log = log;
79
+ this._options = {
80
+ hideVerboseErrors: true,
81
+ isProduction: true,
82
+ ...options
83
+ };
84
+
68
85
  Object.assign(opts, options);
69
86
 
70
87
  apis.forEach((api) => Object.assign(this._root, api));
@@ -152,6 +169,19 @@ class GraphApi {
152
169
 
153
170
  const schema = await this._schema();
154
171
 
172
+ const { isProduction = true, hideVerboseErrors } = this._options;
173
+ const ast = this._gql.parse(body.query);
174
+ const errors = this._gql.validate(
175
+ schema,
176
+ ast,
177
+ gqlRules(body.variables, isProduction, hideVerboseErrors, this._log)
178
+ );
179
+
180
+ if (errors.length > 0) {
181
+ this._log.error('GQL failed', errors);
182
+ return { errors };
183
+ }
184
+
155
185
  const ctx = {
156
186
  token,
157
187
  groups: this._defaultGroups,
@@ -0,0 +1,87 @@
1
+ /* eslint-disable global-require */
2
+ /**
3
+ * @author David Menger
4
+ */
5
+ 'use strict';
6
+
7
+ /** @typedef {import('graphql').ValidationRule} ValidationRule */
8
+
9
+ /**
10
+ * @typedef {object} Logger
11
+ * @prop {Function} log
12
+ * @prop {Function} error
13
+ */
14
+
15
+ /**
16
+ *
17
+ * @param {object} variables
18
+ * @param {boolean} isProduction
19
+ * @param {boolean} hideVerboseErrors
20
+ * @param {Logger} [log=console]
21
+ * @returns {ValidationRule[]}
22
+ */
23
+ function gqlRules (variables, isProduction, hideVerboseErrors, log = console) {
24
+ // OPTIMIZATION FOR LAMBDA PERFORMANCE
25
+ const { GraphQLError, NoSchemaIntrospectionCustomRule } = require('graphql');
26
+ const { createComplexityRule, simpleEstimator } = require('graphql-query-complexity');
27
+ const depthLimit = require('graphql-depth-limit');
28
+
29
+ return [
30
+ ...(hideVerboseErrors ? [NoSchemaIntrospectionCustomRule] : []),
31
+ depthLimit(10),
32
+ createComplexityRule({
33
+ // The maximum allowed query complexity, queries above this threshold will be rejected
34
+ maximumComplexity: 1000,
35
+
36
+ // The query variables. This is needed because the variables are not available
37
+ // in the visitor of the graphql-js library
38
+ variables,
39
+
40
+ // The context object for the request (optional)
41
+ context: {},
42
+
43
+ // The maximum number of query nodes to evaluate (fields, fragments, composite types).
44
+ // If a query contains more than the specified number of nodes, the complexity rule will
45
+ // throw an error, regardless of the complexity of the query.
46
+ //
47
+ // Default: 10_000
48
+ maxQueryNodes: 10000,
49
+
50
+ // Optional callback function to retrieve the determined query complexity
51
+ // Will be invoked whether the query is rejected or not
52
+ // This can be used for logging or to implement rate limiting
53
+ onComplete: (complexity) => {
54
+ if (!isProduction) {
55
+ log.log('Determined query complexity: ', complexity);
56
+ }
57
+ },
58
+
59
+ // Optional function to create a custom error
60
+ createError: (max, actual) => {
61
+ const msg = `GQL Query too complex: ${actual}. Maximum allowed: ${max}`;
62
+
63
+ if (hideVerboseErrors) {
64
+ log.error(msg);
65
+ return new GraphQLError('');
66
+ }
67
+ log.log(msg);
68
+ return new GraphQLError(`Query is too complex: ${actual}. Maximum allowed complexity: ${max}`);
69
+ },
70
+
71
+ // Add any number of estimators. The estimators are invoked in order, the first
72
+ // numeric value that is being returned by an estimator is used as the field complexity.
73
+ // If no estimator returns a value, an exception is raised.
74
+ estimators: [
75
+ // Add more estimators here...
76
+
77
+ // This will assign each field a complexity of 1 if no other estimator
78
+ // returned a value.
79
+ simpleEstimator({
80
+ defaultComplexity: 1
81
+ })
82
+ ]
83
+ })
84
+ ];
85
+ }
86
+
87
+ module.exports = gqlRules;
@@ -109,9 +109,24 @@ type LLMMessage {
109
109
  toolCalls: [ToolCall!]
110
110
  }
111
111
 
112
+ type VectorSearchDocument {
113
+ id: String!
114
+ name: String!
115
+ text: String!
116
+ cosineDistance: Float!
117
+ excludedByCosineDistanceThreshold: Boolean!
118
+ }
119
+
120
+ type VectorSearchResult {
121
+ maximalCosineDistanceThreshold: Float!
122
+ nearestNeighbourCount: Int!
123
+ resultDocuments: [VectorSearchDocument!]!
124
+ }
125
+
112
126
  type PromptInfo {
113
127
  prompt: [LLMMessage!]!
114
128
  result: LLMMessage!
129
+ vectorSearchResult: VectorSearchResult
115
130
  }
116
131
 
117
132
  type UserInteraction {