wingbot 3.76.3 → 3.76.4-alpha.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/LLMSession.js +109 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.76.3",
3
+ "version": "3.76.4-alpha.2",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
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>>}
@@ -224,6 +228,9 @@ class LLMSession {
224
228
  this._res = res || llm?.res;
225
229
 
226
230
  this._preset = preset;
231
+
232
+ /** @type {PossiblyAsyncContent|null} */
233
+ this._fallbackMessage = null;
227
234
  }
228
235
 
229
236
  /**
@@ -850,6 +857,38 @@ class LLMSession {
850
857
  return this;
851
858
  }
852
859
 
860
+ /**
861
+ * Sets a message to send to the user when a subsequent `generate()` call
862
+ * fails (e.g. a provider network timeout). Instead of rejecting - which
863
+ * would surface the raw error to the user - the failure is logged and this
864
+ * message is sent in place of the model's reply, so the chain resolves
865
+ * cleanly. If the message resolves to an empty value, the original error
866
+ * is rethrown (same as when no fallback is set).
867
+ *
868
+ * Only affects `generate()`. `generateStructured()` keeps using delegated
869
+ * errors (see {@link onDelegatedError}).
870
+ *
871
+ * @param {PossiblyAsyncContent} content
872
+ * @returns {this}
873
+ */
874
+ setFallbackMessage (content) {
875
+ this._job(() => {
876
+ this._fallbackMessage = content;
877
+ }, true);
878
+ return this;
879
+ }
880
+
881
+ /**
882
+ * @returns {Promise<string>}
883
+ */
884
+ async _resolveFallbackContent () {
885
+ const fallback = this._fallbackMessage;
886
+ const content = typeof fallback === 'function'
887
+ ? fallback(this._resolveData())
888
+ : fallback;
889
+ return Promise.resolve(content);
890
+ }
891
+
853
892
  /**
854
893
  *
855
894
  * @param {LLMCallPreset} [providerOptions]
@@ -857,7 +896,28 @@ class LLMSession {
857
896
  * @returns {this}
858
897
  */
859
898
  generate (providerOptions = this._preset, logOptions = {}) {
860
- this._job(() => this._generate(providerOptions, logOptions));
899
+ this._job(async () => {
900
+ try {
901
+ return await this._generate(providerOptions, logOptions);
902
+ } catch (e) {
903
+ if (this._fallbackMessage === null) {
904
+ throw e;
905
+ }
906
+
907
+ const content = await this._resolveFallbackContent();
908
+ if (!content) {
909
+ // no usable fallback message - propagate the original error
910
+ throw e;
911
+ }
912
+ this._llm.log.error(`LLMSession.generate failed, sending fallback message: ${e.message}`, e);
913
+
914
+ /** @type {LLMMessage} */
915
+ const result = { role: ROLE_ASSISTANT, content };
916
+ this._generatedIndex = this._chat.length;
917
+ this._chat.push(result);
918
+ return result;
919
+ }
920
+ });
861
921
  return this;
862
922
  }
863
923
 
@@ -914,7 +974,11 @@ class LLMSession {
914
974
  async _generate (providerOptions = this._preset, logOptions = {}) {
915
975
  let result = await this._llm.generate(this, providerOptions, logOptions);
916
976
 
917
- 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;
918
982
  const toolCalls = [];
919
983
  const results = await Promise.all(
920
984
  result.toolCalls.map(async (tc) => {
@@ -936,27 +1000,54 @@ class LLMSession {
936
1000
  );
937
1001
  result = await this._llm.generate(this, providerOptions, logOptions);
938
1002
  } else {
939
- // everything failed
940
- /** @type {LLMCallPreset} */
941
- const overrideChoice = typeof providerOptions === 'string'
942
- ? {
943
- preset: providerOptions,
944
- toolChoice: 'none'
945
- }
946
- : {
947
- ...providerOptions,
948
- toolChoice: 'none'
949
- };
950
- 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;
951
1011
  }
952
1012
  }
953
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
+
954
1026
  this._generatedIndex = this._chat.length;
955
1027
  this._chat.push(result);
956
1028
 
957
1029
  return result;
958
1030
  }
959
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
+
960
1051
  /**
961
1052
  *
962
1053
  * @param {ToolCall} toolCall
@@ -1089,6 +1180,10 @@ class LLMSession {
1089
1180
  * @returns {LLMMessage[]}
1090
1181
  */
1091
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
+ }
1092
1187
  let filtered = result.content
1093
1188
  .replace(/\n\n\n+/g, '\n\n')
1094
1189
  .split(/\n\n+(?!\s*-)/g)