goblin-agents 2.1.0 → 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.
@@ -129,6 +129,17 @@ function vectorToSqlLiteralInt8(vec) {
129
129
  return hex;
130
130
  }
131
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
+
132
143
  class AiAgentShape {
133
144
  id = id('aiAgent');
134
145
  version = number;
@@ -294,7 +305,7 @@ class AiAgent extends Elf {
294
305
 
295
306
  _provider;
296
307
  _conf;
297
- _batchLock = getMutex;
308
+ _mutex = getMutex;
298
309
 
299
310
  /**
300
311
  * @param {string} id
@@ -368,26 +379,30 @@ class AiAgent extends Elf {
368
379
  }
369
380
 
370
381
  async gen(prompt) {
382
+ await this._mutex.lock(this.id);
383
+ this.quest.defer(() => this._mutex.unlock(this.id));
384
+
371
385
  await this._loadProvider();
372
386
 
373
387
  let options;
374
388
  if (this.state.options) {
375
389
  options = this.state.options.toJS();
376
390
  }
377
- const res = 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({
378
397
  model: this.state.model,
379
398
  reasoning: this.state.reasoning,
380
399
  prompt,
381
400
  system: this.state.prompt,
382
- format: this.state.format?.toJS(),
401
+ format,
383
402
  options,
384
403
  stream: false,
385
404
  });
386
- const {thinks, cleanText} = splitThinkTags(res);
387
- for (const think of thinks) {
388
- this.log.dbg(`🧐 ${think}`);
389
- }
390
- return cleanText;
405
+ return cleanText(res);
391
406
  }
392
407
 
393
408
  async reset(contextId, save = false) {
@@ -404,6 +419,9 @@ class AiAgent extends Elf {
404
419
  }
405
420
 
406
421
  async readPDFpages(pdfPath) {
422
+ await this._mutex.lock(this.id);
423
+ this.quest.defer(() => this._mutex.unlock(this.id));
424
+
407
425
  await this._loadProvider();
408
426
 
409
427
  const images = await getPDFImages(pdfPath);
@@ -440,6 +458,10 @@ class AiAgent extends Elf {
440
458
  if (!rawText) {
441
459
  return null;
442
460
  }
461
+
462
+ await this._mutex.lock(this.id);
463
+ this.quest.defer(() => this._mutex.unlock(this.id));
464
+
443
465
  await this._loadProvider();
444
466
  const vectors = await this._provider.embed({
445
467
  model: this.state.model,
@@ -457,8 +479,8 @@ class AiAgent extends Elf {
457
479
  return [];
458
480
  }
459
481
 
460
- await this._batchLock.lock(this.id);
461
- this.quest.defer(() => this._batchLock.unlock(this.id));
482
+ await this._mutex.lock(this.id);
483
+ this.quest.defer(() => this._mutex.unlock(this.id));
462
484
 
463
485
  await this._loadProvider();
464
486
  const res = await this._provider.embedInBatch({
@@ -477,6 +499,9 @@ class AiAgent extends Elf {
477
499
  }
478
500
 
479
501
  async chat(contextId, question, userDesktopId) {
502
+ await this._mutex.lock(this.id);
503
+ this.quest.defer(() => this._mutex.unlock(this.id));
504
+
480
505
  await this._loadProvider();
481
506
  const contextMessages = this.state.messages[contextId] || [];
482
507
  const messages = [
@@ -522,23 +547,22 @@ class AiAgent extends Elf {
522
547
  }
523
548
  }
524
549
 
525
- //keep agent response
526
- const {thinks, cleanText} = splitThinkTags(result.message.content);
527
- this.logic.chat(contextId, 'assistant', cleanText);
528
- for (const think of thinks) {
529
- this.log.dbg(`🧐 ${think}`);
550
+ const res = cleanText(result.message.content);
551
+ if (typeof res === 'string') {
552
+ //keep agent response
553
+ this.logic.chat(contextId, 'assistant', cleanText);
530
554
  }
531
- this.log.dbg(`🐈 Chat session ended : ${contextId}`);
532
555
 
533
- return cleanText;
556
+ this.log.dbg(`🐈 Chat session ended : ${contextId}`);
557
+ return res;
534
558
  }
535
559
 
536
560
  async callTools(contextId, message, userDesktopId) {
537
- const {tool_calls, content} = message;
561
+ const {tool_calls} = message;
538
562
  if (!tool_calls || !userDesktopId) {
539
563
  return false;
540
564
  }
541
- this.logic.callTools(contextId, content, tool_calls);
565
+ this.logic.callTools(contextId, tool_calls);
542
566
  for (const tool of tool_calls) {
543
567
  const {
544
568
  id,
@@ -585,6 +609,10 @@ class AiAgent extends Elf {
585
609
  if (this.state.provider !== 'ollama') {
586
610
  throw new Error('Streaming API not impl.');
587
611
  }
612
+
613
+ await this._mutex.lock(this.id);
614
+ this.quest.defer(() => this._mutex.unlock(this.id));
615
+
588
616
  await this._loadProvider();
589
617
 
590
618
  const contextMessages = this.state.messages[contextId] || [];
@@ -619,6 +647,9 @@ class AiAgent extends Elf {
619
647
  }
620
648
 
621
649
  async resumeExchange(resumePrompt, contextId) {
650
+ await this._mutex.lock(this.id);
651
+ this.quest.defer(() => this._mutex.unlock(this.id));
652
+
622
653
  await this._loadProvider();
623
654
  const contextMessages = this.state.messages[contextId] || [];
624
655
 
@@ -643,6 +674,9 @@ class AiAgent extends Elf {
643
674
  }
644
675
 
645
676
  async react(reactPrompt, contextId, question) {
677
+ await this._mutex.lock(this.id);
678
+ this.quest.defer(() => this._mutex.unlock(this.id));
679
+
646
680
  await this._loadProvider();
647
681
  const feedId = await this.newQuestFeed();
648
682
  const contextMessages = this.state.messages[contextId] || [];
package/lib/llm/utils.js CHANGED
@@ -236,8 +236,23 @@ 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
254
  articles,
255
+ format = 'text',
241
256
  maxTotalTokens,
242
257
  dbg = () => null
243
258
  ) {
@@ -247,15 +262,23 @@ function buildPromptFromArticlesMulti(
247
262
  let sections = [];
248
263
  let sectionTokenCount = 0;
249
264
  for (const article of articles) {
250
- const {kind, title, sourceId, distance, combinedScore, chunk} = article;
251
- const articleBlock = JSON.stringify({
252
- kind,
253
- title,
254
- url: sourceId.startsWith('http') ? sourceId : null,
255
- distance,
256
- combinedScore,
257
- chunk,
258
- });
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`;
280
+ }
281
+
259
282
  dbg(articleBlock);
260
283
  const tokensForArticle = estimateTokenCount(articleBlock);
261
284
 
@@ -268,7 +291,10 @@ function buildPromptFromArticlesMulti(
268
291
  sectionTokenCount += tokensForArticle;
269
292
  }
270
293
 
271
- return `[${sections.join(',')}]`;
294
+ if (format === 'json') {
295
+ return `[${sections.join(',')}]`;
296
+ }
297
+ return sections.join('---\n\n');
272
298
  }
273
299
 
274
300
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goblin-agents",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "AI Agents framework",
5
5
  "author": "Epsitec SA",
6
6
  "contributors": [