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 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
  ];
@@ -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
- prompt: '',
127
- provider: 'ollama',
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
- _batchLock = getMutex;
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
- return await this._provider.gen({
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: this.state.format?.toJS(),
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
- return vectorToSqlLiteral(vectors);
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._batchLock.lock(this.id);
408
- this.quest.defer(() => this._batchLock.unlock(this.id));
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
- return res.map((v) => vectorToSqlLiteral(v));
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, sessionId = null) {
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 (!sessionId) {
518
+ if (question) {
439
519
  this.logic.chat(contextId, 'user', question);
440
- sessionId = Elf.uuid();
441
- this.log.dbg(`🐈 Chat session started : ${sessionId}`);
520
+ //add new question
521
+ messages.push({role: 'user', content: question});
442
522
  }
443
523
 
444
- const result = await this._provider.chat({
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
- return await this.chat(contextId, question, userDesktopId, sessionId);
544
+ //leave for a second turn
545
+ this.log.dbg(`🐈 Chat session ended : ${contextId}`);
546
+ return true;
461
547
  }
462
548
  }
463
549
 
464
- //keep agent response
465
- this.logic.chat(contextId, 'assistant', result.message.content);
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
- return result.message.content;
556
+ this.log.dbg(`🐈 Chat session ended : ${contextId}`);
557
+ return res;
468
558
  }
469
559
 
470
- async callTools(contextId, message, userDesktopId, sessionId) {
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
- ...JSON.parse(tool.function.arguments),
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} sid: ${sessionId}`);
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
 
@@ -49,7 +49,15 @@ class OllamaProvider extends LLMProvider {
49
49
  }
50
50
 
51
51
  async chat(chatRequest) {
52
- const {model, messages, options, format, tools, stream} = chatRequest;
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 {model, system, prompt, format, options, stream} = genRequest;
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 = 0) => {
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
- initialQuestion,
241
- questionsWithArticles,
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 maxTokensPerQuestion = Math.floor(
249
- (maxTotalTokens - buffer) / questionsWithArticles.length
250
- );
251
-
252
- for (const {articles} of questionsWithArticles) {
253
- let section = '';
254
- let sectionTokenCount = estimateTokenCount(section);
255
- for (const article of articles) {
256
- const {title, sourceId, description} = article;
257
- const articleBlock = `\n# ${title} \n## Lien: ${sourceId}\n\n${description}\n`;
258
- const tokensForArticle = estimateTokenCount(articleBlock);
259
-
260
- if (sectionTokenCount + tokensForArticle > maxTokensPerQuestion) {
261
- break;
262
- }
263
-
264
- section += articleBlock;
265
- sectionTokenCount += tokensForArticle;
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
- if (currentTokenCount + sectionTokenCount > maxTotalTokens - buffer) {
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
- prompt += section;
273
- currentTokenCount += sectionTokenCount;
290
+ sections.push(articleBlock);
291
+ sectionTokenCount += tokensForArticle;
274
292
  }
275
- const questionContext = '[QUESTION]\n\n' + initialQuestion;
276
- prompt += questionContext;
277
- return prompt.trim();
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(agent, allChunks, batchSize = 50) {
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goblin-agents",
3
- "version": "1.0.5",
3
+ "version": "2.1.1",
4
4
  "description": "AI Agents framework",
5
5
  "author": "Epsitec SA",
6
6
  "contributors": [