goblin-agents 3.1.1 → 3.3.0

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.
@@ -0,0 +1,4 @@
1
+ const {Elf} = require('xcraft-core-goblin');
2
+ const {ContextCache, ContextCacheLogic} = require('./lib/contextCache.js');
3
+
4
+ exports.xcraftCommands = Elf.birth(ContextCache, ContextCacheLogic);
@@ -0,0 +1,69 @@
1
+ // @ts-check
2
+ const {Elf} = require('xcraft-core-goblin');
3
+ const {record, string} = require('xcraft-core-stones');
4
+
5
+ class ContextCacheShape {
6
+ id = string;
7
+ contexts = record(string, string);
8
+ }
9
+
10
+ class ContextCacheState extends Elf.Sculpt(ContextCacheShape) {}
11
+
12
+ class ContextCacheLogic extends Elf.Spirit {
13
+ state = new ContextCacheState({
14
+ id: 'contextCache',
15
+ contexts: {},
16
+ });
17
+
18
+ append(contextId, markdown) {
19
+ const current = this.state.contexts[contextId] || '';
20
+ this.state.contexts[contextId] = current + markdown;
21
+ }
22
+
23
+ clear(contextId) {
24
+ delete this.state.contexts[contextId];
25
+ }
26
+ }
27
+
28
+ class ContextCache extends Elf.Alone {
29
+ logic = Elf.getLogic(ContextCacheLogic);
30
+ state = new ContextCacheState();
31
+
32
+ async init() {
33
+ return this;
34
+ }
35
+
36
+ async append(contextId, markdown) {
37
+ if (typeof contextId !== 'string') {
38
+ throw new TypeError('contextId must be a string');
39
+ }
40
+ if (typeof markdown !== 'string') {
41
+ throw new TypeError('markdown must be a string');
42
+ }
43
+
44
+ this.logic.append(contextId, markdown);
45
+ }
46
+
47
+ async read(contextId) {
48
+ if (typeof contextId !== 'string') {
49
+ throw new TypeError('contextId must be a string');
50
+ }
51
+
52
+ return this.state.contexts[contextId] || '';
53
+ }
54
+
55
+ async clear(contextId) {
56
+ if (typeof contextId !== 'string') {
57
+ throw new TypeError('contextId must be a string');
58
+ }
59
+
60
+ this.logic.clear(contextId);
61
+ }
62
+ }
63
+
64
+ module.exports = {
65
+ ContextCache,
66
+ ContextCacheLogic,
67
+ ContextCacheShape,
68
+ ContextCacheState,
69
+ };
@@ -13,7 +13,12 @@ const {
13
13
  } = require('xcraft-core-stones');
14
14
  const {getMutex} = require('xcraft-core-utils/lib/locks.js');
15
15
  const LLMProviders = require('./providers.js');
16
- const {OCR_SYSTEM_PROMPT, getPDFImages, splitThinkTags} = require('./utils.js');
16
+ const {
17
+ OCR_SYSTEM_PROMPT,
18
+ getPDFImages,
19
+ splitThinkTags,
20
+ getContextUsage,
21
+ } = require('./utils.js');
17
22
  const Logger = require('../logger.js');
18
23
 
19
24
  const CONVERSATION_TITLE_PROMPT = `Create a concise and informative title that summarizes the main topic of the conversation.
@@ -30,6 +35,15 @@ class MessageShape {
30
35
  tool_name = option(string);
31
36
  }
32
37
 
38
+ class UsageShape {
39
+ used = number;
40
+ reserved = number;
41
+ remaining = number;
42
+ total = number;
43
+ percentage = number;
44
+ nearLimit = boolean;
45
+ }
46
+
33
47
  class OptionsShape {
34
48
  // Active NUMA (Non-Uniform Memory Access) pour optimiser la gestion mémoire sur les architectures multi-sockets.
35
49
  numa = option(boolean);
@@ -150,6 +164,7 @@ class AiAgentShape {
150
164
  role = string; //agent role
151
165
  prompt = string; //default system prompt
152
166
  contexts = record(string, string); //additionnal system prompt context
167
+ usages = record(string, UsageShape);
153
168
  reasonings = record(
154
169
  string,
155
170
  enumeration(false, true, 'low', 'medium', 'high')
@@ -204,6 +219,7 @@ class AiAgentLogic extends Elf.Archetype {
204
219
  provider: defaultValues.provider,
205
220
  model: defaultValues.model,
206
221
  otherModels: defaultValues.otherModels,
222
+ usages: {},
207
223
  messages: {},
208
224
  contexts: {},
209
225
  reasonings: {},
@@ -239,10 +255,21 @@ class AiAgentLogic extends Elf.Archetype {
239
255
  this.patch(patch);
240
256
  state.version = version;
241
257
  }
258
+ _updateContextUsage(contextId, usage) {
259
+ this.state.usages[contextId] = usage;
260
+ }
242
261
 
243
262
  reset(contextId) {
244
263
  const {state} = this;
245
264
  state.messages[contextId] = [];
265
+ state.usages[contextId] = {
266
+ used: 0,
267
+ reserved: 0,
268
+ remaining: this.state.options?.num_ctx || 4096,
269
+ total: this.state.options?.num_ctx || 4096,
270
+ percentage: 0,
271
+ nearLimit: false,
272
+ };
246
273
  if (state.contexts[contextId]) {
247
274
  delete state.contexts[contextId];
248
275
  }
@@ -470,8 +497,8 @@ class AiAgent extends Elf {
470
497
  return cleanText(res);
471
498
  }
472
499
 
473
- async reset(contextId, save = false) {
474
- if (!contextId) {
500
+ async reset(contextId = null, save = false) {
501
+ if (contextId === null) {
475
502
  for (const contextId of Object.keys(this.state.messages)) {
476
503
  await this.reset(contextId);
477
504
  }
@@ -509,7 +536,7 @@ class AiAgent extends Elf {
509
536
  ],
510
537
  },
511
538
  ];
512
- const result = await this._provider.chat({
539
+ const {result} = await this._provider.chat({
513
540
  model: this.state.model,
514
541
  reasoning: this.state.reasoning,
515
542
  messages,
@@ -542,7 +569,7 @@ class AiAgent extends Elf {
542
569
  ],
543
570
  },
544
571
  ];
545
- const result = await this._provider.chat({
572
+ const {result} = await this._provider.chat({
546
573
  model: this.state.model,
547
574
  reasoning: this.state.reasoning,
548
575
  messages,
@@ -594,6 +621,37 @@ class AiAgent extends Elf {
594
621
  this.logic.set(contextId, messages);
595
622
  }
596
623
 
624
+ async getContextUsage(contextId) {
625
+ const usage = this.state.usages[contextId];
626
+ if (!usage) {
627
+ return {
628
+ used: 0,
629
+ reserved: 0,
630
+ remaining: this.state.options?.num_ctx || 4096,
631
+ total: this.state.options?.num_ctx || 4096,
632
+ percentage: 0,
633
+ nearLimit: false,
634
+ };
635
+ }
636
+ return {
637
+ used: usage.used,
638
+ reserved: usage.reserved,
639
+ remaining: usage.remaining,
640
+ total: usage.total,
641
+ percentage: usage.percentage,
642
+ nearLimit: usage.nearLimit,
643
+ };
644
+ }
645
+
646
+ async _updateContextUsage(contextId, response) {
647
+ const usage = getContextUsage(
648
+ response,
649
+ this.state.options?.num_ctx || undefined,
650
+ this.state.options?.num_predict || undefined
651
+ );
652
+ this.logic._updateContextUsage(contextId, usage);
653
+ }
654
+
597
655
  /**
598
656
  * Set chat session context, model and level of reasoning
599
657
  * @param {string} contextId
@@ -673,8 +731,8 @@ ${context}
673
731
  tools: disableTool ? undefined : this.state.tools?.toJS(),
674
732
  };
675
733
 
676
- const result = await this._provider.chat(chatCompletionRequest);
677
-
734
+ const {result, usage} = await this._provider.chat(chatCompletionRequest);
735
+ await this._updateContextUsage(contextId, usage);
678
736
  logger.push('aiAgent.chat', {
679
737
  id: this.id,
680
738
  model: chatCompletionRequest.model,
@@ -695,7 +753,7 @@ ${context}
695
753
  );
696
754
  if (used) {
697
755
  //leave for a second turn
698
- this.log.dbg(`🐈 Chat session ended : ${contextId}`);
756
+ this.log.dbg(`🐈 Chat session ended by a tool call: ${contextId}`);
699
757
  return true;
700
758
  }
701
759
  }
@@ -712,7 +770,7 @@ ${context}
712
770
 
713
771
  async callTools(contextId, message, userDesktopId) {
714
772
  const {tool_calls} = message;
715
- if (!tool_calls || !userDesktopId) {
773
+ if (!tool_calls?.length || !userDesktopId) {
716
774
  return false;
717
775
  }
718
776
  this.logic.callTools(contextId, tool_calls);
@@ -722,20 +780,19 @@ ${context}
722
780
  function: {name},
723
781
  } = tool;
724
782
 
725
- let result = '';
726
- let toolArguments = {};
727
- if (
728
- tool.function.arguments &&
729
- typeof tool.function.arguments === 'string'
730
- ) {
731
- toolArguments = JSON.parse(tool.function.arguments);
732
- } else if (
733
- tool.function.arguments &&
734
- typeof tool.function.arguments === 'object'
735
- ) {
736
- toolArguments = tool.function.arguments;
737
- }
738
783
  try {
784
+ let toolArguments = {};
785
+ if (
786
+ tool.function.arguments &&
787
+ typeof tool.function.arguments === 'string'
788
+ ) {
789
+ toolArguments = JSON.parse(tool.function.arguments);
790
+ } else if (
791
+ tool.function.arguments &&
792
+ typeof tool.function.arguments === 'object'
793
+ ) {
794
+ toolArguments = tool.function.arguments;
795
+ }
739
796
  const payload = {
740
797
  id: this.state.toolServiceId,
741
798
  desktopId: userDesktopId,
@@ -745,10 +802,18 @@ ${context}
745
802
  const toolNamespace = this.state.toolServiceId?.split('@', 1)[0];
746
803
  const toolCmd = `${toolNamespace}.${name}`;
747
804
  this.log.dbg(`🔧 Tool call: ${toolCmd}(${JSON.stringify(payload)})`);
748
- result = await this.quest.cmd(toolCmd, payload);
749
- await this.addToolMessage(contextId, id, name, result);
805
+ const result = await this.quest.cmd(toolCmd, payload);
806
+ const content =
807
+ typeof result === 'string' ? result : JSON.stringify(result ?? null);
808
+ await this.addToolMessage(contextId, id, name, content);
750
809
  } catch (err) {
751
810
  this.log.dbg(`🔧 Tool error : ${err.message}`);
811
+ await this.addToolMessage(
812
+ contextId,
813
+ id,
814
+ name,
815
+ JSON.stringify({status: 'error', message: err.message})
816
+ );
752
817
  }
753
818
  }
754
819
  return true;
@@ -37,9 +37,9 @@ class OllamaProvider extends LLMProvider {
37
37
  });
38
38
  if (useFormat) {
39
39
  chat.message.content = JSON.parse(chat.message.content);
40
- return chat;
40
+ return {result: chat};
41
41
  } else {
42
- return chat;
42
+ return {result: chat};
43
43
  }
44
44
  }
45
45
 
@@ -53,7 +53,7 @@ class OpenAIProvider extends LLMProvider {
53
53
  methods: ['POST'],
54
54
  },
55
55
  });
56
- const {choices} = chat;
56
+ const {choices, usage} = chat;
57
57
  if (chat.error) {
58
58
  const err = new Error(
59
59
  chat.error?.metadata
@@ -67,9 +67,9 @@ class OpenAIProvider extends LLMProvider {
67
67
  choices[0].message.content = JSON.parse(
68
68
  choices[0].message.content
69
69
  ).results;
70
- return choices[0];
70
+ return {result: choices[0], usage};
71
71
  } else {
72
- return choices[0];
72
+ return {result: choices[0], usage};
73
73
  }
74
74
  }
75
75
 
@@ -96,8 +96,8 @@ class OpenAIProvider extends LLMProvider {
96
96
  stream,
97
97
  reasoning,
98
98
  };
99
- const chat = await this.chat(chatRequest);
100
- return chat.message.content;
99
+ const {result} = await this.chat(chatRequest);
100
+ return result.message.content;
101
101
  }
102
102
 
103
103
  async embed(embedRequest) {
package/lib/llm/utils.js CHANGED
@@ -49,6 +49,9 @@ const getPDFImages = async (pdfPath, pagesToProcess) => {
49
49
  };
50
50
 
51
51
  const cleanHtmlContent = (html) => {
52
+ if (typeof html !== 'string') {
53
+ return '';
54
+ }
52
55
  const decodedHtml = HtmlEntities.decode(html);
53
56
  const textOnly = decodedHtml.replace(/<[^>]+>/g, '');
54
57
  const cleanedText = textOnly.replace(/\s+/g, ' ').trim();
@@ -379,6 +382,27 @@ function splitThinkTags(input) {
379
382
  };
380
383
  }
381
384
 
385
+ function getContextUsage(usage, numCtx = 65536, maxTokens = 4096) {
386
+ const used = usage?.prompt_tokens ?? usage?.prompt_eval_count;
387
+
388
+ if (!Number.isFinite(used)) {
389
+ return null;
390
+ }
391
+
392
+ const available = Math.max(0, numCtx - used);
393
+ const reserved = Math.min(maxTokens, available);
394
+ const remaining = available - reserved;
395
+
396
+ return {
397
+ used,
398
+ reserved,
399
+ remaining,
400
+ total: numCtx,
401
+ percentage: Math.min(100, (used / numCtx) * 100),
402
+ nearLimit: used + maxTokens >= numCtx,
403
+ };
404
+ }
405
+
382
406
  module.exports = {
383
407
  chunkArray,
384
408
  embedChunksInBatch,
@@ -394,4 +418,5 @@ module.exports = {
394
418
  groupSentences,
395
419
  buildPromptFromArticlesMulti,
396
420
  splitThinkTags,
421
+ getContextUsage,
397
422
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goblin-agents",
3
- "version": "3.1.1",
3
+ "version": "3.3.0",
4
4
  "description": "AI Agents framework",
5
5
  "author": "Epsitec SA",
6
6
  "contributors": [