wingbot 3.74.0 → 3.74.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.
- package/.github/workflows/deploy.yml +74 -49
- package/index.js +7 -1
- package/package.json +6 -1
- package/src/ChatGpt.js +112 -35
- package/src/LLM.js +140 -48
- package/src/LLMConsts.js +42 -0
- package/src/LLMMockProvider.js +62 -1
- package/src/LLMSession.js +733 -90
- package/src/LLMTool.js +56 -0
- package/src/LLMType.js +331 -0
- package/src/Processor.js +6 -6
- package/src/Responder.js +56 -34
- package/src/ReturnSender.js +2 -2
- package/src/Router.js +7 -2
- package/src/resolvers/expectedInput.js +11 -2
- package/src/resolvers/message.js +2 -3
- package/.claude/settings.local.json +0 -12
package/src/LLMSession.js
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const {
|
|
7
|
+
FILTER_SCOPE_CONVERSATION, ROLE_ASSISTANT, ROLE_USER, ROLE_SYSTEM
|
|
8
|
+
} = require('./LLMConsts');
|
|
7
9
|
|
|
8
10
|
/** @typedef {import('./Responder').QuickReply} QuickReply */
|
|
9
|
-
/** @typedef {import('./LLM').
|
|
11
|
+
/** @typedef {import('./LLM').LLMCallPreset} LLMCallPreset */
|
|
12
|
+
/** @typedef {import('./LLM')} LLM */
|
|
10
13
|
/** @typedef {import('./LLM').LLMLogOptions} LLMLogOptions */
|
|
11
14
|
|
|
12
15
|
/** @typedef {'user'|'assistant'} LLMChatRole */
|
|
@@ -21,20 +24,42 @@ const LLM = require('./LLM');
|
|
|
21
24
|
/**
|
|
22
25
|
* @typedef {object} ToolCall
|
|
23
26
|
* @prop {string} id
|
|
27
|
+
* @prop {string|'function'} type
|
|
24
28
|
* @prop {string} name
|
|
25
29
|
* @prop {string} args - JSON string
|
|
26
30
|
*/
|
|
27
31
|
|
|
32
|
+
/** @typedef {string|Promise<string>} PossiblyAsyncContent */
|
|
33
|
+
|
|
28
34
|
/**
|
|
29
35
|
* @template {LLMRole} [R=LLMRole]
|
|
36
|
+
* @template {PossiblyAsyncContent} [C=string]
|
|
30
37
|
* @typedef {object} LLMMessage
|
|
31
38
|
* @prop {R} role
|
|
32
|
-
* @prop {
|
|
33
|
-
* @prop {string} [toolCallId]
|
|
39
|
+
* @prop {C} [content]
|
|
34
40
|
* @prop {LLMFinishReason} [finishReason]
|
|
35
41
|
* @prop {ToolCall[]} [toolCalls]
|
|
42
|
+
* @prop {string} [toolCallId]
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @template {LLMRole} [R=LLMRole]
|
|
47
|
+
* @typedef {LLMMessage<R, PossiblyAsyncContent>} PossiblyAsyncLLMMessage
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {Promise<LLMMessage<LLMRole,string>|LLMMessage<LLMRole,string>[]>} AsyncLLMMessage
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} FailedLLMAsync
|
|
56
|
+
* @prop {'error'|string} role
|
|
57
|
+
* @prop {Error} error
|
|
36
58
|
*/
|
|
37
59
|
|
|
60
|
+
/** @typedef {FailedLLMAsync|LLMMessage} SyncLLMSrc */
|
|
61
|
+
/** @typedef {FailedLLMAsync|AsyncLLMMessage|PossiblyAsyncLLMMessage} LLMMessageSrc */
|
|
62
|
+
|
|
38
63
|
/**
|
|
39
64
|
* @callback SendCallback
|
|
40
65
|
* @param {LLMMessage[]} messages
|
|
@@ -54,55 +79,313 @@ const LLM = require('./LLM');
|
|
|
54
79
|
* @prop {FilterScope} scope
|
|
55
80
|
*/
|
|
56
81
|
|
|
82
|
+
/**
|
|
83
|
+
* @typedef {object} JsonSchemaProp
|
|
84
|
+
* @prop {string} [name]
|
|
85
|
+
* @prop {string|'string'|'number'|'boolean'|'array'} type - The data type
|
|
86
|
+
* @prop {string} [description] - Description of the parameter
|
|
87
|
+
* @prop {string[]} [enum] - Allowed values for this parameter
|
|
88
|
+
* @prop {number} [minimum] - Minimum value for numeric parameters
|
|
89
|
+
* @prop {number} [maximum] - Maximum value for numeric parameters
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/** @typedef {{ [key: string]: JsonSchemaProp }} SimpleJsonSchema */
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @callback ToolFnCallback
|
|
96
|
+
* @param {{[key: string]: any}} input
|
|
97
|
+
* @returns {string|Promise<string>}
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @typedef {object} FnParamsObject
|
|
102
|
+
* @prop {string} [type] - Schema type ('object')
|
|
103
|
+
* @prop {string} [name] - Schema name (required for structured output)
|
|
104
|
+
* @prop {SimpleJsonSchema} properties - Parameter definitions
|
|
105
|
+
* @prop {string[]} [required] - Required parameters
|
|
106
|
+
* @prop {boolean} [additionalProperties]
|
|
107
|
+
*
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @typedef {object} ToolFunction
|
|
112
|
+
* @prop {string} name - The function name
|
|
113
|
+
* @prop {string} [description] - What the function does
|
|
114
|
+
* @prop {FnParamsObject} [parameters] - Parameter schema
|
|
115
|
+
* @prop {boolean} [strict]
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @typedef {object} ToolInputWithFactory
|
|
120
|
+
* @prop {string} [name] - The function name
|
|
121
|
+
* @prop {FnParamsObject|ParametersFactory} [parameters] - Parameter schema
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
/** @typedef {Omit<ToolFunction, 'parameters'|'name'> & ToolInputWithFactory} ToolFunctionInput */
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @typedef {object} ToolFnObject
|
|
128
|
+
* @prop {ToolFnCallback} fn
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/** @typedef {ToolFnCallback & ToolFunction} CallbackTool */
|
|
132
|
+
/** @typedef {ToolFnObject & ToolFunction} ObjectTool */
|
|
133
|
+
|
|
134
|
+
/** @typedef {CallbackTool | ObjectTool} Tool */
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @typedef {object} ParametersFactory
|
|
138
|
+
* @prop {Function} toJSON()
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
/** @typedef {Omit<Tool, 'parameters'> & Omit<ToolInputWithFactory, 'name'>} ToolInput */
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* CONSIDERATION:
|
|
145
|
+
* - spustit joby až na await, nebo hned? - na await je bezpečnější
|
|
146
|
+
* - joby si musí umět předat výsledek (což je dané jen implementací, nikoliv nutností)
|
|
147
|
+
*
|
|
148
|
+
* CONCEPT
|
|
149
|
+
* - asynchronní metody vracející this vždy přidávají job (job vrací jen LLM výsledek)
|
|
150
|
+
* - synchronní metody vracející this, kde záleží na pořadí volání,
|
|
151
|
+
* přidávají job s "runNowAndSyncWhenQueueIsEmpty=true" parametrem
|
|
152
|
+
* - asynchronní metody vracející data, by měly awaitovat this (aby byla data aktuální)
|
|
153
|
+
* - synchronní metody vracející data NEČEKAJÍ A MAJÍ V NÁZVU "Sync" s výjimkou:
|
|
154
|
+
* - toString
|
|
155
|
+
* - toJson
|
|
156
|
+
*/
|
|
157
|
+
|
|
57
158
|
/**
|
|
58
159
|
* @class LLMSession
|
|
160
|
+
* @implements {PromiseLike<LLMMessage<any>>}
|
|
59
161
|
*/
|
|
60
162
|
class LLMSession {
|
|
61
163
|
|
|
62
164
|
/**
|
|
63
165
|
*
|
|
64
166
|
* @param {LLM} llm
|
|
65
|
-
* @param {
|
|
167
|
+
* @param {(PossiblyAsyncLLMMessage|AsyncLLMMessage)[]} [chat]
|
|
66
168
|
* @param {SendCallback} [onSend]
|
|
67
169
|
* @param {LLMFilter[]} [filters=[]]
|
|
68
170
|
*/
|
|
69
|
-
constructor (llm, chat = [], onSend =
|
|
171
|
+
constructor (llm, chat = [], onSend = null, filters = []) {
|
|
70
172
|
this._llm = llm;
|
|
71
173
|
|
|
72
174
|
this._onSend = onSend;
|
|
73
175
|
|
|
74
|
-
|
|
75
|
-
this._chat = chat;
|
|
176
|
+
this._inExecution = false;
|
|
76
177
|
|
|
77
|
-
this.
|
|
178
|
+
this._jobQ = [];
|
|
179
|
+
|
|
180
|
+
this._worker = null;
|
|
78
181
|
|
|
79
|
-
|
|
182
|
+
/** @type {LLMMessageSrc[]} */
|
|
183
|
+
this._chat = [];
|
|
184
|
+
this.push(...chat);
|
|
185
|
+
|
|
186
|
+
this._generatedIndex = null;
|
|
80
187
|
|
|
81
188
|
/** @type {LLMFilter[]} */
|
|
82
189
|
this._filters = filters;
|
|
83
190
|
|
|
84
191
|
this._SCOPE_CONVERSATION_ROLES = [
|
|
85
|
-
|
|
192
|
+
ROLE_ASSISTANT, ROLE_USER
|
|
86
193
|
];
|
|
194
|
+
|
|
195
|
+
/** @type {Map<string,ObjectTool>} */
|
|
196
|
+
this._tools = new Map();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
_job (task, runNowAndSyncWhenQueueIsEmpty = false) {
|
|
200
|
+
if (runNowAndSyncWhenQueueIsEmpty && this._jobQ.length === 0) {
|
|
201
|
+
task(null);
|
|
202
|
+
} else {
|
|
203
|
+
// @todo remove stack
|
|
204
|
+
this._jobQ.push(task);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// IDEA: then === defered taskl
|
|
209
|
+
|
|
210
|
+
async _runWorker () {
|
|
211
|
+
let fail = null;
|
|
212
|
+
let res = null;
|
|
213
|
+
let lastWasAwait = false;
|
|
214
|
+
|
|
215
|
+
const isAwait = (job) => 'onDone' in job;
|
|
216
|
+
while (this._jobQ.some((job) => isAwait(job))) {
|
|
217
|
+
const job = this._jobQ.shift();
|
|
218
|
+
if (isAwait(job)) {
|
|
219
|
+
lastWasAwait = true;
|
|
220
|
+
if (fail) {
|
|
221
|
+
job.onDone(fail);
|
|
222
|
+
} else {
|
|
223
|
+
job.onDone(null, res);
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
if (lastWasAwait) {
|
|
227
|
+
lastWasAwait = false;
|
|
228
|
+
fail = null;
|
|
229
|
+
res = null;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
this._inExecution = true;
|
|
233
|
+
Error.stackTraceLimit = 50;
|
|
234
|
+
const result = await Promise.resolve(job(res));
|
|
235
|
+
if (typeof result !== 'undefined') {
|
|
236
|
+
res = result;
|
|
237
|
+
}
|
|
238
|
+
} catch (e) {
|
|
239
|
+
fail = e;
|
|
240
|
+
} finally {
|
|
241
|
+
this._inExecution = false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (fail) {
|
|
246
|
+
throw fail;
|
|
247
|
+
}
|
|
248
|
+
return res;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async _awaitIfNotNestedCall () {
|
|
252
|
+
if (this._inExecution) return;
|
|
253
|
+
await this;
|
|
87
254
|
}
|
|
88
255
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
256
|
+
/**
|
|
257
|
+
*
|
|
258
|
+
* @template TResult1
|
|
259
|
+
* @template TResult2
|
|
260
|
+
* @param {(value: any) => TResult1 | PromiseLike<TResult1>} [onFulfilled]
|
|
261
|
+
* @param {(reason: any) => TResult2 | PromiseLike<TResult2>} [onRejected]
|
|
262
|
+
* @returns {PromiseLike<TResult1 | TResult2>}
|
|
263
|
+
*/
|
|
264
|
+
then (onFulfilled, onRejected) {
|
|
265
|
+
let error;
|
|
266
|
+
let result = null;
|
|
267
|
+
this._jobQ.push({
|
|
268
|
+
onDone: (err, res) => {
|
|
269
|
+
if (err) {
|
|
270
|
+
error = err;
|
|
271
|
+
} else {
|
|
272
|
+
result = res;
|
|
273
|
+
}
|
|
94
274
|
}
|
|
95
|
-
return a.role === LLM.ROLE_SYSTEM ? -1 : 1;
|
|
96
275
|
});
|
|
276
|
+
if (this._worker === null) {
|
|
277
|
+
this._worker = this._runWorker()
|
|
278
|
+
.finally(() => {
|
|
279
|
+
this._worker = null;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return this._worker
|
|
283
|
+
.then(() => {
|
|
284
|
+
if (error) {
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
})
|
|
289
|
+
.then(onFulfilled, onRejected);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @returns {ToolFunction[]}
|
|
294
|
+
*/
|
|
295
|
+
get tools () {
|
|
296
|
+
return Array.from(this._tools.values())
|
|
297
|
+
.map(({
|
|
298
|
+
name,
|
|
299
|
+
description = null,
|
|
300
|
+
parameters = {},
|
|
301
|
+
strict = true
|
|
302
|
+
}) => ({
|
|
303
|
+
name,
|
|
304
|
+
...(description && { description }),
|
|
305
|
+
...(typeof strict === 'boolean' ? { strict } : {}),
|
|
306
|
+
parameters: {
|
|
307
|
+
type: 'object',
|
|
308
|
+
properties: {},
|
|
309
|
+
additionalProperties: false,
|
|
310
|
+
required: 'properties' in parameters
|
|
311
|
+
? Object.keys(parameters.properties)
|
|
312
|
+
: [],
|
|
313
|
+
...parameters
|
|
314
|
+
}
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @returns {Promise<LLMMessage[]>}
|
|
320
|
+
*/
|
|
321
|
+
async _resolveMessages () {
|
|
322
|
+
await Promise.all(
|
|
323
|
+
this._chat.map(async (msg) => {
|
|
324
|
+
if ('then' in msg && typeof msg.then === 'function') {
|
|
325
|
+
return msg;
|
|
326
|
+
}
|
|
327
|
+
if ('content' in msg && typeof msg.content !== 'string' && 'then' in msg.content) {
|
|
328
|
+
return msg.content;
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// @ts-ignore
|
|
335
|
+
return this._moveSystemToTop(this._chat);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
*
|
|
340
|
+
* @param {LLMMessageSrc[]} messages
|
|
341
|
+
*/
|
|
342
|
+
_throwAsyncError (messages = this._chat) {
|
|
343
|
+
const errors = messages.filter((c) => 'role' in c && c.role === 'error');
|
|
344
|
+
|
|
345
|
+
if (errors.length === 1) {
|
|
346
|
+
// @ts-ignore
|
|
347
|
+
throw errors[0].error;
|
|
348
|
+
} else if (errors.length) {
|
|
349
|
+
// @ts-ignore
|
|
350
|
+
const errs = errors.map((e) => e.error);
|
|
351
|
+
this._llm.log.log('LLMSession failures', errs);
|
|
352
|
+
throw new Error(errs.map((e) => e.message).join(', '));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
*
|
|
358
|
+
* @template {LLMMessageSrc} T
|
|
359
|
+
* @param {T[]} what
|
|
360
|
+
* @returns {T[]}
|
|
361
|
+
*/
|
|
362
|
+
_moveSystemToTop (what) {
|
|
363
|
+
let nextSystem = 0;
|
|
364
|
+
for (let i = 0; i < what.length; i++) {
|
|
365
|
+
const el = what[i];
|
|
366
|
+
const isSystem = 'role' in el && el.role === 'system';
|
|
367
|
+
if (isSystem && i > nextSystem) {
|
|
368
|
+
what.splice(i, 1);
|
|
369
|
+
what.splice(nextSystem, 0, el);
|
|
370
|
+
}
|
|
371
|
+
if (isSystem && i >= nextSystem) {
|
|
372
|
+
nextSystem++;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
97
375
|
return what;
|
|
98
376
|
}
|
|
99
377
|
|
|
100
|
-
|
|
378
|
+
/**
|
|
379
|
+
*
|
|
380
|
+
* @param {SyncLLMSrc[]} chat
|
|
381
|
+
* @returns {SyncLLMSrc[]}
|
|
382
|
+
*/
|
|
383
|
+
_mergeSystem (chat) {
|
|
101
384
|
/** @type {LLMMessage<any>[]} */
|
|
102
385
|
const sysMessages = [];
|
|
103
386
|
|
|
104
|
-
const otherMessages =
|
|
105
|
-
if (message.role !==
|
|
387
|
+
const otherMessages = chat.filter((message) => {
|
|
388
|
+
if (message.role !== ROLE_SYSTEM) {
|
|
106
389
|
return true;
|
|
107
390
|
}
|
|
108
391
|
sysMessages.push(message);
|
|
@@ -131,7 +414,7 @@ class LLMSession {
|
|
|
131
414
|
|
|
132
415
|
return [
|
|
133
416
|
{
|
|
134
|
-
role:
|
|
417
|
+
role: ROLE_SYSTEM, content
|
|
135
418
|
},
|
|
136
419
|
...otherMessages
|
|
137
420
|
];
|
|
@@ -140,83 +423,155 @@ class LLMSession {
|
|
|
140
423
|
/**
|
|
141
424
|
*
|
|
142
425
|
* @param {boolean} [filtered=false]
|
|
143
|
-
* @returns {
|
|
426
|
+
* @returns {SyncLLMSrc[]}
|
|
144
427
|
*/
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
428
|
+
toArraySync (filtered = false) {
|
|
429
|
+
this._moveSystemToTop(this._chat);
|
|
430
|
+
this._throwAsyncError(this._chat);
|
|
431
|
+
const sync = this._processSyncMessages(this._chat, filtered);
|
|
432
|
+
return this._mergeSystem(sync);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
*
|
|
437
|
+
* @param {LLMMessageSrc[]} messages
|
|
438
|
+
* @param {boolean} [filtered=false]
|
|
439
|
+
* @returns {SyncLLMSrc[]}
|
|
440
|
+
*/
|
|
441
|
+
_processSyncMessages (messages, filtered = false) {
|
|
442
|
+
/** @type {SyncLLMSrc[]} */
|
|
443
|
+
const ret = [];
|
|
444
|
+
|
|
445
|
+
messages.forEach((m) => {
|
|
446
|
+
if (!('role' in m)) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if ('content' in m
|
|
450
|
+
&& (m.content instanceof Promise
|
|
451
|
+
|| (typeof m.content !== 'string' && m.content && 'then' in m.content))) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (filtered && this._filters.length >= 0 && 'content' in m && typeof m.content === 'string') {
|
|
155
455
|
const content = this._filters.reduce((text, filter) => {
|
|
156
456
|
if (!text) {
|
|
157
457
|
return text;
|
|
158
458
|
}
|
|
159
|
-
if (filter.scope !==
|
|
160
|
-
&& (filter.scope !==
|
|
161
|
-
|| !this._SCOPE_CONVERSATION_ROLES.includes(
|
|
459
|
+
if (filter.scope !== m.role
|
|
460
|
+
&& (filter.scope !== FILTER_SCOPE_CONVERSATION
|
|
461
|
+
|| !this._SCOPE_CONVERSATION_ROLES.includes(m.role))) {
|
|
162
462
|
return text;
|
|
163
463
|
}
|
|
164
|
-
const res = filter.filter(text,
|
|
464
|
+
const res = filter.filter(text, m.role);
|
|
165
465
|
return res === true ? text : res;
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
|
|
466
|
+
}, m.content);
|
|
467
|
+
|
|
468
|
+
if (typeof content === 'string') {
|
|
469
|
+
// @ts-ignore
|
|
470
|
+
ret.push({
|
|
471
|
+
...m,
|
|
472
|
+
content
|
|
473
|
+
});
|
|
170
474
|
}
|
|
475
|
+
} else {
|
|
476
|
+
// @ts-ignore
|
|
477
|
+
ret.push(m);
|
|
478
|
+
}
|
|
171
479
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return ret;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
*
|
|
487
|
+
* @param {boolean} [filtered=false]
|
|
488
|
+
* @returns {Promise<LLMMessage[]>}
|
|
489
|
+
*/
|
|
490
|
+
async toArray (filtered = false) {
|
|
491
|
+
await this._awaitIfNotNestedCall();
|
|
492
|
+
const messages = await this._resolveMessages();
|
|
493
|
+
this._throwAsyncError(messages);
|
|
494
|
+
const sync = this._processSyncMessages(messages, filtered);
|
|
495
|
+
return this._mergeSystem(sync);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
*
|
|
500
|
+
* @param {PossiblyAsyncContent} content
|
|
501
|
+
* @returns {boolean}
|
|
502
|
+
*/
|
|
503
|
+
_contentIsPromise (content) {
|
|
504
|
+
return !!content && typeof content !== 'string';
|
|
178
505
|
}
|
|
179
506
|
|
|
180
507
|
/**
|
|
181
508
|
*
|
|
182
|
-
* @param {
|
|
509
|
+
* @param {PossiblyAsyncLLMMessage} m
|
|
183
510
|
* @returns {string}
|
|
184
511
|
*/
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return '>';
|
|
193
|
-
default:
|
|
194
|
-
return '*';
|
|
512
|
+
_contentToString (m) {
|
|
513
|
+
if (m.toolCalls) {
|
|
514
|
+
return `{ REQUESTED TOOLS:\n${m.toolCalls
|
|
515
|
+
.map((t) => ` .${t.name}(${t.args})`).join('\n')} }`;
|
|
516
|
+
}
|
|
517
|
+
if (!m.content) {
|
|
518
|
+
return '[-no-content-]';
|
|
195
519
|
}
|
|
520
|
+
if (typeof m.content === 'string') {
|
|
521
|
+
return m.content;
|
|
522
|
+
}
|
|
523
|
+
return '<Promise>';
|
|
196
524
|
}
|
|
197
525
|
|
|
198
526
|
/**
|
|
199
527
|
*
|
|
200
|
-
* @param {
|
|
528
|
+
* @param {LLMMessageSrc[]} [messages]
|
|
201
529
|
* @returns {string}
|
|
202
530
|
*/
|
|
203
531
|
toString (messages = this._chat) {
|
|
204
532
|
if (messages.length === 0) {
|
|
205
|
-
return '[<empty>]';
|
|
533
|
+
return '-[<empty>]';
|
|
206
534
|
}
|
|
207
535
|
return messages.map((m) => {
|
|
536
|
+
if ('then' in m) {
|
|
537
|
+
return '-{ <Promise> }';
|
|
538
|
+
}
|
|
539
|
+
if (!('role' in m)) {
|
|
540
|
+
return '-!- unknown message -!';
|
|
541
|
+
}
|
|
542
|
+
if ('error' in m) {
|
|
543
|
+
return `-<Error: ${m.error.message}>`;
|
|
544
|
+
}
|
|
208
545
|
switch (m.role) {
|
|
209
|
-
case
|
|
210
|
-
return
|
|
546
|
+
case ROLE_SYSTEM:
|
|
547
|
+
return `- -- system ---\n${m.content}\n--------------`;
|
|
211
548
|
default:
|
|
212
|
-
return `${this._msgPrefix(m)} ${m
|
|
549
|
+
return `${this._msgPrefix(m)} ${this._contentToString(m)}`;
|
|
213
550
|
}
|
|
214
551
|
})
|
|
215
552
|
.join('\n');
|
|
216
553
|
}
|
|
217
554
|
|
|
555
|
+
/**
|
|
556
|
+
*
|
|
557
|
+
* @param {PossiblyAsyncLLMMessage} msg
|
|
558
|
+
* @returns {string}
|
|
559
|
+
*/
|
|
560
|
+
_msgPrefix (msg) {
|
|
561
|
+
switch (msg.role) {
|
|
562
|
+
case ROLE_SYSTEM:
|
|
563
|
+
return '--';
|
|
564
|
+
case ROLE_ASSISTANT:
|
|
565
|
+
return msg.content ? '-<' : '-#';
|
|
566
|
+
case ROLE_USER:
|
|
567
|
+
return '->';
|
|
568
|
+
default:
|
|
569
|
+
return '-():';
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
218
573
|
toJSON () {
|
|
219
|
-
return this.
|
|
574
|
+
return this.toArraySync();
|
|
220
575
|
}
|
|
221
576
|
|
|
222
577
|
/**
|
|
@@ -225,35 +580,148 @@ class LLMSession {
|
|
|
225
580
|
* @returns {this}
|
|
226
581
|
*/
|
|
227
582
|
debug (needRaw = false) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
583
|
+
this._job(() => {
|
|
584
|
+
// eslint-disable-next-line no-console
|
|
585
|
+
console.log(`LLMSession#debug\n${this.toString(
|
|
586
|
+
needRaw
|
|
587
|
+
? this._chat
|
|
588
|
+
: this.toArraySync(false)
|
|
589
|
+
)}`);
|
|
590
|
+
}, true);
|
|
232
591
|
return this;
|
|
233
592
|
}
|
|
234
593
|
|
|
235
594
|
/**
|
|
236
595
|
*
|
|
237
|
-
* @param {
|
|
596
|
+
* @param {...(PossiblyAsyncLLMMessage|AsyncLLMMessage)} messages
|
|
238
597
|
* @returns {this}
|
|
239
598
|
*/
|
|
240
|
-
push (
|
|
241
|
-
|
|
242
|
-
this
|
|
599
|
+
push (...messages) {
|
|
600
|
+
this._job(() => this._pushNow(...messages), true);
|
|
601
|
+
return this;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
*
|
|
606
|
+
* @param {...(PossiblyAsyncLLMMessage|AsyncLLMMessage)} messages
|
|
607
|
+
* @returns {void}
|
|
608
|
+
*/
|
|
609
|
+
_pushNow (...messages) {
|
|
610
|
+
this._chat.push(...messages.map((msg) => {
|
|
611
|
+
if ('then' in msg && typeof msg.then === 'function') {
|
|
612
|
+
const wrapped = (async () => {
|
|
613
|
+
/** @type {SyncLLMSrc[]} */
|
|
614
|
+
let expand;
|
|
615
|
+
try {
|
|
616
|
+
const ret = await msg;
|
|
617
|
+
expand = Array.isArray(ret) ? ret : [ret];
|
|
618
|
+
} catch (e) {
|
|
619
|
+
expand = [{
|
|
620
|
+
role: 'error',
|
|
621
|
+
error: e
|
|
622
|
+
}];
|
|
623
|
+
}
|
|
624
|
+
const index = this._chat.indexOf(wrapped);
|
|
625
|
+
this._chat.splice(index, 1, ...expand);
|
|
626
|
+
return expand;
|
|
627
|
+
})();
|
|
628
|
+
return wrapped;
|
|
629
|
+
}
|
|
630
|
+
if (!('content' in msg) || !this._contentIsPromise(msg.content)) {
|
|
631
|
+
return msg;
|
|
632
|
+
}
|
|
633
|
+
const ret = {
|
|
634
|
+
...msg,
|
|
635
|
+
content: Promise.resolve(msg.content)
|
|
636
|
+
.then((r) => {
|
|
637
|
+
// @ts-ignore
|
|
638
|
+
ret.content = r;
|
|
639
|
+
return r;
|
|
640
|
+
})
|
|
641
|
+
.catch((e) => {
|
|
642
|
+
const index = this._chat.indexOf(ret);
|
|
643
|
+
this._chat.splice(index, 1, { role: 'error', error: e });
|
|
644
|
+
return null;
|
|
645
|
+
})
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
return ret;
|
|
649
|
+
}));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
*
|
|
654
|
+
* @param {...ToolInput} addedTools
|
|
655
|
+
* @returns {this}
|
|
656
|
+
*/
|
|
657
|
+
tool (...addedTools) {
|
|
658
|
+
addedTools.forEach((tool) => {
|
|
659
|
+
if (!tool.name) {
|
|
660
|
+
throw new Error(`Tool is missing .name: ${tool}`);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
this._job(() => {
|
|
664
|
+
for (const input of addedTools) {
|
|
665
|
+
// @ts-ignore
|
|
666
|
+
// eslint-disable-next-line prefer-const, object-curly-newline
|
|
667
|
+
let { fn, parameters = {}, name, ...rest } = input;
|
|
668
|
+
|
|
669
|
+
if (typeof input === 'function') {
|
|
670
|
+
fn = input;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if ('toJSON' in parameters && typeof parameters.toJSON === 'function') {
|
|
674
|
+
parameters = parameters.toJSON();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
this._tools.set(name, {
|
|
678
|
+
fn,
|
|
679
|
+
name,
|
|
680
|
+
// @ts-ignore
|
|
681
|
+
parameters,
|
|
682
|
+
...rest
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}, true);
|
|
686
|
+
return this;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
*
|
|
691
|
+
* @param {string|Promise<string>} content
|
|
692
|
+
* @returns {this}
|
|
693
|
+
*/
|
|
694
|
+
user (content) {
|
|
695
|
+
this.push({
|
|
696
|
+
role: ROLE_USER,
|
|
697
|
+
content
|
|
698
|
+
});
|
|
699
|
+
return this;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
*
|
|
704
|
+
* @param {string|Promise<string>} content
|
|
705
|
+
* @returns {this}
|
|
706
|
+
*/
|
|
707
|
+
assistant (content) {
|
|
708
|
+
this.push({
|
|
709
|
+
role: ROLE_ASSISTANT,
|
|
710
|
+
content
|
|
711
|
+
});
|
|
243
712
|
return this;
|
|
244
713
|
}
|
|
245
714
|
|
|
246
715
|
/**
|
|
247
716
|
*
|
|
248
|
-
* @param {string} content
|
|
717
|
+
* @param {string|Promise<string>} content
|
|
249
718
|
* @returns {this}
|
|
250
719
|
*/
|
|
251
720
|
systemPrompt (content) {
|
|
252
721
|
this.push({
|
|
253
|
-
role:
|
|
722
|
+
role: ROLE_SYSTEM,
|
|
254
723
|
content
|
|
255
724
|
});
|
|
256
|
-
this._sort();
|
|
257
725
|
return this;
|
|
258
726
|
}
|
|
259
727
|
|
|
@@ -263,22 +731,101 @@ class LLMSession {
|
|
|
263
731
|
* @returns {this}
|
|
264
732
|
*/
|
|
265
733
|
addFilter (filter) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
734
|
+
this._job(() => {
|
|
735
|
+
if (Array.isArray(filter)) {
|
|
736
|
+
this._filters.push(...filter);
|
|
737
|
+
} else {
|
|
738
|
+
this._filters.push(filter);
|
|
739
|
+
}
|
|
740
|
+
}, true);
|
|
741
|
+
return this;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
*
|
|
746
|
+
* @param {LLMCallPreset} [providerOptions]
|
|
747
|
+
* @param {LLMLogOptions} [logOptions]
|
|
748
|
+
* @returns {this}
|
|
749
|
+
*/
|
|
750
|
+
generate (providerOptions = undefined, logOptions = {}) {
|
|
751
|
+
this._job(() => this._generate(providerOptions, logOptions));
|
|
752
|
+
return this;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
*
|
|
757
|
+
* @param {FnParamsObject|ParametersFactory} output
|
|
758
|
+
* @param {LLMCallPreset} [providerOptions]
|
|
759
|
+
* @param {LLMLogOptions} [logOptions]
|
|
760
|
+
* @returns {this}
|
|
761
|
+
*/
|
|
762
|
+
generateStructured (output, providerOptions = undefined, logOptions = {}) {
|
|
763
|
+
|
|
764
|
+
const responseFormat = 'toJSON' in output && typeof output.toJSON === 'function'
|
|
765
|
+
? output.toJSON()
|
|
766
|
+
: output;
|
|
767
|
+
|
|
768
|
+
if (!responseFormat.name) {
|
|
769
|
+
throw new Error('Missing root object name for LLM structured output');
|
|
270
770
|
}
|
|
771
|
+
|
|
772
|
+
this._job(async () => {
|
|
773
|
+
const result = await this._generate({
|
|
774
|
+
...(typeof providerOptions === 'object' ? providerOptions : {}),
|
|
775
|
+
...(typeof providerOptions === 'string' ? { preset: providerOptions } : {}),
|
|
776
|
+
responseFormat
|
|
777
|
+
}, logOptions);
|
|
778
|
+
|
|
779
|
+
return JSON.parse(result.content);
|
|
780
|
+
});
|
|
271
781
|
return this;
|
|
272
782
|
}
|
|
273
783
|
|
|
274
784
|
/**
|
|
275
785
|
*
|
|
276
|
-
* @param {
|
|
786
|
+
* @param {LLMCallPreset} [providerOptions]
|
|
277
787
|
* @param {LLMLogOptions} [logOptions]
|
|
278
788
|
* @returns {Promise<LLMMessage<any>>}
|
|
279
789
|
*/
|
|
280
|
-
async
|
|
281
|
-
|
|
790
|
+
async _generate (providerOptions = undefined, logOptions = {}) {
|
|
791
|
+
let result = await this._llm.generate(this, providerOptions, logOptions);
|
|
792
|
+
|
|
793
|
+
if (result.toolCalls?.length) {
|
|
794
|
+
const toolCalls = [];
|
|
795
|
+
const results = await Promise.all(
|
|
796
|
+
result.toolCalls.map(async (tc) => {
|
|
797
|
+
const msg = await this._executeToolCall(tc);
|
|
798
|
+
if (msg) {
|
|
799
|
+
toolCalls.push(tc);
|
|
800
|
+
}
|
|
801
|
+
return msg;
|
|
802
|
+
})
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
if (toolCalls.length) {
|
|
806
|
+
this._pushNow(
|
|
807
|
+
{
|
|
808
|
+
role: ROLE_ASSISTANT,
|
|
809
|
+
toolCalls
|
|
810
|
+
},
|
|
811
|
+
...results.filter((r) => !!r)
|
|
812
|
+
);
|
|
813
|
+
result = await this._llm.generate(this, providerOptions, logOptions);
|
|
814
|
+
} else {
|
|
815
|
+
// everything failed
|
|
816
|
+
/** @type {LLMCallPreset} */
|
|
817
|
+
const overrideChoice = typeof providerOptions === 'string'
|
|
818
|
+
? {
|
|
819
|
+
preset: providerOptions,
|
|
820
|
+
toolChoice: 'none'
|
|
821
|
+
}
|
|
822
|
+
: {
|
|
823
|
+
...providerOptions,
|
|
824
|
+
toolChoice: 'none'
|
|
825
|
+
};
|
|
826
|
+
result = await this._llm.generate(this, overrideChoice, logOptions);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
282
829
|
|
|
283
830
|
this._generatedIndex = this._chat.length;
|
|
284
831
|
this._chat.push(result);
|
|
@@ -286,16 +833,76 @@ class LLMSession {
|
|
|
286
833
|
return result;
|
|
287
834
|
}
|
|
288
835
|
|
|
836
|
+
/**
|
|
837
|
+
*
|
|
838
|
+
* @param {ToolCall} toolCall
|
|
839
|
+
* @returns {Promise<LLMMessage>}
|
|
840
|
+
*/
|
|
841
|
+
async _executeToolCall (toolCall) {
|
|
842
|
+
|
|
843
|
+
const tool = this._tools.get(toolCall.name);
|
|
844
|
+
|
|
845
|
+
if (!tool) {
|
|
846
|
+
this._llm.log.error(`LLM tool "${toolCall.name}": NOT FOUND`, {
|
|
847
|
+
toolCall
|
|
848
|
+
});
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
let args;
|
|
853
|
+
try {
|
|
854
|
+
args = JSON.parse(toolCall.args);
|
|
855
|
+
const fnResult = await Promise.resolve(tool.fn(args));
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* {
|
|
859
|
+
"role": "assistant",
|
|
860
|
+
"tool_calls": [
|
|
861
|
+
{
|
|
862
|
+
"id": "call_123",
|
|
863
|
+
"type": "function",
|
|
864
|
+
"function": {
|
|
865
|
+
"name": "get_weather",
|
|
866
|
+
"arguments": "{\"city\": \"Prague\"}"
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
]
|
|
870
|
+
},
|
|
871
|
+
*/
|
|
872
|
+
return {
|
|
873
|
+
content: typeof fnResult === 'string' ? fnResult : JSON.stringify(fnResult),
|
|
874
|
+
role: 'tool',
|
|
875
|
+
toolCallId: toolCall.id
|
|
876
|
+
};
|
|
877
|
+
} catch (e) {
|
|
878
|
+
this._llm.log.error(`LLM tool ${toolCall.name}: ${e.message}`, e, {
|
|
879
|
+
args, toolCall
|
|
880
|
+
});
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
*
|
|
887
|
+
* @returns {Promise<string>}
|
|
888
|
+
*/
|
|
889
|
+
async lastResponse () {
|
|
890
|
+
await this._awaitIfNotNestedCall();
|
|
891
|
+
return this.lastResponseSync();
|
|
892
|
+
}
|
|
893
|
+
|
|
289
894
|
/**
|
|
290
895
|
*
|
|
291
896
|
* @returns {string}
|
|
292
897
|
*/
|
|
293
|
-
|
|
898
|
+
lastResponseSync () {
|
|
294
899
|
const messages = [];
|
|
295
900
|
for (let i = this._chat.length - 1; i >= 0; i--) {
|
|
296
901
|
const message = this._chat[i];
|
|
297
|
-
|
|
298
|
-
|
|
902
|
+
if (!('role' in message) || !('content' in message)) {
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
if (message.role !== ROLE_ASSISTANT || !message.content) {
|
|
299
906
|
break;
|
|
300
907
|
}
|
|
301
908
|
messages.unshift(message.content);
|
|
@@ -303,19 +910,31 @@ class LLMSession {
|
|
|
303
910
|
return messages.join('\n\n');
|
|
304
911
|
}
|
|
305
912
|
|
|
913
|
+
/**
|
|
914
|
+
*
|
|
915
|
+
* @param {boolean} [dontMarkAsSent=false]
|
|
916
|
+
* @returns {Promise<LLMMessage[]>}
|
|
917
|
+
*/
|
|
918
|
+
async messagesToSend (dontMarkAsSent = false) {
|
|
919
|
+
await this._awaitIfNotNestedCall();
|
|
920
|
+
return this.messagesToSendSync(dontMarkAsSent);
|
|
921
|
+
}
|
|
922
|
+
|
|
306
923
|
/**
|
|
307
924
|
*
|
|
308
925
|
* @param {boolean} [dontMarkAsSent=false]
|
|
309
926
|
* @returns {LLMMessage[]}
|
|
310
927
|
*/
|
|
311
|
-
|
|
928
|
+
messagesToSendSync (dontMarkAsSent = false) {
|
|
312
929
|
if (!this._generatedIndex) {
|
|
313
930
|
return [];
|
|
314
931
|
}
|
|
315
932
|
|
|
316
|
-
|
|
317
|
-
messages =
|
|
933
|
+
const allMessages = this._chat.splice(this._generatedIndex);
|
|
934
|
+
let messages = this._processSyncMessages(allMessages);
|
|
935
|
+
messages = messages.flatMap((msg) => LLMSession.toMessages(msg));
|
|
318
936
|
|
|
937
|
+
// probably issue - generated messages are now not in the chat
|
|
319
938
|
if (dontMarkAsSent) {
|
|
320
939
|
return messages;
|
|
321
940
|
}
|
|
@@ -332,13 +951,37 @@ class LLMSession {
|
|
|
332
951
|
* @returns {this}
|
|
333
952
|
*/
|
|
334
953
|
send (quickReplies = undefined) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
954
|
+
this._job(() => {
|
|
955
|
+
const messages = this.messagesToSendSync();
|
|
956
|
+
this._onSend(messages, quickReplies);
|
|
957
|
+
}, true);
|
|
338
958
|
|
|
339
959
|
return this;
|
|
340
960
|
}
|
|
341
961
|
|
|
962
|
+
/**
|
|
963
|
+
*
|
|
964
|
+
* @param {LLMMessage} result
|
|
965
|
+
* @returns {LLMMessage[]}
|
|
966
|
+
*/
|
|
967
|
+
static toMessages (result) {
|
|
968
|
+
let filtered = result.content
|
|
969
|
+
.replace(/\n\n\n+/g, '\n\n')
|
|
970
|
+
.split(/\n\n+(?!\s*-)/g)
|
|
971
|
+
.map((t) => t.replace(/\s*\n\s+/g, '\n')
|
|
972
|
+
.trim())
|
|
973
|
+
.filter((t) => !!t);
|
|
974
|
+
|
|
975
|
+
if (result.finishReason === 'length' && filtered.length <= 0) {
|
|
976
|
+
filtered = filtered.slice(0, filtered.length - 1);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return filtered.map((content) => ({
|
|
980
|
+
content,
|
|
981
|
+
role: result.role
|
|
982
|
+
}));
|
|
983
|
+
}
|
|
984
|
+
|
|
342
985
|
}
|
|
343
986
|
|
|
344
987
|
module.exports = LLMSession;
|