goblin-agents 0.1.2 → 0.1.5

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/README.md ADDED
@@ -0,0 +1,331 @@
1
+ # 📘 Documentation du module goblin-agents
2
+
3
+ ## Aperçu
4
+
5
+ Le module `goblin-agents` fournit une infrastructure complète pour intégrer des agents d'intelligence artificielle (IA) dans l'écosystème Xcraft. Il permet de créer, configurer et interagir avec différents modèles de langage (LLM) via des fournisseurs comme Ollama et OpenAI. Ce module est conçu pour faciliter l'utilisation d'agents IA dans diverses applications, en offrant des fonctionnalités avancées comme la gestion de conversations, l'analyse de documents PDF, et l'utilisation d'outils externes.
6
+
7
+ ## Structure du module
8
+
9
+ - **AiAgent** : Acteur principal qui encapsule un agent IA et ses fonctionnalités
10
+ - **Providers** : Classes pour interagir avec différents fournisseurs de LLM (Ollama, OpenAI)
11
+ - **Utilitaires** : Fonctions pour le traitement de texte, l'OCR, et la manipulation de documents
12
+
13
+ Le module est organisé autour du pattern Elf, avec une séparation claire entre la logique métier (`AiAgentLogic`) et l'interface de l'acteur (`AiAgent`).
14
+
15
+ ## Fonctionnement global
16
+
17
+ Le module `goblin-agents` permet de créer et gérer des agents IA qui peuvent interagir avec les utilisateurs via des conversations textuelles. Chaque agent est configuré avec un modèle spécifique, un fournisseur (Ollama ou OpenAI), et des paramètres qui définissent son comportement.
18
+
19
+ Les agents peuvent maintenir plusieurs contextes de conversation simultanément, ce qui permet d'avoir des conversations distinctes avec différents utilisateurs ou sur différents sujets. Chaque contexte conserve son propre historique de messages, ce qui permet à l'agent de maintenir la cohérence dans ses réponses.
20
+
21
+ Le module prend en charge diverses fonctionnalités avancées :
22
+ - Génération de texte à partir de prompts
23
+ - Conversations interactives avec historique
24
+ - Extraction de texte à partir d'images de documents PDF
25
+ - Génération d'embeddings vectoriels pour la recherche sémantique
26
+ - Utilisation d'outils externes via un système d'appels d'outils
27
+ - Paradigme ReAct (Reasoning and Acting) pour des agents plus autonomes
28
+
29
+ Les agents peuvent être configurés avec une grande variété d'options qui influencent leur comportement, comme la température (créativité), la taille du contexte, et diverses pénalités pour éviter les répétitions.
30
+
31
+ ## Exemples d'utilisation
32
+
33
+ ### Création d'un agent IA
34
+
35
+ ```javascript
36
+ const {AiAgent} = require('goblin-agents/lib/llm/aiAgent.js');
37
+
38
+ // Dans une méthode d'un acteur Elf
39
+ async initAgents() {
40
+ const feedId = await this.newQuestFeed();
41
+
42
+ // Création d'un agent avec Ollama (local)
43
+ const ollamaAgentId = SmartId.from('aiAgent', 'ollama-mistral-assistant');
44
+ const agent = await new AiAgent(this).create(ollamaAgentId, feedId, {
45
+ name: 'Assistant Personnel',
46
+ role: 'assistant',
47
+ prompt: 'Tu es un assistant personnel serviable et précis.',
48
+ provider: 'ollama',
49
+ model: 'mistral-small',
50
+ host: 'http://127.0.0.1:11434',
51
+ options: {
52
+ temperature: 0.7
53
+ },
54
+ usability: 'stable'
55
+ });
56
+
57
+ // Création d'un agent avec OpenAI (cloud)
58
+ const openAiAgentId = SmartId.from('aiAgent', 'gpt-4o-assistant');
59
+ const agent = await new AiAgent(this).create(openAiAgentId, feedId, {
60
+ name: 'Assistant Personnel',
61
+ role: 'assistant',
62
+ provider: 'open-ai',
63
+ model: 'openai/gpt-4o',
64
+ host: 'https://openrouter.ai/api/v1',
65
+ headers: {
66
+ Authorization: `Bearer <votre-clef-api>`,
67
+ },
68
+ usability: 'stable'
69
+ });
70
+ ```
71
+
72
+ ### Conversation avec un agent
73
+
74
+ ```javascript
75
+
76
+ // Dans une méthode d'un acteur Elf
77
+ async askAgent(desktopId, agentId, contextId, question, saveExchange = false) {
78
+ const feedId = await this.newQuestFeed();
79
+
80
+ // Instancie l'agent approprié par ex. 'aiAgent@gpt-4o-assistant'
81
+ const agent = await new AiAgent(this).create(agentId, feedId);
82
+
83
+ // Continue la conversation sur ce contexte par ex. 'story-xyz' et
84
+ // obtient une réponse à la question de l'utilisateur
85
+ const response = await agent.chat(contextId, question, desktopId);
86
+
87
+ if(saveExchange) {
88
+ //Persiste l'échange contextuel nouvelle paire (question->réponse) sur le disque
89
+ await agent.save();
90
+ }
91
+ return response;
92
+ }
93
+ ```
94
+
95
+ ### Résumer et "squasher" un contexte
96
+
97
+ ```javascript
98
+
99
+ // Dans une méthode d'un acteur Elf
100
+ async resumeAndSquashContexte(agentId, contextId) {
101
+ const feedId = await this.newQuestFeed();
102
+
103
+ // Instancie l'agent approprié par ex. 'aiAgent@gpt-4o-assistant'
104
+ const agent = await new AiAgent(this).create(agentId, feedId);
105
+
106
+ // Demande un résumé des échanges
107
+ const resumePrompt = "Résume l'essentiel de nos échanges en utilisant l'historique des messages";
108
+ const resumedContent = await agent.resumeExchange(resumePrompt, contextId);
109
+
110
+ // Reset la mémoire contextuel et persiste le changement
111
+ await agent.reset(contextId, true);
112
+
113
+ // Ajoute un nouvel historique d'échange a partir du résumé
114
+ await agent.set(contextId, [{role: 'assistant', content: resumedContent}]);
115
+
116
+ // Sauve ce nouvel état
117
+ await agent.save();
118
+ }
119
+ ```
120
+
121
+ ### Utilisation d'outils externes
122
+
123
+ ```javascript
124
+
125
+ // Dans une méthode d'un acteur Elf
126
+ async addWeatherToolSupportToAgent(agentId) {
127
+ const feedId = await this.newQuestFeed();
128
+
129
+ // Instancie l'agent approprié par ex. 'aiAgent@gpt-4o-assistant'
130
+ const agent = await new AiAgent(this).create(agentId, feedId);
131
+
132
+ // Configurer un agent avec des outils
133
+ // Provoquera des envois de commandes sur le bus à l'acteur "weather"
134
+ // en appelant la quête "getWeather(city)"
135
+ await agent.patch({
136
+ toolServiceId: 'weather', //acteur (ici le singleton weather) qui expose des quêtes "outils"
137
+ tools: [
138
+ {
139
+ type: 'function',
140
+ function: {
141
+ name: 'getWeather', //nom de la quête de l'acteur qui récupère la météo
142
+ description: 'Obtenir la météo pour une ville',
143
+ parameters: {
144
+ type: 'object',
145
+ properties: {
146
+ city: {
147
+ type: 'string',
148
+ description: 'Nom de la ville'
149
+ }
150
+ },
151
+ required: ['city']
152
+ }
153
+ }
154
+ }
155
+ ]
156
+ });
157
+ }
158
+
159
+
160
+ // L'agent pourra maintenant utiliser l'outil weather.getWeather
161
+ const question = "Quel temps fait-il à Paris aujourd'hui?";
162
+ const response = await agent.chat(contextId, question, desktopId);
163
+
164
+ ```
165
+
166
+ ### Génération d'embeddings pour la recherche sémantique
167
+
168
+ ```javascript
169
+
170
+ // Dans une méthode d'un acteur Elf
171
+ // {
172
+ // 'indexContent@article-1', 'Contenu à indexer'
173
+ // 'indexContent@article-2', 'Un autre contenu à indexer'
174
+ // }
175
+ //
176
+ async updateKnowledgeBase(articles) {
177
+ const feedId = await this.newQuestFeed();
178
+
179
+ // Instancie l'agent approprié pour faire un embedding
180
+ // par ex. 'aiAgent@default-embedder'
181
+ const agent = await new AiAgent(this).create(agentId, feedId);
182
+
183
+ // Récupère les textes à indexer
184
+ const texts = Object.values(articles);
185
+
186
+ // Prépare un boucle d'insertion par id d'article
187
+ const articlesIds = Object.keys(articles);
188
+
189
+ // Générer des embeddings pour plusieurs textes en une seule requête
190
+ const vectorsBatch = await agent.embedInBatch(texts);
191
+
192
+ // Upsert les articles dans un archetype IndexedContent
193
+ for(const articleId of articleIds) {
194
+ // Récupère les vecteurs correspondant au texte dans le lot
195
+ // et le texte original
196
+ const index = articleIds.indexOf(articleId);
197
+ const vectors = vectorsBatch[index];
198
+ const text = texts[index];
199
+
200
+ const indexedContent = new IndexedContentState({
201
+ id: articleId,
202
+ text, //texte original de l'article
203
+ meta: {
204
+ locale: 'fr', //définit la locale
205
+ scope: 'knowledge-base', //définit le scope
206
+ vectors, //vecteur correspondants au texte
207
+ status: 'published',
208
+ }
209
+ });
210
+
211
+ // Effectue l'upsert de l'archetype
212
+ await new IndexedContent(this).insertOrReplace(
213
+ articleId,
214
+ feedId,
215
+ indexedContent
216
+ );
217
+ }
218
+ }
219
+
220
+ // Dans une méthode d'un acteur Elf
221
+ async queryKnowledgeBase(question = "Comment fonctionne l'apprentissage par renforcement?") {
222
+ const feedId = await this.newQuestFeed();
223
+
224
+ // Instancie l'agent approprié pour faire un embedding
225
+ // par ex. 'aiAgent@default-embedder'
226
+ const agent = await new AiAgent(this).create(agentId, feedId);
227
+
228
+ // Générer un embedding pour un texte, par ex. pour une recherche
229
+ // Convertit la question texte en vecteurs
230
+ const vectors = await agent.embed(question);
231
+
232
+ // Effectue une recherche par similitude dans cryo
233
+ // Utilise un Archetype "IndexedContent"
234
+ // Limite la recherche au contenu indexé en français
235
+ // avec un scope 'knowledge-base'
236
+ // les 100 premier résultats les plus pertinents
237
+ const results = await this.cryo.searchDistance2(
238
+ IndexedContentLogic.db,
239
+ vectors,
240
+ ['fr'],
241
+ ['knowledge-base'],
242
+ 100
243
+ );
244
+
245
+ // Itère sur les données et retourne les résultats
246
+ // a l'appelant
247
+ return Array.from(results);
248
+ }
249
+ ```
250
+
251
+ ## Interactions avec d'autres modules
252
+
253
+ Le module `goblin-agents` interagit avec plusieurs autres composants de l'écosystème Xcraft :
254
+
255
+ - **xcraft-core-goblin** : Fournit l'infrastructure Elf pour la création d'acteurs
256
+ - **xcraft-core-stones** : Utilisé pour la définition des types de données
257
+ - **xcraft-core-utils** : Utilisé pour les appels API REST et la gestion des verrous
258
+
259
+ Le module peut également interagir avec d'autres services Goblin via le système d'appels d'outils, permettant aux agents d'effectuer des actions concrètes dans l'application.
260
+
261
+ ## Configuration avancée
262
+
263
+ - **version** : Version des agents (par défaut: 2)
264
+ - **defaultProfile** : Profil par défaut des agents (par défaut: null)
265
+ - **defaultSettings** : Paramètres par défaut (par défaut: null)
266
+ - **profiles** : Profils pour remplacer les paramètres (par défaut: {})
267
+ - **settings** : Paramètres par nom (par défaut: {})
268
+
269
+ ## Détails des sources
270
+
271
+ ### `aiAgent.js`
272
+
273
+ Ce fichier est le point d'entrée du module, exportant les commandes Xcraft pour l'acteur AiAgent. Il importe l'acteur et sa logique depuis le fichier principal et les expose au système Xcraft.
274
+
275
+ ### `config.js`
276
+
277
+ Ce fichier définit la configuration du module, permettant de spécifier :
278
+
279
+ - Le profil par défaut des agents (`defaultProfile`)
280
+ - Les paramètres par défaut (`defaultSettings`)
281
+ - Les profils pour remplacer les paramètres (`profiles`)
282
+ - Les paramètres par nom (`settings`)
283
+
284
+ Cette configuration permet de personnaliser le comportement des agents au niveau de l'application.
285
+
286
+ ### `lib/llm/aiAgent.js`
287
+
288
+ Ce fichier contient la définition complète de l'acteur AiAgent, avec :
289
+
290
+ - **MessageShape** : Définit la structure des messages échangés (rôle, contenu, images, appels d'outils)
291
+ - **OptionsShape** : Définit les nombreuses options de configuration des modèles LLM
292
+ - **AiAgentShape** : Définit la structure des données de l'agent
293
+ - **AiAgentState** : Classe d'état de l'agent basée sur le shape
294
+ - **AiAgentLogic** : Logique métier de l'agent (mutations d'état)
295
+ - **AiAgent** : Classe principale de l'acteur avec les méthodes d'interaction
296
+
297
+ L'acteur AiAgent offre de nombreuses méthodes pour interagir avec les modèles de langage, gérer les conversations, et utiliser des outils externes. Il prend en charge différentes configurations et options pour personnaliser le comportement des modèles.
298
+
299
+ Les options de configuration sont particulièrement riches et permettent d'ajuster finement le comportement des modèles, notamment :
300
+ - Gestion de la mémoire (NUMA, VRAM)
301
+ - Taille du contexte et traitement par lots
302
+ - Paramètres de génération (température, top_k, top_p)
303
+ - Pénalités pour éviter les répétitions
304
+ - Algorithmes adaptatifs comme Mirostat
305
+
306
+ ### `lib/llm/providers.js`
307
+
308
+ Ce fichier définit les classes pour interagir avec différents fournisseurs de LLM :
309
+
310
+ - **LLMProvider** : Classe abstraite définissant l'interface commune
311
+ - **OllamaProvider** : Implémentation pour le fournisseur Ollama (local)
312
+ - **OpenAIProvider** : Implémentation pour OpenAI et services compatibles
313
+
314
+ Ces classes encapsulent les détails d'implémentation spécifiques à chaque fournisseur, offrant une interface unifiée pour le reste du module. Elles gèrent les différences dans les formats de requête et de réponse, permettant à l'acteur AiAgent de fonctionner de manière transparente avec différents fournisseurs.
315
+
316
+ ### `lib/llm/utils.js`
317
+
318
+ Ce fichier contient diverses fonctions utilitaires pour :
319
+
320
+ - Extraction de texte à partir d'images PDF (`getPDFImages`)
321
+ - Nettoyage et simplification de contenu HTML (`cleanHtmlContent`, `simplifyHtml`)
322
+ - Découpage de texte en segments gérables (`splitLongText`, `html2chunks`)
323
+ - Estimation du nombre de tokens (`estimateTokenCount`)
324
+ - Construction de prompts à partir d'articles (`buildPromptFromArticles`)
325
+ - Traitement par lots des embeddings (`embedChunksInBatch`, `chunkArray`)
326
+
327
+ Ces utilitaires sont essentiels pour préparer les données avant de les envoyer aux modèles de langage et pour traiter les résultats. Ils permettent notamment de gérer les contraintes de taille de contexte des modèles en découpant intelligemment les textes longs.
328
+
329
+ Le module inclut également des fonctionnalités spécifiques pour l'OCR (reconnaissance optique de caractères) avec un prompt système dédié (`OCR_SYSTEM_PROMPT`) qui guide les modèles multimodaux dans l'extraction de texte à partir d'images.
330
+
331
+ *Ce document est une mise à jour de la documentation précédente.*
@@ -11,6 +11,7 @@ const {
11
11
  number,
12
12
  record,
13
13
  } = require('xcraft-core-stones');
14
+ const {getMutex} = require('xcraft-core-utils/lib/locks.js');
14
15
  const LLMProvider = require('./providers.js');
15
16
  const {OCR_SYSTEM_PROMPT, getPDFImages} = require('./utils.js');
16
17
 
@@ -86,6 +87,17 @@ class OptionsShape {
86
87
  stop = option(array(string));
87
88
  }
88
89
 
90
+ function vectorToSqlLiteral(vec) {
91
+ const array = new Float32Array(vec);
92
+ const view = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
93
+ let hex = '';
94
+ for (let i = 0; i < view.length; i++) {
95
+ const byte = view[i];
96
+ hex += byte < 16 ? '0' + byte.toString(16) : byte.toString(16);
97
+ }
98
+ return hex;
99
+ }
100
+
89
101
  class AiAgentShape {
90
102
  id = id('aiAgent');
91
103
  version = number;
@@ -240,6 +252,7 @@ class AiAgent extends Elf {
240
252
 
241
253
  _provider;
242
254
  _conf;
255
+ _batchLock = getMutex;
243
256
 
244
257
  /**
245
258
  * @param {string} id
@@ -276,18 +289,19 @@ class AiAgent extends Elf {
276
289
  async _loadProvider() {
277
290
  const {state} = this;
278
291
  const {provider, host, headers} = state;
292
+ const headersStr = JSON.stringify(headers);
279
293
  if (
280
294
  this._provider &&
281
295
  this._conf.provider === provider &&
282
296
  this._conf.host === host &&
283
- this._conf.headers === headers
297
+ this._conf.headersStr === headersStr
284
298
  ) {
285
299
  return;
286
300
  }
287
301
 
288
302
  const Instance = LLMProvider.get(this.state.provider);
289
303
  this._provider = new Instance(this.state.host, this.state.headers);
290
- this._conf = {provider, host, headers};
304
+ this._conf = {provider, host, headersStr};
291
305
  }
292
306
 
293
307
  /**
@@ -378,21 +392,27 @@ class AiAgent extends Elf {
378
392
  return null;
379
393
  }
380
394
  await this._loadProvider();
381
- return await this._provider.embed({
395
+ const vectors = await this._provider.embed({
382
396
  model: this.state.model,
383
397
  input: rawText,
384
398
  });
399
+ return vectorToSqlLiteral(vectors);
385
400
  }
386
401
 
387
402
  async embedInBatch(rawTexts = []) {
388
403
  if (rawTexts.length === 0) {
389
404
  return [];
390
405
  }
406
+
407
+ await this._batchLock.lock(this.id);
408
+ this.quest.defer(() => this._batchLock.unlock(this.id));
409
+
391
410
  await this._loadProvider();
392
- return await this._provider.embedInBatch({
411
+ const res = await this._provider.embedInBatch({
393
412
  model: this.state.model,
394
413
  input: rawTexts,
395
414
  });
415
+ return res.map((v) => vectorToSqlLiteral(v));
396
416
  }
397
417
 
398
418
  async set(contextId, messages) {
package/lib/llm/utils.js CHANGED
@@ -276,7 +276,60 @@ const buildPromptFromArticles = (articles, question, maxTokens) => {
276
276
  return context.trim();
277
277
  };
278
278
 
279
+ /**
280
+ * Split an array into smaller chunks of a given size.
281
+ *
282
+ * @param {Array} array - The array to be chunked.
283
+ * @param {number} size - The size of each chunk.
284
+ * @returns {Array[]} An array of chunked subarrays.
285
+ */
286
+ function chunkArray(array, size) {
287
+ const chunks = [];
288
+ for (let startIndex = 0; startIndex < array.length; startIndex += size) {
289
+ chunks.push(array.slice(startIndex, startIndex + size));
290
+ }
291
+ return chunks;
292
+ }
293
+
294
+ /**
295
+ * Perform batched embedding of content chunks, associating each embedding
296
+ * with its original contentId and key.
297
+ *
298
+ * @param {object} agent - The AI agent instance with an embedInBatch method.
299
+ * @param {Array<{ contentId: string, key: string, chunk: string }>} allChunks - Array of content to embed.
300
+ * @param {number} [batchSize=500] - Maximum number of chunks per embedding request.
301
+ * @returns {Promise<object>} A map of embeddings structured as:
302
+ * { [contentId]: { [key]: { chunk, embedding } } }
303
+ */
304
+ async function embedChunksInBatch(agent, allChunks, batchSize = 500) {
305
+ const embeddingsByContent = {};
306
+
307
+ for (let start = 0; start < allChunks.length; start += batchSize) {
308
+ const currentBatch = allChunks.slice(start, start + batchSize);
309
+ const inputTexts = currentBatch.map(({chunk}) => chunk);
310
+ const embeddedVectors = await agent.embedInBatch(inputTexts);
311
+
312
+ for (let i = 0; i < embeddedVectors.length; i++) {
313
+ const {contentId, key, chunk} = currentBatch[i];
314
+ const embedding = embeddedVectors[i];
315
+
316
+ if (!embeddingsByContent[contentId]) {
317
+ embeddingsByContent[contentId] = {};
318
+ }
319
+
320
+ embeddingsByContent[contentId][key] = {
321
+ chunk,
322
+ embedding,
323
+ };
324
+ }
325
+ }
326
+
327
+ return embeddingsByContent;
328
+ }
329
+
279
330
  module.exports = {
331
+ chunkArray,
332
+ embedChunksInBatch,
280
333
  extractTitlesAndContent,
281
334
  html2chunks,
282
335
  splitLongText,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goblin-agents",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "AI Agents framework",
5
5
  "author": "Samuel Loup",
6
6
  "license": "MIT",