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 +1 -1
- package/src/BuildRouter.js +11 -4
- package/src/ChatGpt.js +4 -1
- package/src/LLM.js +6 -3
- package/src/LLMDispatcher.js +1 -0
- package/src/LLMRouter.js +25 -14
- package/src/LLMSession.js +52 -13
- package/.claude/settings.local.json +0 -11
package/package.json
CHANGED
package/src/BuildRouter.js
CHANGED
|
@@ -865,13 +865,20 @@ class BuildRouter extends Router {
|
|
|
865
865
|
_createBlockLlmRouter (block) {
|
|
866
866
|
const { llmRouting } = block;
|
|
867
867
|
|
|
868
|
-
if (!llmRouting
|
|
868
|
+
if (!llmRouting) {
|
|
869
869
|
return null;
|
|
870
870
|
}
|
|
871
871
|
|
|
872
|
-
const routeResolver =
|
|
873
|
-
|
|
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
|
-
|
|
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
|
|
package/src/LLMDispatcher.js
CHANGED
package/src/LLMRouter.js
CHANGED
|
@@ -66,7 +66,8 @@ const LLMType = require('./LLMType');
|
|
|
66
66
|
*/
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* @typedef {Pick<LLMRoutingCache,
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
-
}
|