wingbot 3.76.4-alpha.8 → 3.76.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.76.4-alpha.8",
3
+ "version": "3.76.4",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
package/src/Ai.js CHANGED
@@ -9,8 +9,7 @@ const { vars } = require('./utils/stateVariables');
9
9
  const { deepEqual } = require('./utils/deepMapTools');
10
10
  const systemEntities = require('./systemEntities');
11
11
  const CustomEntityDetectionModel = require('./wingbot/CustomEntityDetectionModel');
12
-
13
- let uq = 1;
12
+ const GlobalIntents = require('./GlobalIntents');
14
13
 
15
14
  /** @typedef {import('./AiMatching').Compare} Compare */
16
15
 
@@ -444,21 +443,16 @@ class Ai {
444
443
  const rules = this.matcher.preprocessRule(intents);
445
444
  const matcher = this._createIntentMatcher(rules, usedEntities);
446
445
  const entitiesSetState = Ai.ai.matcher.getSetStateForEntityRules(rules);
447
- const id = uq++;
448
446
 
449
- const resolver = {
450
- path,
451
- globalIntents: new Map([[id, {
452
- id,
453
- matcher,
454
- usedEntities,
455
- entitiesSetState,
456
- local: false,
457
- action: '/*',
458
- title,
459
- meta
460
- }]])
461
- };
447
+ const resolver = GlobalIntents.resolver(path, {
448
+ matcher,
449
+ usedEntities,
450
+ entitiesSetState,
451
+ local: false,
452
+ action: '/*',
453
+ title,
454
+ meta
455
+ });
462
456
 
463
457
  return resolver;
464
458
  }
@@ -488,12 +482,10 @@ class Ai {
488
482
  const rules = this.matcher.preprocessRule(intents);
489
483
  const matcher = this._createIntentMatcher(rules, usedEntities);
490
484
  const entitiesSetState = Ai.ai.matcher.getSetStateForEntityRules(rules);
491
- const id = uq++;
492
485
 
493
- const resolver = {
486
+ const resolver = GlobalIntents.resolver(
494
487
  path,
495
- globalIntents: new Map([[id, {
496
- id,
488
+ {
497
489
  matcher,
498
490
  usedEntities,
499
491
  entitiesSetState,
@@ -501,8 +493,8 @@ class Ai {
501
493
  action: '/*',
502
494
  title,
503
495
  meta: {}
504
- }]])
505
- };
496
+ }
497
+ );
506
498
 
507
499
  return resolver;
508
500
  }
@@ -865,13 +865,20 @@ class BuildRouter extends Router {
865
865
  _createBlockLlmRouter (block) {
866
866
  const { llmRouting } = block;
867
867
 
868
- if (!llmRouting || !llmRouting.prompt || !llmRouting.prompt.trim()) {
868
+ if (!llmRouting) {
869
869
  return null;
870
870
  }
871
871
 
872
- const routeResolver = LLMRouter.defaultRoute(
873
- async (resolvedData) => compileWithState(null, null, llmRouting.prompt, resolvedData)
874
- );
872
+ const routeResolver = llmRouting.prompt && llmRouting.prompt.trim()
873
+ ? LLMRouter.defaultRoute(
874
+ async (resolvedData) => compileWithState(
875
+ null,
876
+ null,
877
+ llmRouting.prompt,
878
+ resolvedData
879
+ )
880
+ )
881
+ : null;
875
882
  const options = llmRouting.bounce === 'keepAllow'
876
883
  ? { keepUserInInteractionsWithBounceAllowed: true }
877
884
  : {};
package/src/ChatGpt.js CHANGED
@@ -47,6 +47,7 @@ const LLM = require('./LLM');
47
47
  * @prop {ChatGPTModel} [model]
48
48
  * @prop {'none'|'low'|'medium'|'high'} [reasoningEffort]
49
49
  * @prop {number} [presencePenalty=0.0]
50
+ * @prop {number} [frequencyPenalty=0.0]
50
51
  * @prop {number} [requestTokens]
51
52
  * @prop {number} [tokensLimit]
52
53
  * @prop {number} [temperature=1.0]
@@ -209,6 +210,7 @@ class ChatGpt {
209
210
  requestTokens: null,
210
211
  tokensLimit: null,
211
212
  presencePenalty: 0.0, // -2.0-2.0
213
+ frequencyPenalty: 0.0, // -2.0-2.0
212
214
  temperature: 1.0,
213
215
  model: 'gpt-4o-mini',
214
216
  transcriptLength: -5,
@@ -305,6 +307,7 @@ class ChatGpt {
305
307
  tokensLimit,
306
308
  model,
307
309
  presencePenalty,
310
+ frequencyPenalty,
308
311
  temperature,
309
312
  reasoningEffort,
310
313
  functions = [],
@@ -403,7 +406,7 @@ class ChatGpt {
403
406
 
404
407
  body = {
405
408
  model,
406
- frequency_penalty: 0,
409
+ frequency_penalty: frequencyPenalty ?? 0,
407
410
  presence_penalty: presencePenalty,
408
411
  ...(requestTokens ? {
409
412
  max_completion_tokens: requestTokens
package/src/LLM.js CHANGED
@@ -107,6 +107,9 @@ const LLMSession = require('./LLMSession');
107
107
  * @prop {'low'|'medium'|'high'|string} [verbosity]
108
108
  * @prop {'text'|SimpleJsonSchema} [responseFormat]
109
109
  * @prop {number} [temperature]
110
+ * @prop {number} [presencePenalty]
111
+ * @prop {number} [frequencyPenalty]
112
+ * @prop {number} [maxToolCallRounds]
110
113
  */
111
114
 
112
115
  /**
@@ -157,10 +160,12 @@ const LLMSession = require('./LLMSession');
157
160
  /**
158
161
  * @typedef {object} PromptInfo
159
162
  * @prop {LLMMessage[]} prompt
160
- * @prop {LLMMessage} result
163
+ * @prop {LLMMessage|null} result
161
164
  * @prop {VectorSearchResult} [vectorSearchResult]
162
165
  * @prop {ToolFunction[]} [tools]
163
166
  * @prop {number} [durationMs]
167
+ * @prop {LLMCallOptions} [options]
168
+ * @prop {string} [error]
164
169
  */
165
170
 
166
171
  /**
@@ -427,24 +432,47 @@ class LLM {
427
432
  const opts = this.llmOptions(preset);
428
433
  const prompt = await session.toArray(true);
429
434
  const startTime = Date.now();
430
- const result = await this._provider.requestChat(prompt, opts, session.tools);
435
+ let result = null;
436
+ let error = null;
437
+ try {
438
+ result = await this._provider.requestChat(prompt, opts, session.tools);
439
+ } catch (e) {
440
+ error = e;
441
+ }
431
442
  const durationMs = Date.now() - startTime;
432
- this.logPrompt(prompt, result, logOptions.vectorSearchResult, session.tools, durationMs);
443
+ this.logPrompt(
444
+ prompt,
445
+ result,
446
+ logOptions.vectorSearchResult,
447
+ session.tools,
448
+ durationMs,
449
+ opts,
450
+ error
451
+ );
452
+ if (error) throw error;
433
453
  return result;
434
454
  }
435
455
 
436
456
  /**
437
457
  *
438
458
  * @param {LLMMessage[]} prompt
439
- * @param {LLMMessage} result
459
+ * @param {LLMMessage|null} result
440
460
  * @param {VectorSearchResult} [vectorSearchResult]
441
461
  * @param {ToolFunction[]} [tools]
442
462
  * @param {number} [durationMs]
463
+ * @param {LLMCallOptions} [options]
464
+ * @param {Error} [error]
443
465
  */
444
- logPrompt (prompt, result, vectorSearchResult, tools, durationMs) {
466
+ logPrompt (prompt, result, vectorSearchResult, tools, durationMs, options, error) {
445
467
  this._lastResult = result;
446
468
  this._configuration.logger.logPrompt({
447
- prompt, result, vectorSearchResult, tools, durationMs
469
+ prompt,
470
+ result,
471
+ vectorSearchResult,
472
+ tools,
473
+ durationMs,
474
+ ...(options != null && { options }),
475
+ ...(error != null && { error: error?.message || String(error) })
448
476
  });
449
477
  }
450
478
 
package/src/LLMSession.js CHANGED
@@ -175,6 +175,30 @@ const stateData = require('./utils/stateData');
175
175
  // before we force a tool-less final answer (guards against tool-call loops)
176
176
  const MAX_TOOL_CALL_ROUNDS = 5;
177
177
 
178
+ // LLMs occasionally emit the same answer twice in a row within a single message
179
+ // shorter texts are left alone - repeating them can be intentional
180
+ const MIN_REPEATED_LENGTH = 24;
181
+ const MAX_DEDUPLICATED_LENGTH = 8000;
182
+
183
+ const REPEATED_TEXT_RE = new RegExp(`^([\\s\\S]{${MIN_REPEATED_LENGTH},}?)(?:\\s*\\1)+$`);
184
+
185
+ /**
186
+ * Collapses text consisting purely of a repeated block ("hello. hello.") into a single occurrence
187
+ *
188
+ * @param {string} text
189
+ * @returns {string}
190
+ */
191
+ function deduplicateRepeatedText (text) {
192
+ const trimmed = text.trim();
193
+
194
+ if (trimmed.length < MIN_REPEATED_LENGTH * 2 || trimmed.length > MAX_DEDUPLICATED_LENGTH) {
195
+ return text;
196
+ }
197
+ const match = trimmed.match(REPEATED_TEXT_RE);
198
+
199
+ return match ? match[1] : text;
200
+ }
201
+
178
202
  /**
179
203
  * @class LLMSession
180
204
  * @implements {PromiseLike<LLMMessage<any>>}
@@ -973,12 +997,13 @@ class LLMSession {
973
997
  * @returns {Promise<LLMMessage<any>>}
974
998
  */
975
999
  async _generate (providerOptions = this._preset, logOptions = {}) {
1000
+ const { maxToolCallRounds = MAX_TOOL_CALL_ROUNDS } = this._llm.llmOptions(providerOptions);
976
1001
  let result = await this._llm.generate(this, providerOptions, logOptions);
977
1002
 
978
1003
  // the model may chain several rounds of tool calls before it produces
979
1004
  // a final text answer - keep resolving them until it stops (bounded)
980
1005
  let rounds = 0;
981
- while (result.toolCalls?.length && rounds < MAX_TOOL_CALL_ROUNDS) {
1006
+ while (result.toolCalls?.length && rounds < maxToolCallRounds) {
982
1007
  rounds += 1;
983
1008
  const toolCalls = [];
984
1009
  const results = await Promise.all(
@@ -1017,7 +1042,7 @@ class LLMSession {
1017
1042
  // a tool-call message (content === null) to the send pipeline
1018
1043
  if (result.toolCalls?.length) {
1019
1044
  this._llm.log.error(
1020
- `LLMSession: reached MAX_TOOL_CALL_ROUNDS (${MAX_TOOL_CALL_ROUNDS}), `
1045
+ `LLMSession: reached maxToolCallRounds (${maxToolCallRounds}), `
1021
1046
  + 'dropping pending tool calls and forcing a tool-less final answer',
1022
1047
  { toolCalls: result.toolCalls }
1023
1048
  );
@@ -1185,12 +1210,13 @@ class LLMSession {
1185
1210
  if (typeof result.content !== 'string') {
1186
1211
  return [];
1187
1212
  }
1188
- let filtered = result.content
1213
+ let filtered = deduplicateRepeatedText(result.content.trim())
1189
1214
  .replace(/\n\n\n+/g, '\n\n')
1190
1215
  .split(/\n\n+(?!\s*-)/g)
1191
- .map((t) => t.replace(/\s*\n\s+/g, '\n')
1192
- .trim())
1193
- .filter((t) => !!t);
1216
+ .map((t) => deduplicateRepeatedText(t.replace(/\s*\n\s+/g, '\n')
1217
+ .trim()))
1218
+ .filter((t, i, all) => !!t
1219
+ && !(t.length >= MIN_REPEATED_LENGTH && t === all[i - 1]));
1194
1220
 
1195
1221
  if (result.finishReason === 'length' && filtered.length <= 0) {
1196
1222
  filtered = filtered.slice(0, filtered.length - 1);
@@ -0,0 +1,85 @@
1
+ /*
2
+ * @author David Menger
3
+ */
4
+ 'use strict';
5
+
6
+ /**
7
+ * null = text+voice
8
+ * t = text only
9
+ * v = voice only
10
+ * s = ssml
11
+ *
12
+ * @typedef {"t"|"v"|"s"|null} Purpose
13
+ */
14
+
15
+ /**
16
+ * @typedef Translation
17
+ * @property {string|string[]} t - text alternatives
18
+ * @property {string | null} l - language
19
+ * @property {Purpose | Purpose[]} [p] - purposes
20
+ */
21
+
22
+ /**
23
+ * @typedef {Translation[] | Translation | string[] | string} Translations
24
+ */
25
+
26
+ /**
27
+ * @param {string | string[] | Translation | Translation[]} translations
28
+ * @returns {translations is Translation[]}
29
+ */
30
+ function isArrayOfObjects (translations) {
31
+ return Array.isArray(translations)
32
+ && typeof translations[0] === 'object'
33
+ && translations[0] !== null;
34
+ }
35
+
36
+ function isTextObjectEmpty (text) {
37
+ if (!text || !text.t) {
38
+ return true;
39
+ }
40
+ if (Array.isArray(text.t)) {
41
+ return !text.t.some((t) => !!t);
42
+ }
43
+ return false;
44
+ }
45
+
46
+ /**
47
+ * @param {Translations} translations
48
+ * @param {string} [lang]
49
+ * @param {boolean} [disableDefaulting] - it will try to find translation for other language
50
+ * @returns {null|string|string[]}
51
+ */
52
+ function getLanguageText (translations, lang = null, disableDefaulting = false) {
53
+ let foundText;
54
+
55
+ if (isArrayOfObjects(translations)) {
56
+ if (lang) {
57
+ // @ts-ignore
58
+ foundText = translations.find((t) => t.l === lang);
59
+ } else if (disableDefaulting) {
60
+ // @ts-ignore
61
+ [foundText] = translations;
62
+ }
63
+ if (isTextObjectEmpty(foundText) && !disableDefaulting) {
64
+ // @ts-ignore
65
+ foundText = translations.find((t) => !isTextObjectEmpty(t));
66
+ }
67
+ foundText = foundText ? foundText.t : null;
68
+ } else {
69
+ foundText = translations;
70
+ }
71
+ if (Array.isArray(foundText)) {
72
+ foundText = foundText.filter((f) => !!f);
73
+
74
+ if (foundText.length === 0) {
75
+ return '';
76
+ }
77
+ }
78
+ return foundText || '';
79
+ }
80
+
81
+ module.exports = {
82
+ getLanguageText,
83
+ isTextObjectEmpty,
84
+ isArrayOfObjects
85
+ };
@@ -6,6 +6,7 @@
6
6
  const hbs = require('./hbs');
7
7
  const stateData = require('../utils/stateData');
8
8
  const getCondition = require('../utils/getCondition');
9
+ const { isTextObjectEmpty, getLanguageText } = require('./languageText');
9
10
 
10
11
  const ASPECT_SQUARE = 'square';
11
12
  const ASPECT_HORISONTAL = 'horisontal';
@@ -20,10 +21,6 @@ const WEBVIEW_FULL = 'full';
20
21
  const WEBVIEW_TALL = 'tall';
21
22
  const WEBVIEW_COMPACT = 'compact';
22
23
 
23
- /**
24
- * @typedef {Translation[] | Translation | string[] | string} Translations
25
- */
26
-
27
24
  /**
28
25
  * @typedef {object} TextObject
29
26
  * @prop {string} t - text
@@ -31,76 +28,10 @@ const WEBVIEW_COMPACT = 'compact';
31
28
  * @prop {string|null} p - purpose
32
29
  */
33
30
 
34
- /**
35
- * null = text+voice
36
- * t = text only
37
- * v = voice only
38
- * s = ssml
39
- *
40
- * @typedef {"t"|"v"|"s"|null} Purpose
41
- */
42
-
43
- /**
44
- * @typedef Translation
45
- * @property {string|string[]} t - text alternatives
46
- * @property {string | null} l - language
47
- * @property {Purpose | Purpose[]} [p] - purposes
48
- */
49
-
50
- /**
51
- * @param {string | string[] | Translation | Translation[]} translations
52
- * @returns {translations is Translation[]}
53
- */
54
- function isArrayOfObjects (translations) {
55
- return Array.isArray(translations)
56
- && typeof translations[0] === 'object'
57
- && translations[0] !== null;
58
- }
59
-
60
- function isTextObjectEmpty (text) {
61
- if (!text || !text.t) {
62
- return true;
63
- }
64
- if (Array.isArray(text.t)) {
65
- return !text.t.some((t) => !!t);
66
- }
67
- return false;
68
- }
69
-
70
- /**
71
- * @param {Translations} translations
72
- * @param {string} [lang]
73
- * @param {boolean} [disableDefaulting] - it will try to find translation for other language
74
- * @returns {null|string|string[]}
75
- */
76
- function getLanguageText (translations, lang = null, disableDefaulting = false) {
77
- let foundText;
78
-
79
- if (isArrayOfObjects(translations)) {
80
- if (lang) {
81
- // @ts-ignore
82
- foundText = translations.find((t) => t.l === lang);
83
- } else if (disableDefaulting) {
84
- // @ts-ignore
85
- [foundText] = translations;
86
- }
87
- if (isTextObjectEmpty(foundText) && !disableDefaulting) {
88
- // @ts-ignore
89
- foundText = translations.find((t) => !isTextObjectEmpty(t));
90
- }
91
- foundText = foundText ? foundText.t : null;
92
- } else {
93
- foundText = translations;
94
- }
95
- if (Array.isArray(foundText)) {
96
- foundText = foundText.filter((f) => !!f);
31
+ /** @typedef {import('./languageText').Translation} Translation */
32
+ /** @typedef {import('./languageText').Translations} Translations */
33
+ /** @typedef {import('./languageText').Purpose} Purpose */
97
34
 
98
- if (foundText.length === 0) {
99
- return '';
100
- }
101
- }
102
- return foundText || '';
103
- }
104
35
  /**
105
36
  *
106
37
  * @param {Translations} translations
@@ -88,7 +88,8 @@ class PromptAssert {
88
88
  */
89
89
  _flatResults () {
90
90
  return this._prompts
91
- .map((prompt) => prompt.result);
91
+ .map((prompt) => prompt.result)
92
+ .filter(Boolean);
92
93
  }
93
94
 
94
95
  /**
@@ -190,7 +191,7 @@ class PromptAssert {
190
191
  return [
191
192
  `${i + 1}) ${PromptAssert.promptStats(p.prompt)}`,
192
193
  `${prompt.join('\n ')}`,
193
- `# ${PromptAssert._content(p.result)}`
194
+ `# ${p.result ? PromptAssert._content(p.result) : `[ERROR] ${p.error || 'unknown'}`}`
194
195
  ].join('\n ');
195
196
  });
196
197
  }
@@ -16,13 +16,19 @@ function extractText (payload) {
16
16
  return payload.message.text;
17
17
  }
18
18
 
19
- // button message
20
- if (payload.message && payload.message.attachment
21
- && payload.message.attachment.type === 'template'
22
- && payload.message.attachment.payload
23
- && payload.message.attachment.payload.text) {
24
-
25
- return payload.message.attachment.payload.text;
19
+ // button message — the orchestrator normalizes a singular `attachment` into
20
+ // a plural `attachments` array before persisting, so accept both shapes;
21
+ // otherwise template messages (e.g. button templates) logged via the
22
+ // orchestrator drop out of the transcript entirely.
23
+ const attachment = payload.message
24
+ && (payload.message.attachment
25
+ || (Array.isArray(payload.message.attachments) && payload.message.attachments[0]));
26
+ if (attachment
27
+ && attachment.type === 'template'
28
+ && attachment.payload
29
+ && attachment.payload.text) {
30
+
31
+ return attachment.payload.text;
26
32
  }
27
33
 
28
34
  if (!payload.postback) {
@@ -10,17 +10,41 @@ try {
10
10
  } catch (er) {
11
11
  handlebars = { compile: (text) => () => text };
12
12
  }
13
+ const { getLanguageText } = require('../resolvers/languageText');
13
14
  const stateData = require('./stateData');
14
15
 
16
+ /** @typedef {import('../resolvers/languageText').Translation} Translation */
17
+
15
18
  /** @typedef {import('../Request')} Request */
16
19
  /** @typedef {import('../Responder')} Responder */
17
20
 
21
+ function compileAndRenderOut (template, req, res, data) {
22
+ if (!template) {
23
+ return '';
24
+ }
25
+ if (typeof template !== 'string') {
26
+ return `${template}`;
27
+ }
28
+ if (!template.includes('{{')) {
29
+ return template;
30
+ }
31
+
32
+ const useData = data || stateData(req, res);
33
+ let result = template;
34
+ let hasHandlebars = true;
35
+ for (let i = 0; i < 10 && hasHandlebars; i++) {
36
+ result = handlebars.compile(result)(useData);
37
+ hasHandlebars = template.includes('{{');
38
+ }
39
+ return result;
40
+ }
41
+
18
42
  /**
19
43
  * Utility, which helps to render handlebars syntax with all variables within conversations state
20
44
  *
21
45
  * @param {Request} req
22
46
  * @param {Responder} res
23
- * @param {string} template
47
+ * @param {string|Translation[]} template
24
48
  * @param {object} forcedData
25
49
  * @returns {string}
26
50
  * @example
@@ -39,15 +63,15 @@ function compileWithState (req, res, template, forcedData = null) {
39
63
  if (!template) {
40
64
  return '';
41
65
  }
42
- if (typeof template !== 'string') {
43
- return `${template}`;
44
- }
45
- const data = forcedData || stateData(req, res);
46
- let result = template;
47
- for (let i = 0; i < 10 && result.includes('{{'); i++) {
48
- result = handlebars.compile(result)(data);
66
+ let tpl;
67
+ let data;
68
+ if (Array.isArray(template)) {
69
+ data = stateData(req, res);
70
+ tpl = getLanguageText(template, data.lang);
71
+ } else {
72
+ tpl = template;
49
73
  }
50
- return result;
74
+ return compileAndRenderOut(tpl, req, res, forcedData || data);
51
75
  }
52
76
 
53
77
  module.exports = compileWithState;