wingbot 3.73.15 → 3.74.1

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/src/LLMSession.js CHANGED
@@ -3,10 +3,13 @@
3
3
  */
4
4
  'use strict';
5
5
 
6
- const LLM = require('./LLM');
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').LLMProviderOptions} LLMProviderOptions */
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 {string} content
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 {LLMMessage<any>[]} [chat]
167
+ * @param {(PossiblyAsyncLLMMessage|AsyncLLMMessage)[]} [chat]
66
168
  * @param {SendCallback} [onSend]
67
169
  * @param {LLMFilter[]} [filters=[]]
68
170
  */
69
- constructor (llm, chat = [], onSend = () => {}, filters = []) {
171
+ constructor (llm, chat = [], onSend = null, filters = []) {
70
172
  this._llm = llm;
71
173
 
72
174
  this._onSend = onSend;
73
175
 
74
- /** @type {LLMMessage<any>[]} */
75
- this._chat = chat;
176
+ this._inExecution = false;
76
177
 
77
- this._generatedIndex = null;
178
+ this._jobQ = [];
179
+
180
+ this._worker = null;
78
181
 
79
- this._sort();
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
- LLM.ROLE_ASSISTANT, LLM.ROLE_USER
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
- _sort (what = this._chat) {
90
- what.sort((a, z) => {
91
- if (a.role === z.role
92
- || (a.role !== LLM.ROLE_SYSTEM && z.role !== LLM.ROLE_SYSTEM)) {
93
- return 0;
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
- _mergeSystem () {
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 = this._chat.filter((message) => {
105
- if (message.role !== LLM.ROLE_SYSTEM) {
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: LLM.ROLE_SYSTEM, content
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 {LLMMessage[]}
426
+ * @returns {SyncLLMSrc[]}
144
427
  */
145
- toArray (filtered = false) {
146
- const messages = this._mergeSystem();
147
- if (!filtered || this._filters.length === 0) {
148
- return messages;
149
- }
150
- return messages
151
- .map((message) => {
152
- if (!message.content) {
153
- return message;
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 !== message.role
160
- && (filter.scope !== LLM.FILTER_SCOPE_CONVERSATION
161
- || !this._SCOPE_CONVERSATION_ROLES.includes(message.role))) {
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, message.role);
464
+ const res = filter.filter(text, m.role);
165
465
  return res === true ? text : res;
166
- }, message.content);
167
-
168
- if (!content) {
169
- return null;
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
- return {
173
- ...message,
174
- content
175
- };
176
- })
177
- .filter((message) => message !== null);
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 {LLMMessage} msg
509
+ * @param {PossiblyAsyncLLMMessage} m
183
510
  * @returns {string}
184
511
  */
185
- _msgPrefix (msg) {
186
- switch (msg.role) {
187
- case LLM.ROLE_SYSTEM:
188
- return '-';
189
- case LLM.ROLE_ASSISTANT:
190
- return msg.content ? '<' : '#';
191
- case LLM.ROLE_USER:
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 {LLMMessage[]} [messages]
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 LLM.ROLE_SYSTEM:
210
- return `--- system ---\n${m.content}\n--------------`;
546
+ case ROLE_SYSTEM:
547
+ return `- -- system ---\n${m.content}\n--------------`;
211
548
  default:
212
- return `${this._msgPrefix(m)} ${m.content}`;
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.toArray();
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
- // eslint-disable-next-line no-console
229
- console.log('LLMSession#debug\n', this.toString(
230
- needRaw ? this._chat : this.toArray()
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 {LLMMessage} message
596
+ * @param {...(PossiblyAsyncLLMMessage|AsyncLLMMessage)} messages
238
597
  * @returns {this}
239
598
  */
240
- push (message) {
241
- // if its system, append it on top
242
- this._chat.push(message);
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: LLM.ROLE_SYSTEM,
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
- if (Array.isArray(filter)) {
267
- this._filters.push(...filter);
268
- } else {
269
- this._filters.push(filter);
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 {LLMProviderOptions} [providerOptions={}]
786
+ * @param {LLMCallPreset} [providerOptions]
277
787
  * @param {LLMLogOptions} [logOptions]
278
788
  * @returns {Promise<LLMMessage<any>>}
279
789
  */
280
- async generate (providerOptions = {}, logOptions = {}) {
281
- const result = await this._llm.generate(this, providerOptions, logOptions);
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
- lastResponse () {
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
- if (message.role !== LLM.ROLE_ASSISTANT || !message.content) {
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
- messagesToSend (dontMarkAsSent = false) {
928
+ messagesToSendSync (dontMarkAsSent = false) {
312
929
  if (!this._generatedIndex) {
313
930
  return [];
314
931
  }
315
932
 
316
- let messages = this._chat.splice(this._generatedIndex);
317
- messages = messages.flatMap((msg) => LLM.toMessages(msg));
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
- const messages = this.messagesToSend();
336
-
337
- this._onSend(messages, quickReplies);
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;