wingbot 3.76.4-alpha.9 → 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.9",
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,8 @@ 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]
110
112
  * @prop {number} [maxToolCallRounds]
111
113
  */
112
114
 
@@ -158,10 +160,12 @@ const LLMSession = require('./LLMSession');
158
160
  /**
159
161
  * @typedef {object} PromptInfo
160
162
  * @prop {LLMMessage[]} prompt
161
- * @prop {LLMMessage} result
163
+ * @prop {LLMMessage|null} result
162
164
  * @prop {VectorSearchResult} [vectorSearchResult]
163
165
  * @prop {ToolFunction[]} [tools]
164
166
  * @prop {number} [durationMs]
167
+ * @prop {LLMCallOptions} [options]
168
+ * @prop {string} [error]
165
169
  */
166
170
 
167
171
  /**
@@ -428,24 +432,47 @@ class LLM {
428
432
  const opts = this.llmOptions(preset);
429
433
  const prompt = await session.toArray(true);
430
434
  const startTime = Date.now();
431
- 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
+ }
432
442
  const durationMs = Date.now() - startTime;
433
- 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;
434
453
  return result;
435
454
  }
436
455
 
437
456
  /**
438
457
  *
439
458
  * @param {LLMMessage[]} prompt
440
- * @param {LLMMessage} result
459
+ * @param {LLMMessage|null} result
441
460
  * @param {VectorSearchResult} [vectorSearchResult]
442
461
  * @param {ToolFunction[]} [tools]
443
462
  * @param {number} [durationMs]
463
+ * @param {LLMCallOptions} [options]
464
+ * @param {Error} [error]
444
465
  */
445
- logPrompt (prompt, result, vectorSearchResult, tools, durationMs) {
466
+ logPrompt (prompt, result, vectorSearchResult, tools, durationMs, options, error) {
446
467
  this._lastResult = result;
447
468
  this._configuration.logger.logPrompt({
448
- 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) })
449
476
  });
450
477
  }
451
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>>}
@@ -1186,12 +1210,13 @@ class LLMSession {
1186
1210
  if (typeof result.content !== 'string') {
1187
1211
  return [];
1188
1212
  }
1189
- let filtered = result.content
1213
+ let filtered = deduplicateRepeatedText(result.content.trim())
1190
1214
  .replace(/\n\n\n+/g, '\n\n')
1191
1215
  .split(/\n\n+(?!\s*-)/g)
1192
- .map((t) => t.replace(/\s*\n\s+/g, '\n')
1193
- .trim())
1194
- .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]));
1195
1220
 
1196
1221
  if (result.finishReason === 'length' && filtered.length <= 0) {
1197
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;