wingbot 3.76.3 → 3.76.4-alpha.10
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 +2 -2
- package/src/ChatGpt.js +4 -1
- package/src/LLM.js +25 -3
- package/src/LLMDispatcher.js +1 -0
- package/src/LLMRouter.js +25 -14
- package/src/LLMSession.js +111 -14
- package/src/LLMType.js +21 -1
- package/src/graphApi/schema.gql +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wingbot",
|
|
3
|
-
"version": "3.76.
|
|
3
|
+
"version": "3.76.4-alpha.10",
|
|
4
4
|
"description": "Enterprise Messaging Bot Conversation Engine",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -76,4 +76,4 @@
|
|
|
76
76
|
"axios": "^1.6.4",
|
|
77
77
|
"handlebars": "^4.0.0"
|
|
78
78
|
}
|
|
79
|
-
}
|
|
79
|
+
}
|
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
|
@@ -107,11 +107,13 @@ 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} [maxToolCallRounds]
|
|
110
111
|
*/
|
|
111
112
|
|
|
112
113
|
/**
|
|
113
114
|
* @typedef {object} LLMLogOptions
|
|
114
115
|
* @prop {VectorSearchResult} [vectorSearchResult]
|
|
116
|
+
* @prop {ToolFunction[]} [tools]
|
|
115
117
|
*/
|
|
116
118
|
|
|
117
119
|
/**
|
|
@@ -158,6 +160,9 @@ const LLMSession = require('./LLMSession');
|
|
|
158
160
|
* @prop {LLMMessage[]} prompt
|
|
159
161
|
* @prop {LLMMessage} result
|
|
160
162
|
* @prop {VectorSearchResult} [vectorSearchResult]
|
|
163
|
+
* @prop {ToolFunction[]} [tools]
|
|
164
|
+
* @prop {number} [durationMs]
|
|
165
|
+
* @prop {string} [toolChoice]
|
|
161
166
|
*/
|
|
162
167
|
|
|
163
168
|
/**
|
|
@@ -423,8 +428,17 @@ class LLM {
|
|
|
423
428
|
async generate (session, preset = {}, logOptions = {}) {
|
|
424
429
|
const opts = this.llmOptions(preset);
|
|
425
430
|
const prompt = await session.toArray(true);
|
|
431
|
+
const startTime = Date.now();
|
|
426
432
|
const result = await this._provider.requestChat(prompt, opts, session.tools);
|
|
427
|
-
|
|
433
|
+
const durationMs = Date.now() - startTime;
|
|
434
|
+
this.logPrompt(
|
|
435
|
+
prompt,
|
|
436
|
+
result,
|
|
437
|
+
logOptions.vectorSearchResult,
|
|
438
|
+
session.tools,
|
|
439
|
+
durationMs,
|
|
440
|
+
opts.toolChoice
|
|
441
|
+
);
|
|
428
442
|
return result;
|
|
429
443
|
}
|
|
430
444
|
|
|
@@ -433,11 +447,19 @@ class LLM {
|
|
|
433
447
|
* @param {LLMMessage[]} prompt
|
|
434
448
|
* @param {LLMMessage} result
|
|
435
449
|
* @param {VectorSearchResult} [vectorSearchResult]
|
|
450
|
+
* @param {ToolFunction[]} [tools]
|
|
451
|
+
* @param {number} [durationMs]
|
|
452
|
+
* @param {string} [toolChoice]
|
|
436
453
|
*/
|
|
437
|
-
logPrompt (prompt, result, vectorSearchResult) {
|
|
454
|
+
logPrompt (prompt, result, vectorSearchResult, tools, durationMs, toolChoice) {
|
|
438
455
|
this._lastResult = result;
|
|
439
456
|
this._configuration.logger.logPrompt({
|
|
440
|
-
prompt,
|
|
457
|
+
prompt,
|
|
458
|
+
result,
|
|
459
|
+
vectorSearchResult,
|
|
460
|
+
tools,
|
|
461
|
+
durationMs,
|
|
462
|
+
...(toolChoice != null && { toolChoice })
|
|
441
463
|
});
|
|
442
464
|
}
|
|
443
465
|
|
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
|
@@ -93,6 +93,7 @@ const stateData = require('./utils/stateData');
|
|
|
93
93
|
* @prop {string[]} [enum] - Allowed values for this parameter
|
|
94
94
|
* @prop {number} [minimum] - Minimum value for numeric parameters
|
|
95
95
|
* @prop {number} [maximum] - Maximum value for numeric parameters
|
|
96
|
+
* @prop {string} [format] - JSON Schema string format, e.g. 'date-time', 'email', 'uuid'
|
|
96
97
|
*/
|
|
97
98
|
|
|
98
99
|
/** @typedef {{ [key: string]: JsonSchemaProp }} SimpleJsonSchema */
|
|
@@ -170,6 +171,10 @@ const stateData = require('./utils/stateData');
|
|
|
170
171
|
* @prop {LLMCallPreset} [preset]
|
|
171
172
|
*/
|
|
172
173
|
|
|
174
|
+
// max number of consecutive tool-call rounds resolved within a single generate()
|
|
175
|
+
// before we force a tool-less final answer (guards against tool-call loops)
|
|
176
|
+
const MAX_TOOL_CALL_ROUNDS = 5;
|
|
177
|
+
|
|
173
178
|
/**
|
|
174
179
|
* @class LLMSession
|
|
175
180
|
* @implements {PromiseLike<LLMMessage<any>>}
|
|
@@ -224,6 +229,9 @@ class LLMSession {
|
|
|
224
229
|
this._res = res || llm?.res;
|
|
225
230
|
|
|
226
231
|
this._preset = preset;
|
|
232
|
+
|
|
233
|
+
/** @type {PossiblyAsyncContent|null} */
|
|
234
|
+
this._fallbackMessage = null;
|
|
227
235
|
}
|
|
228
236
|
|
|
229
237
|
/**
|
|
@@ -850,6 +858,38 @@ class LLMSession {
|
|
|
850
858
|
return this;
|
|
851
859
|
}
|
|
852
860
|
|
|
861
|
+
/**
|
|
862
|
+
* Sets a message to send to the user when a subsequent `generate()` call
|
|
863
|
+
* fails (e.g. a provider network timeout). Instead of rejecting - which
|
|
864
|
+
* would surface the raw error to the user - the failure is logged and this
|
|
865
|
+
* message is sent in place of the model's reply, so the chain resolves
|
|
866
|
+
* cleanly. If the message resolves to an empty value, the original error
|
|
867
|
+
* is rethrown (same as when no fallback is set).
|
|
868
|
+
*
|
|
869
|
+
* Only affects `generate()`. `generateStructured()` keeps using delegated
|
|
870
|
+
* errors (see {@link onDelegatedError}).
|
|
871
|
+
*
|
|
872
|
+
* @param {PossiblyAsyncContent} content
|
|
873
|
+
* @returns {this}
|
|
874
|
+
*/
|
|
875
|
+
setFallbackMessage (content) {
|
|
876
|
+
this._job(() => {
|
|
877
|
+
this._fallbackMessage = content;
|
|
878
|
+
}, true);
|
|
879
|
+
return this;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* @returns {Promise<string>}
|
|
884
|
+
*/
|
|
885
|
+
async _resolveFallbackContent () {
|
|
886
|
+
const fallback = this._fallbackMessage;
|
|
887
|
+
const content = typeof fallback === 'function'
|
|
888
|
+
? fallback(this._resolveData())
|
|
889
|
+
: fallback;
|
|
890
|
+
return Promise.resolve(content);
|
|
891
|
+
}
|
|
892
|
+
|
|
853
893
|
/**
|
|
854
894
|
*
|
|
855
895
|
* @param {LLMCallPreset} [providerOptions]
|
|
@@ -857,7 +897,28 @@ class LLMSession {
|
|
|
857
897
|
* @returns {this}
|
|
858
898
|
*/
|
|
859
899
|
generate (providerOptions = this._preset, logOptions = {}) {
|
|
860
|
-
this._job(() =>
|
|
900
|
+
this._job(async () => {
|
|
901
|
+
try {
|
|
902
|
+
return await this._generate(providerOptions, logOptions);
|
|
903
|
+
} catch (e) {
|
|
904
|
+
if (this._fallbackMessage === null) {
|
|
905
|
+
throw e;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const content = await this._resolveFallbackContent();
|
|
909
|
+
if (!content) {
|
|
910
|
+
// no usable fallback message - propagate the original error
|
|
911
|
+
throw e;
|
|
912
|
+
}
|
|
913
|
+
this._llm.log.error(`LLMSession.generate failed, sending fallback message: ${e.message}`, e);
|
|
914
|
+
|
|
915
|
+
/** @type {LLMMessage} */
|
|
916
|
+
const result = { role: ROLE_ASSISTANT, content };
|
|
917
|
+
this._generatedIndex = this._chat.length;
|
|
918
|
+
this._chat.push(result);
|
|
919
|
+
return result;
|
|
920
|
+
}
|
|
921
|
+
});
|
|
861
922
|
return this;
|
|
862
923
|
}
|
|
863
924
|
|
|
@@ -912,9 +973,14 @@ class LLMSession {
|
|
|
912
973
|
* @returns {Promise<LLMMessage<any>>}
|
|
913
974
|
*/
|
|
914
975
|
async _generate (providerOptions = this._preset, logOptions = {}) {
|
|
976
|
+
const { maxToolCallRounds = MAX_TOOL_CALL_ROUNDS } = this._llm.llmOptions(providerOptions);
|
|
915
977
|
let result = await this._llm.generate(this, providerOptions, logOptions);
|
|
916
978
|
|
|
917
|
-
|
|
979
|
+
// the model may chain several rounds of tool calls before it produces
|
|
980
|
+
// a final text answer - keep resolving them until it stops (bounded)
|
|
981
|
+
let rounds = 0;
|
|
982
|
+
while (result.toolCalls?.length && rounds < maxToolCallRounds) {
|
|
983
|
+
rounds += 1;
|
|
918
984
|
const toolCalls = [];
|
|
919
985
|
const results = await Promise.all(
|
|
920
986
|
result.toolCalls.map(async (tc) => {
|
|
@@ -936,27 +1002,54 @@ class LLMSession {
|
|
|
936
1002
|
);
|
|
937
1003
|
result = await this._llm.generate(this, providerOptions, logOptions);
|
|
938
1004
|
} else {
|
|
939
|
-
// everything failed
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
...providerOptions,
|
|
948
|
-
toolChoice: 'none'
|
|
949
|
-
};
|
|
950
|
-
result = await this._llm.generate(this, overrideChoice, logOptions);
|
|
1005
|
+
// everything failed - force a final text answer without tools
|
|
1006
|
+
this._llm.log.error(
|
|
1007
|
+
`LLMSession: all ${result.toolCalls.length} tool call(s) failed in round ${rounds}, `
|
|
1008
|
+
+ 'forcing a tool-less final answer',
|
|
1009
|
+
{ toolCalls: result.toolCalls }
|
|
1010
|
+
);
|
|
1011
|
+
result = await this._generateWithoutTools(providerOptions, logOptions);
|
|
1012
|
+
break;
|
|
951
1013
|
}
|
|
952
1014
|
}
|
|
953
1015
|
|
|
1016
|
+
// safety net: if the model is still requesting tools (e.g. it hit the
|
|
1017
|
+
// round limit), force one final tool-less generation so we never return
|
|
1018
|
+
// a tool-call message (content === null) to the send pipeline
|
|
1019
|
+
if (result.toolCalls?.length) {
|
|
1020
|
+
this._llm.log.error(
|
|
1021
|
+
`LLMSession: reached maxToolCallRounds (${maxToolCallRounds}), `
|
|
1022
|
+
+ 'dropping pending tool calls and forcing a tool-less final answer',
|
|
1023
|
+
{ toolCalls: result.toolCalls }
|
|
1024
|
+
);
|
|
1025
|
+
result = await this._generateWithoutTools(providerOptions, logOptions);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
954
1028
|
this._generatedIndex = this._chat.length;
|
|
955
1029
|
this._chat.push(result);
|
|
956
1030
|
|
|
957
1031
|
return result;
|
|
958
1032
|
}
|
|
959
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
*
|
|
1036
|
+
* @param {LLMCallPreset} providerOptions
|
|
1037
|
+
* @param {LLMLogOptions} logOptions
|
|
1038
|
+
* @returns {Promise<LLMMessage<any>>}
|
|
1039
|
+
*/
|
|
1040
|
+
_generateWithoutTools (providerOptions, logOptions) {
|
|
1041
|
+
const overrideChoice = typeof providerOptions === 'string'
|
|
1042
|
+
? {
|
|
1043
|
+
preset: providerOptions,
|
|
1044
|
+
toolChoice: 'none'
|
|
1045
|
+
}
|
|
1046
|
+
: {
|
|
1047
|
+
...providerOptions,
|
|
1048
|
+
toolChoice: 'none'
|
|
1049
|
+
};
|
|
1050
|
+
return this._llm.generate(this, overrideChoice, logOptions);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
960
1053
|
/**
|
|
961
1054
|
*
|
|
962
1055
|
* @param {ToolCall} toolCall
|
|
@@ -1089,6 +1182,10 @@ class LLMSession {
|
|
|
1089
1182
|
* @returns {LLMMessage[]}
|
|
1090
1183
|
*/
|
|
1091
1184
|
static toMessages (result) {
|
|
1185
|
+
// tool-call / structured messages carry no text content - nothing to send
|
|
1186
|
+
if (typeof result.content !== 'string') {
|
|
1187
|
+
return [];
|
|
1188
|
+
}
|
|
1092
1189
|
let filtered = result.content
|
|
1093
1190
|
.replace(/\n\n\n+/g, '\n\n')
|
|
1094
1191
|
.split(/\n\n+(?!\s*-)/g)
|
package/src/LLMType.js
CHANGED
|
@@ -25,6 +25,7 @@ class LLMType {
|
|
|
25
25
|
this._name = name;
|
|
26
26
|
|
|
27
27
|
this._description = null;
|
|
28
|
+
this._format = null;
|
|
28
29
|
this._min = null;
|
|
29
30
|
this._max = null;
|
|
30
31
|
|
|
@@ -155,6 +156,24 @@ class LLMType {
|
|
|
155
156
|
return this;
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Sets the JSON Schema `format` for a string property.
|
|
161
|
+
*
|
|
162
|
+
* Supported values: `'date-time'`, `'time'`, `'date'`, `'duration'`,
|
|
163
|
+
* `'email'`, `'hostname'`, `'ipv4'`, `'ipv6'`, `'uuid'`.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* const createdAt = LLMType.string().format('date-time').description('Creation timestamp');
|
|
167
|
+
*
|
|
168
|
+
* @param {'date-time'|'time'|'date'|'duration'|'email'|
|
|
169
|
+
* 'hostname'|'ipv4'|'ipv6'|'uuid'|string} value
|
|
170
|
+
* @returns {this}
|
|
171
|
+
*/
|
|
172
|
+
format (value) {
|
|
173
|
+
this._format = value;
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
|
|
158
177
|
/**
|
|
159
178
|
* Sets minimum boundary and maps it to a type-specific JSON Schema keyword.
|
|
160
179
|
*
|
|
@@ -282,7 +301,8 @@ class LLMType {
|
|
|
282
301
|
_baseSchema () {
|
|
283
302
|
return /** @type {object} */ ({
|
|
284
303
|
...(this._description !== null && { description: this._description }),
|
|
285
|
-
...(this._name !== null && { name: this._name })
|
|
304
|
+
...(this._name !== null && { name: this._name }),
|
|
305
|
+
...(this._format !== null && { format: this._format })
|
|
286
306
|
});
|
|
287
307
|
}
|
|
288
308
|
|
package/src/graphApi/schema.gql
CHANGED
|
@@ -123,10 +123,18 @@ type VectorSearchResult {
|
|
|
123
123
|
resultDocuments: [VectorSearchDocument!]!
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
type ToolFunction {
|
|
127
|
+
name: String!
|
|
128
|
+
description: String
|
|
129
|
+
parameters: Any
|
|
130
|
+
}
|
|
131
|
+
|
|
126
132
|
type PromptInfo {
|
|
127
133
|
prompt: [LLMMessage!]!
|
|
128
134
|
result: LLMMessage!
|
|
129
135
|
vectorSearchResult: VectorSearchResult
|
|
136
|
+
tools: [ToolFunction!]
|
|
137
|
+
durationMs: Float
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
type UserInteraction {
|