wingbot 3.76.4-alpha.1 → 3.76.4-alpha.11

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.1",
3
+ "version": "3.76.4-alpha.11",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -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
@@ -441,6 +441,7 @@ class ChatGpt {
441
441
 
442
442
  this._log('#GPT request', body);
443
443
 
444
+ const startTime = Date.now();
444
445
  const response = await this._fetch(apiUrl, {
445
446
  method: 'POST',
446
447
  headers: {
@@ -470,7 +471,9 @@ class ChatGpt {
470
471
 
471
472
  const [choice] = responseData.choices;
472
473
 
473
- this._log('#GPT response', { choice, responseData });
474
+ const durationMs = Date.now() - startTime;
475
+
476
+ this._log('#GPT response', { choice, responseData, durationMs });
474
477
 
475
478
  return choice;
476
479
  } catch (e) {
package/src/LLM.js CHANGED
@@ -112,6 +112,7 @@ const LLMSession = require('./LLMSession');
112
112
  /**
113
113
  * @typedef {object} LLMLogOptions
114
114
  * @prop {VectorSearchResult} [vectorSearchResult]
115
+ * @prop {ToolFunction[]} [tools]
115
116
  */
116
117
 
117
118
  /**
@@ -158,6 +159,7 @@ const LLMSession = require('./LLMSession');
158
159
  * @prop {LLMMessage[]} prompt
159
160
  * @prop {LLMMessage} result
160
161
  * @prop {VectorSearchResult} [vectorSearchResult]
162
+ * @prop {ToolFunction[]} [tools]
161
163
  */
162
164
 
163
165
  /**
@@ -424,7 +426,7 @@ class LLM {
424
426
  const opts = this.llmOptions(preset);
425
427
  const prompt = await session.toArray(true);
426
428
  const result = await this._provider.requestChat(prompt, opts, session.tools);
427
- this.logPrompt(prompt, result, logOptions.vectorSearchResult);
429
+ this.logPrompt(prompt, result, logOptions.vectorSearchResult, session.tools);
428
430
  return result;
429
431
  }
430
432
 
@@ -433,11 +435,12 @@ class LLM {
433
435
  * @param {LLMMessage[]} prompt
434
436
  * @param {LLMMessage} result
435
437
  * @param {VectorSearchResult} [vectorSearchResult]
438
+ * @param {ToolFunction[]} [tools]
436
439
  */
437
- logPrompt (prompt, result, vectorSearchResult) {
440
+ logPrompt (prompt, result, vectorSearchResult, tools) {
438
441
  this._lastResult = result;
439
442
  this._configuration.logger.logPrompt({
440
- prompt, result, vectorSearchResult
443
+ prompt, result, vectorSearchResult, tools
441
444
  });
442
445
  }
443
446
 
@@ -260,6 +260,7 @@ class LLMDispatcher {
260
260
  ...routing,
261
261
  actionList: [],
262
262
  actionListString: '',
263
+ structuredOutput: LLMRouter.structuredOutput(),
263
264
  byAction: new Map(),
264
265
  router
265
266
  };
package/src/LLMRouter.js CHANGED
@@ -66,7 +66,8 @@ const LLMType = require('./LLMType');
66
66
  */
67
67
 
68
68
  /**
69
- * @typedef {Pick<LLMRoutingCache, 'actionList'|'actionListString'|'byAction'>} LLMRoutingInput
69
+ * @typedef {Pick<LLMRoutingCache,
70
+ * 'actionList'|'actionListString'|'byAction'|'structuredOutput'>} LLMRoutingInput
70
71
  */
71
72
 
72
73
  /**
@@ -74,6 +75,7 @@ const LLMType = require('./LLMType');
74
75
  * @prop {boolean} [keepUserInInteractionsWithBounceAllowed]
75
76
  * @prop {string} actionListString
76
77
  * @prop {LLMAction[]} actionList
78
+ * @prop {object} structuredOutput - JSON schema for the routing response
77
79
  * @prop {ILLMRouter|null} router
78
80
  * @prop {Map<string, GlobalIntentResolved>} byAction
79
81
  * @prop {GlobalIntentResolved[]} routes
@@ -142,24 +144,31 @@ class LLMRouter {
142
144
  }
143
145
 
144
146
  /**
145
- * @type {object}
147
+ * Sentinel action returned when the user did not change the conversation
148
+ * context (or when the model can't decide). Not present in `byAction`.
149
+ *
150
+ * @type {string}
146
151
  */
147
- static _cachedStructuredOutput = null;
152
+ static NOT_CHANGED = '-not-changed-';
148
153
 
149
154
  /**
155
+ * Builds the JSON schema for the routing response. The `action` field is
156
+ * constrained to an enum of the routable actions plus the `-not-changed-`
157
+ * sentinel, so the model can only return a valid routing target.
158
+ *
159
+ * Called once per router config from `_prepareRoutingInput`; the result is
160
+ * stored on the routing cache, so it is not rebuilt per request.
150
161
  *
162
+ * @param {string[]} [actions] - routable action values (e.g. `/btm/dodavatel`)
151
163
  * @returns {object}
152
164
  */
153
- static structuredOutput () {
154
- if (LLMRouter._cachedStructuredOutput === null) {
155
- LLMRouter._cachedStructuredOutput = LLMType
156
- .object({
157
- action: LLMType.string()
158
- .description('recommended action')
159
- }, 'conversation_routing_information')
160
- .toJSON();
161
- }
162
- return LLMRouter._cachedStructuredOutput;
165
+ static structuredOutput (actions = []) {
166
+ return LLMType
167
+ .object({
168
+ action: LLMType.enum([LLMRouter.NOT_CHANGED, ...actions])
169
+ .description('recommended action')
170
+ }, 'conversation_routing_information')
171
+ .toJSON();
163
172
  }
164
173
 
165
174
  /**
@@ -183,7 +192,7 @@ class LLMRouter {
183
192
 
184
193
  let res;
185
194
  try {
186
- res = await session.generateStructured(LLMRouter.structuredOutput());
195
+ res = await session.generateStructured(routing.structuredOutput);
187
196
  } catch (e) {
188
197
  // Fail closed: any error in routing classification is treated
189
198
  // as "no context change" so the user's current flow continues.
@@ -315,6 +324,7 @@ class LLMRouter {
315
324
  : {}),
316
325
  actionListString,
317
326
  actionList,
327
+ structuredOutput: LLMRouter.structuredOutput(actionList.map((a) => a.action)),
318
328
  byAction,
319
329
  router,
320
330
  routes: resolvedLlm
@@ -367,6 +377,7 @@ class LLMRouter {
367
377
  : {
368
378
  actionListString: '',
369
379
  actionList: [],
380
+ structuredOutput: LLMRouter.structuredOutput(),
370
381
  byAction: new Map(),
371
382
  router: null,
372
383
  routes: []
package/src/LLMSession.js CHANGED
@@ -170,6 +170,10 @@ const stateData = require('./utils/stateData');
170
170
  * @prop {LLMCallPreset} [preset]
171
171
  */
172
172
 
173
+ // max number of consecutive tool-call rounds resolved within a single generate()
174
+ // before we force a tool-less final answer (guards against tool-call loops)
175
+ const MAX_TOOL_CALL_ROUNDS = 5;
176
+
173
177
  /**
174
178
  * @class LLMSession
175
179
  * @implements {PromiseLike<LLMMessage<any>>}
@@ -970,7 +974,11 @@ class LLMSession {
970
974
  async _generate (providerOptions = this._preset, logOptions = {}) {
971
975
  let result = await this._llm.generate(this, providerOptions, logOptions);
972
976
 
973
- if (result.toolCalls?.length) {
977
+ // the model may chain several rounds of tool calls before it produces
978
+ // a final text answer - keep resolving them until it stops (bounded)
979
+ let rounds = 0;
980
+ while (result.toolCalls?.length && rounds < MAX_TOOL_CALL_ROUNDS) {
981
+ rounds += 1;
974
982
  const toolCalls = [];
975
983
  const results = await Promise.all(
976
984
  result.toolCalls.map(async (tc) => {
@@ -992,27 +1000,54 @@ class LLMSession {
992
1000
  );
993
1001
  result = await this._llm.generate(this, providerOptions, logOptions);
994
1002
  } else {
995
- // everything failed
996
- /** @type {LLMCallPreset} */
997
- const overrideChoice = typeof providerOptions === 'string'
998
- ? {
999
- preset: providerOptions,
1000
- toolChoice: 'none'
1001
- }
1002
- : {
1003
- ...providerOptions,
1004
- toolChoice: 'none'
1005
- };
1006
- result = await this._llm.generate(this, overrideChoice, logOptions);
1003
+ // everything failed - force a final text answer without tools
1004
+ this._llm.log.error(
1005
+ `LLMSession: all ${result.toolCalls.length} tool call(s) failed in round ${rounds}, `
1006
+ + 'forcing a tool-less final answer',
1007
+ { toolCalls: result.toolCalls }
1008
+ );
1009
+ result = await this._generateWithoutTools(providerOptions, logOptions);
1010
+ break;
1007
1011
  }
1008
1012
  }
1009
1013
 
1014
+ // safety net: if the model is still requesting tools (e.g. it hit the
1015
+ // round limit), force one final tool-less generation so we never return
1016
+ // a tool-call message (content === null) to the send pipeline
1017
+ if (result.toolCalls?.length) {
1018
+ this._llm.log.error(
1019
+ `LLMSession: reached MAX_TOOL_CALL_ROUNDS (${MAX_TOOL_CALL_ROUNDS}), `
1020
+ + 'dropping pending tool calls and forcing a tool-less final answer',
1021
+ { toolCalls: result.toolCalls }
1022
+ );
1023
+ result = await this._generateWithoutTools(providerOptions, logOptions);
1024
+ }
1025
+
1010
1026
  this._generatedIndex = this._chat.length;
1011
1027
  this._chat.push(result);
1012
1028
 
1013
1029
  return result;
1014
1030
  }
1015
1031
 
1032
+ /**
1033
+ *
1034
+ * @param {LLMCallPreset} providerOptions
1035
+ * @param {LLMLogOptions} logOptions
1036
+ * @returns {Promise<LLMMessage<any>>}
1037
+ */
1038
+ _generateWithoutTools (providerOptions, logOptions) {
1039
+ const overrideChoice = typeof providerOptions === 'string'
1040
+ ? {
1041
+ preset: providerOptions,
1042
+ toolChoice: 'none'
1043
+ }
1044
+ : {
1045
+ ...providerOptions,
1046
+ toolChoice: 'none'
1047
+ };
1048
+ return this._llm.generate(this, overrideChoice, logOptions);
1049
+ }
1050
+
1016
1051
  /**
1017
1052
  *
1018
1053
  * @param {ToolCall} toolCall
@@ -1145,6 +1180,10 @@ class LLMSession {
1145
1180
  * @returns {LLMMessage[]}
1146
1181
  */
1147
1182
  static toMessages (result) {
1183
+ // tool-call / structured messages carry no text content - nothing to send
1184
+ if (typeof result.content !== 'string') {
1185
+ return [];
1186
+ }
1148
1187
  let filtered = result.content
1149
1188
  .replace(/\n\n\n+/g, '\n\n')
1150
1189
  .split(/\n\n+(?!\s*-)/g)
@@ -1,11 +0,0 @@
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
- }