goblin-agents 1.0.5 → 2.1.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/config.js +0 -24
- package/lib/llm/aiAgent.js +165 -34
- package/lib/llm/providers.js +22 -2
- package/lib/llm/utils.js +79 -33
- package/package.json +1 -1
package/config.js
CHANGED
|
@@ -7,28 +7,4 @@ module.exports = [
|
|
|
7
7
|
message: 'Agents version',
|
|
8
8
|
default: 2,
|
|
9
9
|
},
|
|
10
|
-
{
|
|
11
|
-
type: 'input',
|
|
12
|
-
name: 'defaultProfile',
|
|
13
|
-
message: 'Agents default profile',
|
|
14
|
-
default: null,
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
type: 'input',
|
|
18
|
-
name: 'defaultSettings',
|
|
19
|
-
message: 'Agents default setting',
|
|
20
|
-
default: null,
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
type: 'input',
|
|
24
|
-
name: 'profiles',
|
|
25
|
-
message: 'Agents profiles for overriding settings',
|
|
26
|
-
default: {},
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
type: 'input',
|
|
30
|
-
name: 'settings',
|
|
31
|
-
message: 'Settings by name',
|
|
32
|
-
default: {},
|
|
33
|
-
},
|
|
34
10
|
];
|
package/lib/llm/aiAgent.js
CHANGED
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
} = require('xcraft-core-stones');
|
|
14
14
|
const {getMutex} = require('xcraft-core-utils/lib/locks.js');
|
|
15
15
|
const LLMProvider = require('./providers.js');
|
|
16
|
-
const {OCR_SYSTEM_PROMPT, getPDFImages} = require('./utils.js');
|
|
16
|
+
const {OCR_SYSTEM_PROMPT, getPDFImages, splitThinkTags} = require('./utils.js');
|
|
17
17
|
|
|
18
18
|
class MessageShape {
|
|
19
19
|
role = enumeration('assistant', 'user', 'tool');
|
|
@@ -98,6 +98,48 @@ function vectorToSqlLiteral(vec) {
|
|
|
98
98
|
return hex;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Convertit un vecteur float32 normalisé [-1,1]
|
|
103
|
+
* en int8 [-128,127] et retourne une chaîne hex
|
|
104
|
+
* pour stockage en SQL.
|
|
105
|
+
*
|
|
106
|
+
* @param {Array|Float32Array} vec - vecteur normalisé
|
|
107
|
+
* @returns {string} - représentation hex
|
|
108
|
+
*/
|
|
109
|
+
function vectorToSqlLiteralInt8(vec) {
|
|
110
|
+
const dimensions = vec.length;
|
|
111
|
+
const qvec = new Int8Array(dimensions);
|
|
112
|
+
|
|
113
|
+
// Quantization uniforme
|
|
114
|
+
const step = 2.0 / 255.0;
|
|
115
|
+
for (let i = 0; i < dimensions; i++) {
|
|
116
|
+
qvec[i] = Math.round((vec[i] + 1.0) / step - 128);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Vue mémoire en octets non signés
|
|
120
|
+
const view = new Uint8Array(qvec.buffer, qvec.byteOffset, qvec.byteLength);
|
|
121
|
+
|
|
122
|
+
// Conversion en hex
|
|
123
|
+
let hex = '';
|
|
124
|
+
for (let i = 0; i < view.length; i++) {
|
|
125
|
+
const byte = view[i];
|
|
126
|
+
hex += byte < 16 ? '0' + byte.toString(16) : byte.toString(16);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return hex;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function cleanText(text) {
|
|
133
|
+
if (typeof text !== 'string') {
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
const {thinks, cleanText} = splitThinkTags(text);
|
|
137
|
+
for (const think of thinks) {
|
|
138
|
+
this.log.dbg(`🧐 ${think}`);
|
|
139
|
+
}
|
|
140
|
+
return cleanText;
|
|
141
|
+
}
|
|
142
|
+
|
|
101
143
|
class AiAgentShape {
|
|
102
144
|
id = id('aiAgent');
|
|
103
145
|
version = number;
|
|
@@ -114,6 +156,8 @@ class AiAgentShape {
|
|
|
114
156
|
tools = option(array(object));
|
|
115
157
|
toolServiceId = option(string);
|
|
116
158
|
usability = enumeration('disabled', 'experimental', 'stable', 'deprecated');
|
|
159
|
+
reasoning = option(boolean);
|
|
160
|
+
vectorQuality = option(enumeration('f32', 'int8'));
|
|
117
161
|
meta = MetaShape;
|
|
118
162
|
}
|
|
119
163
|
|
|
@@ -123,15 +167,17 @@ class AiAgentState extends Elf.Sculpt(AiAgentShape) {}
|
|
|
123
167
|
const defaultValues = {
|
|
124
168
|
name: 'agent',
|
|
125
169
|
role: 'assistant',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
model: 'mistral-small',
|
|
170
|
+
provider: 'open-ai',
|
|
171
|
+
model: '',
|
|
129
172
|
options: {},
|
|
130
173
|
host: 'http://127.0.0.1:11434',
|
|
131
174
|
headers: {},
|
|
132
175
|
format: null,
|
|
176
|
+
toolServiceId: null,
|
|
133
177
|
tools: null,
|
|
178
|
+
reasoning: false,
|
|
134
179
|
usability: 'disabled',
|
|
180
|
+
vectorQuality: 'f32',
|
|
135
181
|
};
|
|
136
182
|
|
|
137
183
|
class AiAgentLogic extends Elf.Archetype {
|
|
@@ -149,8 +195,10 @@ class AiAgentLogic extends Elf.Archetype {
|
|
|
149
195
|
host: defaultValues.host,
|
|
150
196
|
headers: defaultValues.headers,
|
|
151
197
|
format: defaultValues.format,
|
|
198
|
+
toolServiceId: defaultValues.toolServiceId,
|
|
152
199
|
tools: defaultValues.tools,
|
|
153
200
|
usability: defaultValues.usability,
|
|
201
|
+
vectorQuality: 'f32',
|
|
154
202
|
meta: {
|
|
155
203
|
status: 'published',
|
|
156
204
|
},
|
|
@@ -240,6 +288,11 @@ class AiAgentLogic extends Elf.Archetype {
|
|
|
240
288
|
state._state.set(path, newValue);
|
|
241
289
|
}
|
|
242
290
|
|
|
291
|
+
publish() {
|
|
292
|
+
const {state} = this;
|
|
293
|
+
state.meta.status = 'published';
|
|
294
|
+
}
|
|
295
|
+
|
|
243
296
|
trash() {
|
|
244
297
|
const {state} = this;
|
|
245
298
|
state.meta.status = 'trashed';
|
|
@@ -252,7 +305,7 @@ class AiAgent extends Elf {
|
|
|
252
305
|
|
|
253
306
|
_provider;
|
|
254
307
|
_conf;
|
|
255
|
-
|
|
308
|
+
_mutex = getMutex;
|
|
256
309
|
|
|
257
310
|
/**
|
|
258
311
|
* @param {string} id
|
|
@@ -300,7 +353,7 @@ class AiAgent extends Elf {
|
|
|
300
353
|
}
|
|
301
354
|
|
|
302
355
|
const Instance = LLMProvider.get(this.state.provider);
|
|
303
|
-
this._provider = new Instance(this.state.host, this.state.headers);
|
|
356
|
+
this._provider = new Instance(this.state.host, this.state.headers.toJS());
|
|
304
357
|
this._conf = {provider, host, headersStr};
|
|
305
358
|
}
|
|
306
359
|
|
|
@@ -326,20 +379,30 @@ class AiAgent extends Elf {
|
|
|
326
379
|
}
|
|
327
380
|
|
|
328
381
|
async gen(prompt) {
|
|
382
|
+
await this._mutex.lock(this.id);
|
|
383
|
+
this.quest.defer(() => this._mutex.unlock(this.id));
|
|
384
|
+
|
|
329
385
|
await this._loadProvider();
|
|
330
386
|
|
|
331
387
|
let options;
|
|
332
388
|
if (this.state.options) {
|
|
333
389
|
options = this.state.options.toJS();
|
|
334
390
|
}
|
|
335
|
-
|
|
391
|
+
const format = this.state.format
|
|
392
|
+
? typeof this.state.format === 'string'
|
|
393
|
+
? this.state.format
|
|
394
|
+
: this.state.format.toJS()
|
|
395
|
+
: undefined;
|
|
396
|
+
let res = await this._provider.gen({
|
|
336
397
|
model: this.state.model,
|
|
398
|
+
reasoning: this.state.reasoning,
|
|
337
399
|
prompt,
|
|
338
400
|
system: this.state.prompt,
|
|
339
|
-
format
|
|
401
|
+
format,
|
|
340
402
|
options,
|
|
341
403
|
stream: false,
|
|
342
404
|
});
|
|
405
|
+
return cleanText(res);
|
|
343
406
|
}
|
|
344
407
|
|
|
345
408
|
async reset(contextId, save = false) {
|
|
@@ -356,6 +419,9 @@ class AiAgent extends Elf {
|
|
|
356
419
|
}
|
|
357
420
|
|
|
358
421
|
async readPDFpages(pdfPath) {
|
|
422
|
+
await this._mutex.lock(this.id);
|
|
423
|
+
this.quest.defer(() => this._mutex.unlock(this.id));
|
|
424
|
+
|
|
359
425
|
await this._loadProvider();
|
|
360
426
|
|
|
361
427
|
const images = await getPDFImages(pdfPath);
|
|
@@ -380,6 +446,7 @@ class AiAgent extends Elf {
|
|
|
380
446
|
];
|
|
381
447
|
const result = await this._provider.chat({
|
|
382
448
|
model: this.state.model,
|
|
449
|
+
reasoning: this.state.reasoning,
|
|
383
450
|
messages,
|
|
384
451
|
});
|
|
385
452
|
descriptions.push(result.message.content);
|
|
@@ -391,12 +458,20 @@ class AiAgent extends Elf {
|
|
|
391
458
|
if (!rawText) {
|
|
392
459
|
return null;
|
|
393
460
|
}
|
|
461
|
+
|
|
462
|
+
await this._mutex.lock(this.id);
|
|
463
|
+
this.quest.defer(() => this._mutex.unlock(this.id));
|
|
464
|
+
|
|
394
465
|
await this._loadProvider();
|
|
395
466
|
const vectors = await this._provider.embed({
|
|
396
467
|
model: this.state.model,
|
|
397
468
|
input: rawText,
|
|
398
469
|
});
|
|
399
|
-
|
|
470
|
+
let vectorToSql = vectorToSqlLiteral;
|
|
471
|
+
if (this.state.vectorQuality === 'int8') {
|
|
472
|
+
vectorToSql = vectorToSqlLiteralInt8;
|
|
473
|
+
}
|
|
474
|
+
return vectorToSql(vectors);
|
|
400
475
|
}
|
|
401
476
|
|
|
402
477
|
async embedInBatch(rawTexts = []) {
|
|
@@ -404,30 +479,35 @@ class AiAgent extends Elf {
|
|
|
404
479
|
return [];
|
|
405
480
|
}
|
|
406
481
|
|
|
407
|
-
await this.
|
|
408
|
-
this.quest.defer(() => this.
|
|
482
|
+
await this._mutex.lock(this.id);
|
|
483
|
+
this.quest.defer(() => this._mutex.unlock(this.id));
|
|
409
484
|
|
|
410
485
|
await this._loadProvider();
|
|
411
486
|
const res = await this._provider.embedInBatch({
|
|
412
487
|
model: this.state.model,
|
|
413
488
|
input: rawTexts,
|
|
414
489
|
});
|
|
415
|
-
|
|
490
|
+
let vectorToSql = vectorToSqlLiteral;
|
|
491
|
+
if (this.state.vectorQuality === 'int8') {
|
|
492
|
+
vectorToSql = vectorToSqlLiteralInt8;
|
|
493
|
+
}
|
|
494
|
+
return res.map((v) => vectorToSql(v));
|
|
416
495
|
}
|
|
417
496
|
|
|
418
497
|
async set(contextId, messages) {
|
|
419
498
|
this.logic.set(contextId, messages);
|
|
420
499
|
}
|
|
421
500
|
|
|
422
|
-
async chat(contextId, question, userDesktopId
|
|
501
|
+
async chat(contextId, question, userDesktopId) {
|
|
502
|
+
await this._mutex.lock(this.id);
|
|
503
|
+
this.quest.defer(() => this._mutex.unlock(this.id));
|
|
504
|
+
|
|
423
505
|
await this._loadProvider();
|
|
424
506
|
const contextMessages = this.state.messages[contextId] || [];
|
|
425
507
|
const messages = [
|
|
426
508
|
{role: 'system', content: this.state.prompt},
|
|
427
509
|
...Array.from(contextMessages).map((m) => m._state.toJS()), //history
|
|
428
510
|
];
|
|
429
|
-
//add new question
|
|
430
|
-
messages.push({role: 'user', content: question});
|
|
431
511
|
|
|
432
512
|
let options;
|
|
433
513
|
if (this.state.options) {
|
|
@@ -435,39 +515,49 @@ class AiAgent extends Elf {
|
|
|
435
515
|
options = pState?.options;
|
|
436
516
|
}
|
|
437
517
|
|
|
438
|
-
if (
|
|
518
|
+
if (question) {
|
|
439
519
|
this.logic.chat(contextId, 'user', question);
|
|
440
|
-
|
|
441
|
-
|
|
520
|
+
//add new question
|
|
521
|
+
messages.push({role: 'user', content: question});
|
|
442
522
|
}
|
|
443
523
|
|
|
444
|
-
|
|
524
|
+
this.log.dbg(`🐈 Chat session started : ${contextId}`);
|
|
525
|
+
|
|
526
|
+
const chatCompletionRequest = {
|
|
445
527
|
model: this.state.model,
|
|
446
528
|
messages,
|
|
447
529
|
options,
|
|
448
530
|
format: this.state.format?.toJS(),
|
|
449
531
|
tools: this.state.tools?.toJS(),
|
|
450
|
-
|
|
532
|
+
reasoning: this.state.reasoning,
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const result = await this._provider.chat(chatCompletionRequest);
|
|
451
536
|
|
|
452
537
|
if (this.state.toolServiceId && this.state.tools) {
|
|
453
538
|
const used = await this.callTools(
|
|
454
539
|
contextId,
|
|
455
540
|
result.message,
|
|
456
|
-
userDesktopId
|
|
457
|
-
sessionId
|
|
541
|
+
userDesktopId
|
|
458
542
|
);
|
|
459
543
|
if (used) {
|
|
460
|
-
|
|
544
|
+
//leave for a second turn
|
|
545
|
+
this.log.dbg(`🐈 Chat session ended : ${contextId}`);
|
|
546
|
+
return true;
|
|
461
547
|
}
|
|
462
548
|
}
|
|
463
549
|
|
|
464
|
-
|
|
465
|
-
|
|
550
|
+
const res = cleanText(result.message.content);
|
|
551
|
+
if (typeof res === 'string') {
|
|
552
|
+
//keep agent response
|
|
553
|
+
this.logic.chat(contextId, 'assistant', cleanText);
|
|
554
|
+
}
|
|
466
555
|
|
|
467
|
-
|
|
556
|
+
this.log.dbg(`🐈 Chat session ended : ${contextId}`);
|
|
557
|
+
return res;
|
|
468
558
|
}
|
|
469
559
|
|
|
470
|
-
async callTools(contextId, message, userDesktopId
|
|
560
|
+
async callTools(contextId, message, userDesktopId) {
|
|
471
561
|
const {tool_calls} = message;
|
|
472
562
|
if (!tool_calls || !userDesktopId) {
|
|
473
563
|
return false;
|
|
@@ -478,24 +568,34 @@ class AiAgent extends Elf {
|
|
|
478
568
|
id,
|
|
479
569
|
function: {name},
|
|
480
570
|
} = tool;
|
|
571
|
+
|
|
481
572
|
let result = '';
|
|
573
|
+
let toolArguments = {};
|
|
574
|
+
if (
|
|
575
|
+
tool.function.arguments &&
|
|
576
|
+
typeof tool.function.arguments === 'string'
|
|
577
|
+
) {
|
|
578
|
+
toolArguments = JSON.parse(tool.function.arguments);
|
|
579
|
+
} else if (
|
|
580
|
+
tool.function.arguments &&
|
|
581
|
+
typeof tool.function.arguments === 'object'
|
|
582
|
+
) {
|
|
583
|
+
toolArguments = tool.function.arguments;
|
|
584
|
+
}
|
|
482
585
|
try {
|
|
483
586
|
const payload = {
|
|
484
587
|
id: this.state.toolServiceId,
|
|
485
588
|
desktopId: userDesktopId,
|
|
486
|
-
|
|
589
|
+
contextId,
|
|
590
|
+
...toolArguments,
|
|
487
591
|
};
|
|
488
592
|
const toolNamespace = this.state.toolServiceId?.split('@', 1)[0];
|
|
489
593
|
const toolCmd = `${toolNamespace}.${name}`;
|
|
490
|
-
this.log.dbg(
|
|
491
|
-
`🔧 Tool call: ${toolCmd}(${JSON.stringify(
|
|
492
|
-
payload
|
|
493
|
-
)}) sid: ${sessionId}`
|
|
494
|
-
);
|
|
594
|
+
this.log.dbg(`🔧 Tool call: ${toolCmd}(${JSON.stringify(payload)})`);
|
|
495
595
|
result = await this.quest.cmd(toolCmd, payload);
|
|
496
596
|
await this.addToolMessage(contextId, id, name, result);
|
|
497
597
|
} catch (err) {
|
|
498
|
-
this.log.dbg(`🔧 Tool error : ${err}
|
|
598
|
+
this.log.dbg(`🔧 Tool error : ${err}`);
|
|
499
599
|
}
|
|
500
600
|
}
|
|
501
601
|
return true;
|
|
@@ -509,6 +609,10 @@ class AiAgent extends Elf {
|
|
|
509
609
|
if (this.state.provider !== 'ollama') {
|
|
510
610
|
throw new Error('Streaming API not impl.');
|
|
511
611
|
}
|
|
612
|
+
|
|
613
|
+
await this._mutex.lock(this.id);
|
|
614
|
+
this.quest.defer(() => this._mutex.unlock(this.id));
|
|
615
|
+
|
|
512
616
|
await this._loadProvider();
|
|
513
617
|
|
|
514
618
|
const contextMessages = this.state.messages[contextId] || [];
|
|
@@ -527,6 +631,7 @@ class AiAgent extends Elf {
|
|
|
527
631
|
const response = await this._provider.chat({
|
|
528
632
|
model: this.state.model,
|
|
529
633
|
messages,
|
|
634
|
+
reasoning: this.state.reasoning,
|
|
530
635
|
options,
|
|
531
636
|
stream: true,
|
|
532
637
|
});
|
|
@@ -542,6 +647,9 @@ class AiAgent extends Elf {
|
|
|
542
647
|
}
|
|
543
648
|
|
|
544
649
|
async resumeExchange(resumePrompt, contextId) {
|
|
650
|
+
await this._mutex.lock(this.id);
|
|
651
|
+
this.quest.defer(() => this._mutex.unlock(this.id));
|
|
652
|
+
|
|
545
653
|
await this._loadProvider();
|
|
546
654
|
const contextMessages = this.state.messages[contextId] || [];
|
|
547
655
|
|
|
@@ -559,12 +667,16 @@ class AiAgent extends Elf {
|
|
|
559
667
|
model: this.state.model,
|
|
560
668
|
prompt,
|
|
561
669
|
system: resumePrompt,
|
|
670
|
+
reasoning: this.state.reasoning,
|
|
562
671
|
options,
|
|
563
672
|
stream: false,
|
|
564
673
|
});
|
|
565
674
|
}
|
|
566
675
|
|
|
567
676
|
async react(reactPrompt, contextId, question) {
|
|
677
|
+
await this._mutex.lock(this.id);
|
|
678
|
+
this.quest.defer(() => this._mutex.unlock(this.id));
|
|
679
|
+
|
|
568
680
|
await this._loadProvider();
|
|
569
681
|
const feedId = await this.newQuestFeed();
|
|
570
682
|
const contextMessages = this.state.messages[contextId] || [];
|
|
@@ -584,6 +696,7 @@ class AiAgent extends Elf {
|
|
|
584
696
|
model: this.state.model,
|
|
585
697
|
messages,
|
|
586
698
|
options,
|
|
699
|
+
reasoning: this.state.reasoning,
|
|
587
700
|
format: {
|
|
588
701
|
type: 'object',
|
|
589
702
|
properties: {
|
|
@@ -683,6 +796,19 @@ class AiAgent extends Elf {
|
|
|
683
796
|
];
|
|
684
797
|
}
|
|
685
798
|
|
|
799
|
+
async getToolChatContextHistory(contextId) {
|
|
800
|
+
const contextMessages = this.state.messages[contextId] || [];
|
|
801
|
+
return [
|
|
802
|
+
...Array.from(contextMessages)
|
|
803
|
+
.map((m) => m._state.toJS())
|
|
804
|
+
.filter((m) => {
|
|
805
|
+
if (m.role === 'tool' && !m.tool_calls) {
|
|
806
|
+
return m;
|
|
807
|
+
}
|
|
808
|
+
}),
|
|
809
|
+
];
|
|
810
|
+
}
|
|
811
|
+
|
|
686
812
|
async getBaseSettings() {
|
|
687
813
|
const {provider, host, headers, model} = this.state.toJS();
|
|
688
814
|
return {provider, host, headers, model};
|
|
@@ -693,6 +819,11 @@ class AiAgent extends Elf {
|
|
|
693
819
|
await this.persist();
|
|
694
820
|
}
|
|
695
821
|
|
|
822
|
+
async publish() {
|
|
823
|
+
this.logic.publish();
|
|
824
|
+
await this.persist();
|
|
825
|
+
}
|
|
826
|
+
|
|
696
827
|
delete() {}
|
|
697
828
|
}
|
|
698
829
|
|
package/lib/llm/providers.js
CHANGED
|
@@ -49,7 +49,15 @@ class OllamaProvider extends LLMProvider {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
async chat(chatRequest) {
|
|
52
|
-
const {
|
|
52
|
+
const {
|
|
53
|
+
model,
|
|
54
|
+
messages,
|
|
55
|
+
options,
|
|
56
|
+
format,
|
|
57
|
+
tools,
|
|
58
|
+
stream,
|
|
59
|
+
reasoning,
|
|
60
|
+
} = chatRequest;
|
|
53
61
|
const useFormat = !!format;
|
|
54
62
|
const chat = await this.#ollama.chat({
|
|
55
63
|
model,
|
|
@@ -58,6 +66,7 @@ class OllamaProvider extends LLMProvider {
|
|
|
58
66
|
format,
|
|
59
67
|
tools,
|
|
60
68
|
stream,
|
|
69
|
+
think: reasoning,
|
|
61
70
|
});
|
|
62
71
|
if (useFormat) {
|
|
63
72
|
chat.message.content = JSON.parse(chat.message.content);
|
|
@@ -68,7 +77,15 @@ class OllamaProvider extends LLMProvider {
|
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
async gen(genRequest) {
|
|
71
|
-
const {
|
|
80
|
+
const {
|
|
81
|
+
model,
|
|
82
|
+
system,
|
|
83
|
+
prompt,
|
|
84
|
+
format,
|
|
85
|
+
options,
|
|
86
|
+
stream,
|
|
87
|
+
reasoning,
|
|
88
|
+
} = genRequest;
|
|
72
89
|
const useFormat = !!format;
|
|
73
90
|
const gen = await this.#ollama.generate({
|
|
74
91
|
model,
|
|
@@ -77,6 +94,7 @@ class OllamaProvider extends LLMProvider {
|
|
|
77
94
|
format,
|
|
78
95
|
options,
|
|
79
96
|
stream,
|
|
97
|
+
think: reasoning,
|
|
80
98
|
});
|
|
81
99
|
if (useFormat) {
|
|
82
100
|
return JSON.parse(gen.response);
|
|
@@ -120,6 +138,7 @@ class OpenAIProvider extends LLMProvider {
|
|
|
120
138
|
}
|
|
121
139
|
|
|
122
140
|
async chat(chatRequest) {
|
|
141
|
+
delete chatRequest.reasoning; //not yet supported
|
|
123
142
|
if (chatRequest.options) {
|
|
124
143
|
chatRequest = {...chatRequest, ...chatRequest.options};
|
|
125
144
|
delete chatRequest.options;
|
|
@@ -145,6 +164,7 @@ class OpenAIProvider extends LLMProvider {
|
|
|
145
164
|
},
|
|
146
165
|
},
|
|
147
166
|
};
|
|
167
|
+
|
|
148
168
|
delete chatRequest.format;
|
|
149
169
|
}
|
|
150
170
|
const query = `${this.#baseUrl}/chat/completions`;
|
package/lib/llm/utils.js
CHANGED
|
@@ -90,7 +90,7 @@ const splitLongText = (text, maxLength, baseId) => {
|
|
|
90
90
|
return segments;
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
-
const html2chunks = async (html, chunkSize = 300, chunkOverlap =
|
|
93
|
+
const html2chunks = async (html, chunkSize = 300, chunkOverlap = 50) => {
|
|
94
94
|
const textSplitter = RecursiveCharacterTextSplitter.fromLanguage('html', {
|
|
95
95
|
chunkSize,
|
|
96
96
|
chunkOverlap,
|
|
@@ -236,45 +236,65 @@ function groupSentences(text, maxSize) {
|
|
|
236
236
|
return chunks;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Construit un prompt à partir d'une liste d'articles, avec gestion du format et de la limite de tokens.
|
|
241
|
+
*
|
|
242
|
+
* @param {ArticleShape[]} articles - Liste des articles sources utilisés pour générer le prompt.
|
|
243
|
+
* @param {"text"|"json"} [format="text"] - Format de sortie attendu :
|
|
244
|
+
* - `"text"` → prompt en texte brut
|
|
245
|
+
* - `"json"` → prompt structuré en JSON
|
|
246
|
+
* @param {number} [maxTotalTokens] - Nombre maximal de tokens autorisés dans le prompt.
|
|
247
|
+
* Si non défini, aucun contrôle n'est appliqué.
|
|
248
|
+
* @param {function} [dbg=() => null] - Fonction de débogage optionnelle appelée
|
|
249
|
+
* avec des informations intermédiaires (signature `(info: any) => void`).
|
|
250
|
+
* @returns {string} - Le prompt généré, soit en texte brut (string),
|
|
251
|
+
* soit en objet JSON stringifié selon le format choisi.
|
|
252
|
+
*/
|
|
239
253
|
function buildPromptFromArticlesMulti(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
maxTotalTokens
|
|
254
|
+
articles,
|
|
255
|
+
format = 'text',
|
|
256
|
+
maxTotalTokens,
|
|
257
|
+
dbg = () => null
|
|
243
258
|
) {
|
|
244
|
-
let prompt = '[CONTEXT]\n\n';
|
|
245
|
-
let currentTokenCount = estimateTokenCount(prompt);
|
|
246
|
-
|
|
247
259
|
const buffer = 16000; // Réserve de tokens pour l'instruction + réponse
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
for (const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
260
|
+
const maxTokens = Math.floor(maxTotalTokens - buffer);
|
|
261
|
+
|
|
262
|
+
let sections = [];
|
|
263
|
+
let sectionTokenCount = 0;
|
|
264
|
+
for (const article of articles) {
|
|
265
|
+
const {kind, title, sourceId, description, finalScore} = article;
|
|
266
|
+
|
|
267
|
+
let articleBlock = '';
|
|
268
|
+
if (format === 'json') {
|
|
269
|
+
articleBlock = JSON.stringify({
|
|
270
|
+
kind,
|
|
271
|
+
title,
|
|
272
|
+
url: sourceId.startsWith('http') ? sourceId : null,
|
|
273
|
+
finalScore,
|
|
274
|
+
description,
|
|
275
|
+
});
|
|
276
|
+
} else {
|
|
277
|
+
articleBlock += `# ${title}\n\n`;
|
|
278
|
+
articleBlock += `**score :** ${finalScore}\n\n`;
|
|
279
|
+
articleBlock += `\`\`\`html\n${description}\n\`\`\`\n\n`;
|
|
266
280
|
}
|
|
267
281
|
|
|
268
|
-
|
|
282
|
+
dbg(articleBlock);
|
|
283
|
+
const tokensForArticle = estimateTokenCount(articleBlock);
|
|
284
|
+
|
|
285
|
+
if (sectionTokenCount + tokensForArticle > maxTokens) {
|
|
286
|
+
dbg('MAX TOKEN REACHED');
|
|
269
287
|
break;
|
|
270
288
|
}
|
|
271
289
|
|
|
272
|
-
|
|
273
|
-
|
|
290
|
+
sections.push(articleBlock);
|
|
291
|
+
sectionTokenCount += tokensForArticle;
|
|
274
292
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
293
|
+
|
|
294
|
+
if (format === 'json') {
|
|
295
|
+
return `[${sections.join(',')}]`;
|
|
296
|
+
}
|
|
297
|
+
return sections.join('---\n\n');
|
|
278
298
|
}
|
|
279
299
|
|
|
280
300
|
/**
|
|
@@ -302,12 +322,17 @@ function chunkArray(array, size) {
|
|
|
302
322
|
* @returns {Promise<object>} A map of embeddings structured as:
|
|
303
323
|
* { [contentId]: { [key]: { chunk, embedding } } }
|
|
304
324
|
*/
|
|
305
|
-
async function embedChunksInBatch(
|
|
325
|
+
async function embedChunksInBatch(
|
|
326
|
+
agent,
|
|
327
|
+
allChunks,
|
|
328
|
+
batchSize = 50,
|
|
329
|
+
prefix = 'passage: '
|
|
330
|
+
) {
|
|
306
331
|
const embeddingsByContent = {};
|
|
307
332
|
|
|
308
333
|
for (let start = 0; start < allChunks.length; start += batchSize) {
|
|
309
334
|
const currentBatch = allChunks.slice(start, start + batchSize);
|
|
310
|
-
const inputTexts = currentBatch.map(({chunk}) => chunk);
|
|
335
|
+
const inputTexts = currentBatch.map(({chunk}) => `${prefix}${chunk}`);
|
|
311
336
|
const embeddedVectors = await agent.embedInBatch(inputTexts);
|
|
312
337
|
|
|
313
338
|
for (let i = 0; i < embeddedVectors.length; i++) {
|
|
@@ -328,6 +353,26 @@ async function embedChunksInBatch(agent, allChunks, batchSize = 50) {
|
|
|
328
353
|
return embeddingsByContent;
|
|
329
354
|
}
|
|
330
355
|
|
|
356
|
+
function splitThinkTags(input) {
|
|
357
|
+
const thinkRegex = /<think>([\s\S]*?)<\/think>/g;
|
|
358
|
+
let match;
|
|
359
|
+
const thinks = [];
|
|
360
|
+
let cleanText = input;
|
|
361
|
+
|
|
362
|
+
// Extraire toutes les sections <think>...</think>
|
|
363
|
+
while ((match = thinkRegex.exec(input)) !== null) {
|
|
364
|
+
thinks.push(match[1].trim());
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Supprimer les balises et leur contenu du texte
|
|
368
|
+
cleanText = cleanText.replace(thinkRegex, '').trim();
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
thinks, // Tableau avec le contenu des balises <think>
|
|
372
|
+
cleanText, // Texte sans balises <think>
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
331
376
|
module.exports = {
|
|
332
377
|
chunkArray,
|
|
333
378
|
embedChunksInBatch,
|
|
@@ -342,4 +387,5 @@ module.exports = {
|
|
|
342
387
|
sliceText,
|
|
343
388
|
groupSentences,
|
|
344
389
|
buildPromptFromArticlesMulti,
|
|
390
|
+
splitThinkTags,
|
|
345
391
|
};
|