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.
- package/contextCache.js +4 -0
- package/lib/contextCache.js +69 -0
- package/lib/llm/aiAgent.js +89 -24
- package/lib/llm/providers/ollama.js +2 -2
- package/lib/llm/providers/opanAi.js +5 -5
- package/lib/llm/utils.js +25 -0
- package/package.json +1 -1
package/contextCache.js
ADDED
|
@@ -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
|
+
};
|
package/lib/llm/aiAgent.js
CHANGED
|
@@ -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 {
|
|
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 (
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
|
100
|
-
return
|
|
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
|
};
|