wingbot 3.76.18 → 3.77.0-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,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npx mocha:*)",
5
+ "Bash(npx tsc *)",
6
+ "Bash(npm test *)",
7
+ "Bash(node -e \"const p = require\\('./bot/plugins/BTMLLM'\\); console.log\\('factory type:', typeof p\\); console.log\\('factory name:', p.name\\);\")",
8
+ "Bash(grep -n \"prompt\\\\`\\\\|tagged\\\\|render\\\\|compile\\\\|hbs\" /Users/ondrejveres/Wingbot/wingbot-llm/src/prompt.js)"
9
+ ]
10
+ }
11
+ }
package/index.js CHANGED
@@ -70,6 +70,7 @@ const LLM = require('./src/LLM');
70
70
  const LLMSession = require('./src/LLMSession');
71
71
  const LLMTool = require('./src/LLMTool');
72
72
  const LLMType = require('./src/LLMType');
73
+ const LLMToolRunner = require('./src/LLMToolRunner');
73
74
  const LLMRouter = require('./src/LLMRouter');
74
75
  const prompt = require('./src/prompt');
75
76
 
@@ -167,6 +168,7 @@ module.exports = {
167
168
  LLMSession,
168
169
  LLMTool,
169
170
  LLMType,
171
+ LLMToolRunner,
170
172
 
171
173
  LLMRouter,
172
174
  prompt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.76.18",
3
+ "version": "3.77.0-alpha.1",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -75,7 +75,7 @@
75
75
  },
76
76
  "optionalDependencies": {
77
77
  "axios": "^1.6.4",
78
- "handlebars": "^4.0.0"
78
+ "handlebars": "4.7.10"
79
79
  },
80
80
  "allowScripts": {
81
81
  "fsevents@2.3.2": true,
@@ -157,12 +157,7 @@ const DUMMY_ROUTE = { id: 0, path: null, resolvers: [] };
157
157
  * @returns {string}
158
158
  */
159
159
 
160
- /**
161
- * @typedef {object} ILogger
162
- * @prop {Function} log
163
- * @prop {Function} warn
164
- * @prop {Function} error
165
- */
160
+ /** @typedef {import('./log/types').LevelLogger} ILogger */
166
161
 
167
162
  /**
168
163
  * @template {BaseConfiguration} [C=object]
@@ -206,11 +201,7 @@ const DUMMY_ROUTE = { id: 0, path: null, resolvers: [] };
206
201
  * @prop {string|number} [resolverId] - only for text messages with random characters
207
202
  */
208
203
 
209
- /**
210
- * @typedef {object} Logger
211
- * @prop {Function} log
212
- * @prop {Function} error
213
- */
204
+ /** @typedef {import('./log/types').LevelLogger} Logger */
214
205
 
215
206
  /**
216
207
  * @typedef {object} RunningReq
@@ -400,6 +391,30 @@ class BuildRouter extends Router {
400
391
  return this._botLoaded;
401
392
  }
402
393
 
394
+ /**
395
+ * What an expired request was about, without the whole request and
396
+ * responder (they hold the state and the transcript).
397
+ *
398
+ * @param {any[]} args - the reduce() arguments: (req, res, ...)
399
+ * @param {number} timeoutMs
400
+ * @returns {object}
401
+ */
402
+ static _expireInfo (args, timeoutMs) {
403
+ const [req] = Array.isArray(args) ? args : [];
404
+ let action = null;
405
+ try {
406
+ action = req && typeof req.action === 'function' ? req.action() : null;
407
+ } catch (e) {
408
+ // the action is only a hint
409
+ }
410
+ return {
411
+ senderId: (req && req.senderId) || null,
412
+ pageId: (req && req.pageId) || null,
413
+ action,
414
+ timeoutMs
415
+ };
416
+ }
417
+
403
418
  async reduce (...args) {
404
419
  await this.preload();
405
420
 
@@ -411,7 +426,7 @@ class BuildRouter extends Router {
411
426
  const timeoutPromise = new Promise((resolve) => {
412
427
  timeout = setTimeout(() => {
413
428
  timeout = null;
414
- this._logger.error('BuildRouter EXPIRE', { args });
429
+ this._logger.error('BuildRouter EXPIRE', BuildRouter._expireInfo(args, this.runningReqTimeout));
415
430
  this._runningReqs = this._runningReqs
416
431
  .filter((rr) => rr !== runningRequest);
417
432
  resolve();
@@ -527,7 +542,10 @@ class BuildRouter extends Router {
527
542
  this._runningReqs
528
543
  .filter((req) => {
529
544
  if (req.started < threshold) {
530
- this._logger.error('BuildRouter EXPIRE filter', req);
545
+ this._logger.error('BuildRouter EXPIRE filter', {
546
+ ...BuildRouter._expireInfo(req.args, this.runningReqTimeout),
547
+ startedAt: new Date(req.started).toISOString()
548
+ });
531
549
  return false;
532
550
  }
533
551
  return true;
@@ -35,11 +35,7 @@ const TYPE_INFO = 'Info';
35
35
  * @prop {string} [ro] - referrer || origin
36
36
  */
37
37
 
38
- /**
39
- * @typedef {object} Logger
40
- * @prop {Function} log
41
- * @prop {Function} error
42
- */
38
+ /** @typedef {import('./log/types').LevelLogger} Logger */
43
39
 
44
40
  /**
45
41
  * @typedef {object} AuditLogEntry
package/src/ChatGpt.js CHANGED
@@ -4,7 +4,6 @@
4
4
  'use strict';
5
5
 
6
6
  const nodeFetch = require('node-fetch').default;
7
- const util = require('util');
8
7
  const { PHONE_REGEX, EMAIL_REGEX } = require('./systemEntities/regexps');
9
8
  const LLM = require('./LLM');
10
9
 
@@ -101,11 +100,7 @@ const LLM = require('./LLM');
101
100
  * @prop {ChatGPTUsage} usage
102
101
  */
103
102
 
104
- /**
105
- * @typedef {object} Logger
106
- * @prop {Function} log
107
- * @prop {Function} error
108
- */
103
+ /** @typedef {import('./log/types').LevelLogger} Logger */
109
104
 
110
105
  /**
111
106
  * @typedef {object} GptToolCall
@@ -178,6 +173,9 @@ const LLM = require('./LLM');
178
173
  /**
179
174
  * @class ChatGpt
180
175
  */
176
+ // how much of the input and the output the info row of an LLM call keeps
177
+ const SUMMARY_TEXT_LENGTH = 2000;
178
+
181
179
  class ChatGpt {
182
180
 
183
181
  /**
@@ -242,15 +240,41 @@ class ChatGpt {
242
240
  return this._options.model;
243
241
  }
244
242
 
245
- _log (msg, ...args) {
246
- if (this._logger === console) {
247
-
248
- this._logger.log(msg, ...args.map((arg) => util.inspect(arg, {
249
- showHidden: false, depth: null, colors: true
250
- })));
251
- } else {
252
- this._logger.log(msg, ...args);
253
- }
243
+ /**
244
+ * The info row of one LLM call: the new input (the last message, not the
245
+ * whole transcript), the output and the usage. The whole request and
246
+ * response are in the debug rows.
247
+ *
248
+ * @param {object} body - the request body
249
+ * @param {ChatGPTChoice} choice
250
+ * @param {LLMUsage} usage
251
+ * @param {number} durationMs
252
+ * @returns {object}
253
+ */
254
+ static _summary (body, choice, usage, durationMs) {
255
+ const { messages = [], tools = [] } = body;
256
+ const last = messages[messages.length - 1] || {};
257
+ const { message = {}, finish_reason: finishReason } = choice || {};
258
+ const cut = (text) => (typeof text === 'string' && text.length > SUMMARY_TEXT_LENGTH
259
+ ? `${text.slice(0, SUMMARY_TEXT_LENGTH)}… (${text.length - SUMMARY_TEXT_LENGTH} more chars)`
260
+ : text);
261
+ return {
262
+ ...usage,
263
+ durationMs,
264
+ finishReason,
265
+ messageCount: messages.length,
266
+ toolCount: tools.length,
267
+ input: { role: last.role, content: cut(last.content) },
268
+ output: cut(message.content),
269
+ ...(message.tool_calls && message.tool_calls.length
270
+ ? {
271
+ toolCalls: message.tool_calls.map((tc) => ({
272
+ name: tc.function && tc.function.name,
273
+ args: cut(tc.function && tc.function.arguments)
274
+ }))
275
+ }
276
+ : {})
277
+ };
254
278
  }
255
279
 
256
280
  /**
@@ -457,7 +481,8 @@ class ChatGpt {
457
481
 
458
482
  const apiUrl = `${this._openAiEndpoint}/chat/completions${this._apiKey ? `?api-version=${encodeURIComponent(this._apiVersion)}` : ''}`;
459
483
 
460
- this._log('#GPT request', body);
484
+ // the whole prompt: verbose only (or replayed with an error)
485
+ this._logger.debug('#GPT request', body);
461
486
 
462
487
  const startTime = Date.now();
463
488
  const response = await this._fetch(apiUrl, {
@@ -508,11 +533,14 @@ class ChatGpt {
508
533
 
509
534
  const durationMs = Date.now() - startTime;
510
535
 
511
- this._log('#GPT response', { choice, responseData, durationMs });
536
+ this._logger.debug('#GPT response body', { responseData, durationMs });
537
+ this._logger.info('#GPT response', ChatGpt._summary(body, choice, llmUsage, durationMs));
512
538
 
513
539
  return { choice, usage: llmUsage };
514
540
  } catch (e) {
515
- this._logger.log('#GPT failed', {
541
+ // the caller logs the error once (LLMSession: the fallback message);
542
+ // the detail goes with it (replayed with the error)
543
+ this._logger.debug('#GPT failed', {
516
544
  message: e.message, body, responseText, responseData, status, statusText
517
545
  });
518
546
  throw e;
@@ -95,11 +95,7 @@ const DEFAULT_TEXT_THRESHOLD = 0.8;
95
95
  * @prop {number} stepCount
96
96
  */
97
97
 
98
- /**
99
- * @typedef {object} Logger
100
- * @prop {Function} log
101
- * @prop {Function} error
102
- */
98
+ /** @typedef {import('./log/types').LevelLogger} Logger */
103
99
 
104
100
  /**
105
101
  * Automated Conversation tests runner
package/src/LLM.js CHANGED
@@ -60,6 +60,7 @@ const LLMSession = require('./LLMSession');
60
60
  * @typedef {object} PrepocessedRuleData
61
61
  * @prop {Function} condition
62
62
  * @prop {PreprocessorOutput} rule
63
+ * @prop {string[]} [aiTags] - original rule definition (from the designer), for logs
63
64
  */
64
65
 
65
66
  /**
@@ -75,12 +76,24 @@ const LLMSession = require('./LLMSession');
75
76
  * @typedef {RuleScore & PreprocessedRule} RuleWithScore
76
77
  */
77
78
 
79
+ /**
80
+ * @typedef {object} EvaluatedRule
81
+ * @prop {string|number} [id]
82
+ * @prop {EvaluationRuleAction} [action]
83
+ * @prop {string[]} [aiTags]
84
+ * @prop {boolean} conditionPassed - false, when the rule was skipped by its condition
85
+ * @prop {number} score - 0, when the rule did not match at all
86
+ * @prop {boolean} matched - score reached the threshold
87
+ */
88
+
78
89
  /**
79
90
  * @typedef {object} EvaluationResult
80
91
  * @prop {string} action
81
92
  * @prop {boolean} discard
82
93
  * @prop {RuleWithScore[]} results
83
94
  * @prop {object} setState
95
+ * @prop {number} [threshold] - minimal score of a matching rule
96
+ * @prop {EvaluatedRule[]} [evaluated] - every rule and the decision made about it
84
97
  */
85
98
 
86
99
  /**
@@ -190,11 +203,7 @@ const LLMSession = require('./LLMSession');
190
203
  * @prop {LogPrompt} logPrompt
191
204
  */
192
205
 
193
- /**
194
- * @typedef {object} Logger
195
- * @prop {Function} log
196
- * @prop {Function} error
197
- */
206
+ /** @typedef {import('./log/types').LevelLogger} Logger */
198
207
 
199
208
  /**
200
209
  * @typedef {object} VectorSearchDocument
@@ -527,6 +536,8 @@ class LLM {
527
536
 
528
537
  return {
529
538
  ...rest,
539
+ // kept, so logs can show the rule as it was defined in the designer
540
+ aiTags,
530
541
  condition,
531
542
  rule
532
543
  };
@@ -559,14 +570,27 @@ class LLM {
559
570
  const setState = {};
560
571
 
561
572
  const sAct = Object.values(LLM.EVALUATION_ACTIONS);
573
+ const { threshold } = this._ai;
574
+
575
+ /** @type {EvaluatedRule[]} */
576
+ const evaluated = [];
562
577
 
563
578
  const results = rules
564
- .filter((rule) => rule.condition(req, res))
579
+ .filter((rule) => {
580
+ const conditionPassed = !!rule.condition(req, res);
581
+ if (!conditionPassed) {
582
+ evaluated.push(LLM._describeRule(rule, false, 0, threshold));
583
+ }
584
+ return conditionPassed;
585
+ })
565
586
  .map((rule) => {
566
587
  const matched = this._ai.matcher
567
588
  .matchText(text, rule.rule, nlpResult, state);
568
589
 
569
- if (!matched || matched.score < this._ai.threshold) {
590
+ const score = matched ? matched.score : 0;
591
+ evaluated.push(LLM._describeRule(rule, true, score, threshold));
592
+
593
+ if (!matched || matched.score < threshold) {
570
594
  return null;
571
595
  }
572
596
 
@@ -594,10 +618,51 @@ class LLM {
594
618
  setState,
595
619
  results,
596
620
  discard,
597
- action: discard ? null : topRankedAction
621
+ action: discard ? null : topRankedAction,
622
+ threshold,
623
+ evaluated
624
+ };
625
+ }
626
+
627
+ /**
628
+ * Rule as a plain object for logs (RegExp objects serialize as {})
629
+ *
630
+ * @param {PreprocessedRule} rule
631
+ * @param {boolean} conditionPassed
632
+ * @param {number} score
633
+ * @param {number} threshold
634
+ * @returns {EvaluatedRule}
635
+ */
636
+ static _describeRule (rule, conditionPassed, score, threshold) {
637
+ // @ts-ignore
638
+ const { id, action = null, aiTags } = rule;
639
+ return {
640
+ id,
641
+ action,
642
+ aiTags: aiTags || LLM._ruleTags(rule.rule),
643
+ conditionPassed,
644
+ score,
645
+ matched: conditionPassed && score >= threshold
598
646
  };
599
647
  }
600
648
 
649
+ /**
650
+ * Fallback for rules added without their definition
651
+ *
652
+ * @param {PreprocessorOutput} rule
653
+ * @returns {string[]}
654
+ */
655
+ static _ruleTags (rule) {
656
+ if (!rule) return [];
657
+ /** @type {any} */
658
+ const r = rule;
659
+ return [
660
+ ...(r.regexps || []).map((x) => `regexp:${x.r instanceof RegExp ? x.r.source : x.r}`),
661
+ ...(r.intents || []).map((i) => `intent:${i}`),
662
+ ...(r.entities || []).map((e) => `@${e.entity}${e.optional ? '?' : ''}`)
663
+ ];
664
+ }
665
+
601
666
  }
602
667
 
603
668
  module.exports = LLM;
@@ -39,11 +39,7 @@ const BASE64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{
39
39
  * @prop {RouterResolver} resolve
40
40
  */
41
41
 
42
- /**
43
- * @typedef {object} Logger
44
- * @prop {Function} log
45
- * @prop {Function} error
46
- */
42
+ /** @typedef {import('./log/types').LevelLogger} Logger */
47
43
 
48
44
  /**
49
45
  * @typedef {object} DispatcherOptions
package/src/LLMSession.js CHANGED
@@ -101,6 +101,7 @@ const stateData = require('./utils/stateData');
101
101
  /**
102
102
  * @callback ToolFnCallback
103
103
  * @param {{[key: string]: any}} input
104
+ * @param {LLMSession} [session] - call `session.stop()` to end the generation after the tool
104
105
  * @returns {string|Promise<string>}
105
106
  */
106
107
 
@@ -256,6 +257,8 @@ class LLMSession {
256
257
 
257
258
  /** @type {PossiblyAsyncContent|null} */
258
259
  this._fallbackMessage = null;
260
+
261
+ this._stopped = false;
259
262
  }
260
263
 
261
264
  /**
@@ -374,6 +377,15 @@ class LLMSession {
374
377
  .then(onFulfilled, onRejected);
375
378
  }
376
379
 
380
+ /**
381
+ * the preset (or the provider options), the session generates with by default
382
+ *
383
+ * @returns {string|object}
384
+ */
385
+ get preset () {
386
+ return this._preset;
387
+ }
388
+
377
389
  /**
378
390
  * @returns {ToolFunction[]}
379
391
  */
@@ -429,7 +441,7 @@ class LLMSession {
429
441
  } else if (errors.length) {
430
442
  // @ts-ignore
431
443
  const errs = errors.map((e) => e.error);
432
- this._llm.log.log('LLMSession failures', errs);
444
+ this._llm.log.debug('LLMSession failures', { errors: errs.map((e) => e.message) });
433
445
  throw new Error(errs.map((e) => e.message).join(', '));
434
446
  }
435
447
  }
@@ -903,6 +915,28 @@ class LLMSession {
903
915
  return this;
904
916
  }
905
917
 
918
+ /**
919
+ * Ends the generation right after the current round of tool calls - the model
920
+ * is not asked again and nothing generated in this session is sent.
921
+ *
922
+ * Meant to be called from within a tool, which takes over the conversation
923
+ * (e.g. makes a postback to an interaction). It's applied immediately
924
+ * (not queued), because the tool already runs inside a queued job.
925
+ *
926
+ * @returns {this}
927
+ */
928
+ stop () {
929
+ this._stopped = true;
930
+ return this;
931
+ }
932
+
933
+ /**
934
+ * @returns {boolean}
935
+ */
936
+ isStoppedSync () {
937
+ return this._stopped;
938
+ }
939
+
906
940
  /**
907
941
  * @returns {Promise<string>}
908
942
  */
@@ -1024,6 +1058,16 @@ class LLMSession {
1024
1058
  },
1025
1059
  ...results.filter((r) => !!r)
1026
1060
  );
1061
+ }
1062
+
1063
+ if (this._stopped) {
1064
+ // a tool took over the conversation - drop any text generated
1065
+ // alongside the tool calls and don't ask the model again
1066
+ result = { role: ROLE_ASSISTANT, content: null };
1067
+ break;
1068
+ }
1069
+
1070
+ if (toolCalls.length) {
1027
1071
  result = await this._llm.generate(this, providerOptions, logOptions);
1028
1072
  } else {
1029
1073
  // everything failed - force a final text answer without tools
@@ -1093,7 +1137,7 @@ class LLMSession {
1093
1137
  let args;
1094
1138
  try {
1095
1139
  args = JSON.parse(toolCall.args);
1096
- const fnResult = await Promise.resolve(tool.fn(args));
1140
+ const fnResult = await Promise.resolve(tool.fn(args, this));
1097
1141
 
1098
1142
  /**
1099
1143
  * {
@@ -1193,6 +1237,9 @@ class LLMSession {
1193
1237
  */
1194
1238
  send (quickReplies = undefined) {
1195
1239
  this._job(() => {
1240
+ if (this._stopped) {
1241
+ return;
1242
+ }
1196
1243
  const messages = this.messagesToSendSync();
1197
1244
  this._onSend(messages, quickReplies);
1198
1245
  }, true);