data-primals-engine 1.5.1 → 1.5.2

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.
@@ -1,44 +1,47 @@
1
1
  import { getCollectionForUser, modelsCollection } from "../mongodb.js";
2
2
  import { Logger } from "../../gameObject.js";
3
- import { ChatOpenAI } from "@langchain/openai";
4
- import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
5
3
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
6
4
  import {searchData, patchData, deleteData, insertData} from "../data/index.js";
7
- import { getDataAsString } from "../../data.js";
8
- import i18n from "../../i18n.js";
9
- import {generateLimiter} from "../user.js";
10
- import rateLimit from "express-rate-limit";
11
- import {maxAIReflectiveSteps} from "../../constants.js";
12
5
  import {providers} from "./constants.js";
13
- import {ChatDeepSeek} from "@langchain/deepseek";
14
- import {ChatAnthropic} from "@langchain/anthropic";
15
6
  import {Config} from "../../config.js";
7
+ import { Event } from "../../events.js";
8
+ import rateLimit from "express-rate-limit";
9
+ import {generateLimiter} from "../user.js";
10
+ import {maxAIReflectiveSteps} from "../../constants.js";
11
+ import i18n from "../../i18n.js";
12
+ import {parseSafeJSON} from "../../core.js";
16
13
 
17
14
  let logger = null;
18
15
 
19
- export const getAIProvider= (aiProvider, aiModel, apiKey)=>{
20
- let llm;
16
+ export const getAIProvider= async (aiProvider, aiModel, apiKey)=>{
21
17
  try {
22
18
  switch (aiProvider) {
23
- case 'OpenAI':
24
- llm = new ChatOpenAI({apiKey, model: aiModel, temperature: 0.7});
25
- break;
26
- case 'Google':
27
- llm = new ChatGoogleGenerativeAI({apiKey, model: aiModel, temperature: 0.7});
28
- break;
29
- case 'DeepSeek':
30
- llm = new ChatDeepSeek({apiKey, model: aiModel, temperature: 0.7});
31
- break;
32
- case 'Anthropic':
33
- llm = new ChatAnthropic({apiKey, model: aiModel, temperature: 0.7});
34
- break;
19
+ case 'OpenAI': {
20
+ const { ChatOpenAI } = await import("@langchain/openai");
21
+ return new ChatOpenAI({apiKey, model: aiModel, temperature: 0.7});
22
+ }
23
+ case 'Google': {
24
+ const { ChatGoogleGenerativeAI } = await import("@langchain/google-genai");
25
+ return new ChatGoogleGenerativeAI({apiKey, model: aiModel, temperature: 0.7});
26
+ }
27
+ case 'DeepSeek': {
28
+ const { ChatDeepSeek } = await import("@langchain/deepseek");
29
+ return new ChatDeepSeek({apiKey, model: aiModel, temperature: 0.7});
30
+ }
31
+ case 'Anthropic': {
32
+ const { ChatAnthropic } = await import("@langchain/anthropic");
33
+ return new ChatAnthropic({apiKey, model: aiModel, temperature: 0.7});
34
+ }
35
35
  default:
36
36
  throw new Error(`Unsupported AI provider: ${aiProvider}`);
37
37
  }
38
- return llm;
39
- }
40
- catch (e) {
41
- return null;
38
+ } catch (e) {
39
+ if (e.code === 'ERR_MODULE_NOT_FOUND') {
40
+ logger.error(`[Assistant] The package for the '${aiProvider}' provider is not installed. Please run 'npm install @langchain/${aiProvider.toLowerCase()}' to use this provider.`);
41
+ throw new Error(`The AI provider '${aiProvider}' is not installed. Please ask the administrator to install the corresponding package.`);
42
+ }
43
+ logger.error(`[Assistant] Error initializing AI provider '${aiProvider}': ${e.message}`);
44
+ throw e; // Re-throw other errors
42
45
  }
43
46
  }
44
47
  export const assistantGlobalLimiter = rateLimit({
@@ -317,14 +320,10 @@ COMMANDE FINALE :
317
320
  * @returns {object} Le filtre corrigé.
318
321
  */
319
322
  function correctAIFilter(filter) {
320
- if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
321
- return filter; // Pas un objet à corriger
322
- }
323
+ if (!filter || typeof filter !== 'object' || Array.isArray(filter)) return filter;
323
324
 
324
325
  const keys = Object.keys(filter);
325
- if (keys.length <= 1) {
326
- return filter; // Rien à corriger
327
- }
326
+ if (keys.length <= 1) return filter;
328
327
 
329
328
  // Vérifie si toutes les clés sont des opérateurs (commencent par '$')
330
329
  const allKeysAreOperators = keys.every(key => key.startsWith('$'));
@@ -335,7 +334,7 @@ function correctAIFilter(filter) {
335
334
  return { '$and': andConditions };
336
335
  }
337
336
 
338
- return filter; // Aucune correction nécessaire
337
+ return filter;
339
338
  }
340
339
 
341
340
  /**
@@ -350,30 +349,6 @@ async function executeTool(action, params, user, allModels) {
350
349
  logger.debug(`[Assistant] Exécution de l'outil: ${action} avec les paramètres:`, params);
351
350
  try {
352
351
  switch (action) {
353
- case 'search': {
354
- const modelDef = allModels.find(m => m.name === params.model);
355
- if (!modelDef) {
356
- return `Erreur: Le modèle '${params.model}' n'a pas été trouvé. Impossible de lancer la recherche.`;
357
- }
358
-
359
- const searchResult = await searchData({
360
- model: params.model,
361
- filter: params.filter,
362
- limit: params.limit || 10,
363
- sort: params.sort
364
- }, user);
365
-
366
- if (searchResult.data.length === 0) {
367
- return i18n.t('assistant.noResults', "Aucun résultat trouvé.");
368
- }
369
-
370
- const resultString = i18n.t('assistant.searchResults', "Voici les résultats :") +
371
- searchResult.data.map(item =>
372
- `\n- ${getDataAsString(modelDef, item, { i18n, t: i18n.t }, allModels, true)}`
373
- ).join('');
374
-
375
- return resultString;
376
- }
377
352
  case 'search_models': {
378
353
  const { main: foundModels, related: relatedModels } = await searchModels(params.query, user);
379
354
 
@@ -402,16 +377,13 @@ async function executeTool(action, params, user, allModels) {
402
377
  /**
403
378
  * Gère la requête de chat, soit en exécutant une action confirmée,
404
379
  * soit en lançant la boucle de raisonnement de l'IA.
405
- * @param {string} message - Le message de l'utilisateur.
406
- * @param {Array} history - L'historique de la conversation.
407
- * @param {string} provider - Le fournisseur d'IA ('OpenAI' ou 'google').
408
- * @param {object} context - Contexte additionnel.
380
+ @param {object} params - La liste des paramètres envoyés par la requete
409
381
  * @param {object} user - L'objet utilisateur.
410
- * @param {object} confirmedAction - Une action pré-approuvée par l'utilisateur.
411
382
  * @returns {Promise<object>} La réponse de l'assistant.
412
383
  */
413
- async function handleChatRequest(message, history, provider, context, user, confirmedAction) {
384
+ export async function handleChatRequest(params, user) {
414
385
 
386
+ const { message, history, provider, confirmedAction } = params;
415
387
  const allModels = await modelsCollection.find({$or: [{_user: {$exists: false}}, {_user: user.username}]}).toArray();
416
388
 
417
389
  // --- GESTION D'UNE ACTION CONFIRMÉE ---
@@ -435,8 +407,9 @@ async function handleChatRequest(message, history, provider, context, user, conf
435
407
 
436
408
  // --- INITIALISATION DE L'IA ---
437
409
  let llm;
410
+ let llmOptions = { };
438
411
  try {
439
- const p = provider || 'OpenAI';
412
+ const p = Config.Get('assistant.provider', provider ||'OpenAI');
440
413
  const envKeyName = providers[p].key;
441
414
  if (!envKeyName) return {success: false, message: `Fournisseur IA non supporté : ${p}`};
442
415
 
@@ -446,15 +419,17 @@ async function handleChatRequest(message, history, provider, context, user, conf
446
419
 
447
420
  if (!apiKey) return {success: false, message: `Clé API pour ${p} (${envKeyName}) non trouvée.`};
448
421
 
449
- llm = getAIProvider(p, providers[p]?.defaultModel, apiKey);
422
+ const model = Config.Get('assistant.model', providers[p]?.defaultModel);
423
+ llmOptions = { provider: p, model, apiKey };
424
+ llm = await getAIProvider(llmOptions.provider, llmOptions.model, llmOptions.apiKey);
450
425
  } catch (initError) {
451
426
  logger.error(`[Assistant] Erreur d'initialisation du client IA: ${initError.message}`);
452
427
  return {success: false, message: `Erreur d'initialisation du client IA: ${initError.message}`};
453
428
  }
454
429
 
455
- // --- PRÉPARATION DE L'HISTORIQUE DE CONVERSATION ---
456
- const systemPrompt = createSystemPrompt([], user.lang || 'en');
457
- const conversationHistory = history
430
+ const systemPrompt = await Event.Trigger('OnSystemPrompt', 'event', 'user', user);
431
+
432
+ const conversationHistory = (history || [])
458
433
  .filter(msg => msg.text && !(msg.from === 'bot' && msg.text.startsWith(i18n.t('assistant.welcome'))))
459
434
  .map(msg => new (msg.from === 'user' ? HumanMessage : SystemMessage)(msg.text));
460
435
 
@@ -479,7 +454,7 @@ async function handleChatRequest(message, history, provider, context, user, conf
479
454
 
480
455
  if (match && match[0]) {
481
456
  // Si un JSON est trouvé, on tente de le parser
482
- parsedResponse = JSON.parse(match[0]);
457
+ parsedResponse = parseSafeJSON(match[0]);
483
458
  } else {
484
459
  // Aucun JSON trouvé, c'est probablement une réponse textuelle simple.
485
460
  return { success: true, displayMessage: llmOutput };
@@ -497,80 +472,32 @@ async function handleChatRequest(message, history, provider, context, user, conf
497
472
  logger.debug(`[Assistant] Action décidée par l'IA: ${parsedResponse.action}`, parsedResponse);
498
473
  conversationHistory.push(new SystemMessage(JSON.stringify(parsedResponse)));
499
474
 
500
- const { action, params } = parsedResponse;
475
+ const { action, params:parsedParams } = parsedResponse;
501
476
 
502
477
  // Correction automatique du filtre généré par l'IA, qui hallucine parfois
503
- if (params && params.filter) {
504
- logger.debug(`[Assistant] Filtre original de l'IA: ${JSON.stringify(params.filter)}`);
505
- params.filter = correctAIFilter(params.filter);
478
+ if (parsedParams && parsedParams.filter) {
479
+ logger.debug(`[Assistant] Filtre original de l'IA: ${JSON.stringify(parsedParams.filter)}`);
480
+ parsedParams.filter = correctAIFilter(parsedParams.filter);
506
481
  }
507
482
 
508
- // Action de génération de graphique, gérée par le front-end
509
- if (action === 'generateChart') {
510
- // On retourne directement la configuration du graphique au client.
511
- return { success: true, chartConfig: params };
512
- }
513
-
514
- // Action de génération de vue HTML
515
- if (action === 'generateHtmlView') {
516
- const viewData = await searchData({
517
- model: params.model,
518
- filter: params.filter,
519
- limit: params.limit || 10,
520
- depth: 2 // Pour avoir accès aux relations de premier niveau dans les templates
521
- }, user);
522
-
523
- if (viewData.data.length === 0) {
524
- return { success: true, displayMessage: i18n.t('assistant.htmlView.noResult', "Je n'ai trouvé aucune donnée correspondante pour cette vue.") };
525
- }
526
-
527
- // On retourne la configuration de la vue ET les données au client.
528
- return { success: true, htmlViewConfig: { ...params, data: viewData.data } };
483
+ // Outils pour le raisonnement interne de l'IA
484
+ if (['search_models'].includes(action)) { // On a enlevé 'search' d'ici
485
+ const toolResult = await executeTool(action, parsedParams, user, allModels);
486
+ conversationHistory.push(new SystemMessage(`Résultat de l'outil '${action}':\n${toolResult}`));
487
+ continue; // On continue la boucle pour que l'IA puisse raisonner avec ce nouveau résultat
529
488
  }
530
489
 
531
- // NOUVEAU: Action de recherche à afficher, gérée par le front-end
532
- if (action === 'search') {
533
- const searchResult = await searchData({
534
- model: params.model,
535
- filter: params.filter,
536
- limit: params.limit || 10,
537
- sort: params.sort
538
- }, user);
539
- if (searchResult.data?.length > 0) {
540
- return { success: true, dataResult: { model: params.model, data: searchResult.data } };
541
- } else {
542
- return { success: true, displayMessage: i18n.t('assistant.search.noResults', "Je n'ai trouvé aucun résultat.") };
543
- }
544
- }
490
+ const res = await Event.Trigger('OnChatAction', 'event', 'user', action, params, parsedResponse, llmOptions, user);
545
491
 
546
- // Actions nécessitant une confirmation de l'utilisateur
547
- if (['post', 'update', 'delete'].includes(action)) {
548
- const confirmationMessage = i18n.t('assistant.confirmActionPrompt', "Veuillez confirmer l'action suivante :");
492
+ if( !res ) {
493
+ // Si l'action n'est reconnue par aucune des logiques ci-dessus
494
+ logger.warn(`[Assistant] Action non reconnue reçue de l'IA: ${action}`);
549
495
  return {
550
496
  success: true,
551
- displayMessage: confirmationMessage,
552
- confirmationRequest: parsedResponse
497
+ displayMessage: i18n.t('assistant.unknownAction', "Désolé, je ne comprends pas la commande '{{action}}'.", {action})
553
498
  };
554
499
  }
555
-
556
- // Action finale pour afficher un message
557
- if (action === 'displayMessage') {
558
- return { success: true, displayMessage: params.message };
559
- }
560
-
561
- // Outils pour le raisonnement interne de l'IA
562
- if (['search_models'].includes(action)) { // On a enlevé 'search' d'ici
563
- const toolResult = await executeTool(action, params, user, allModels);
564
- conversationHistory.push(new SystemMessage(`Résultat de l'outil '${action}':\n${toolResult}`));
565
- continue; // On continue la boucle pour que l'IA puisse raisonner avec ce nouveau résultat
566
- }
567
-
568
- // Si l'action n'est reconnue par aucune des logiques ci-dessus
569
- logger.warn(`[Assistant] Action non reconnue reçue de l'IA: ${action}`);
570
- return {
571
- success: true,
572
- displayMessage: i18n.t('assistant.unknownAction', "Désolé, je ne comprends pas la commande '{{action}}'.", { action })
573
- };
500
+ return res;
574
501
  }
575
502
 
576
503
  // Si la boucle se termine sans une action finale
@@ -603,12 +530,74 @@ async function executeConfirmedAction(action, params, user) {
603
530
  }
604
531
  }
605
532
 
533
+ /**
534
+ * Gère une action finale décidée par l'IA et retourne une réponse pour le client.
535
+ * Cette fonction est appelée via le système d'événements 'OnChatAction'.
536
+ * @param {string} action - L'action à exécuter.
537
+ * @param {object} params - Les paramètres de l'action.
538
+ * @param {object} parsedResponse - La réponse JSON originale de l'IA, pour les demandes de confirmation.
539
+ * @param {object} user - L'objet utilisateur.
540
+ * @returns {Promise<object|boolean>} - L'objet de réponse final ou false si l'action n'est pas reconnue.
541
+ */
542
+ async function handleFinalChatAction(action, params, parsedResponse, user) {
543
+ // Action de génération de graphique, gérée par le front-end
544
+ if (action === 'generateChart') {
545
+ return {success: true, chartConfig: params};
546
+ }
547
+ // Action de génération de vue HTML
548
+ if (action === 'generateHtmlView') {
549
+ const viewData = await searchData({
550
+ model: params.model,
551
+ filter: params.filter,
552
+ limit: params.limit || 10,
553
+ depth: 2 // Pour avoir accès aux relations de premier niveau dans les templates
554
+ }, user);
555
+
556
+ if (viewData.data.length === 0) {
557
+ return {
558
+ success: true,
559
+ displayMessage: i18n.t('assistant.htmlView.noResult', "Je n'ai trouvé aucune donnée correspondante pour cette vue.")
560
+ };
561
+ }
562
+ return {success: true, htmlViewConfig: {...params, data: viewData.data}};
563
+ }
564
+
565
+ // Action de recherche à afficher, gérée par le front-end
566
+ if (action === 'search') {
567
+ const searchResult = await searchData({ model: params.model, filter: params.filter, limit: params.limit || 10, sort: params.sort }, user);
568
+ if (searchResult.data?.length > 0) {
569
+ return {success: true, dataResult: {model: params.model, data: searchResult.data}};
570
+ } else {
571
+ return { success: true, displayMessage: i18n.t('assistant.search.noResults', "Je n'ai trouvé aucun résultat.") };
572
+ }
573
+ }
574
+
575
+ // Actions nécessitant une confirmation de l'utilisateur
576
+ if (['post', 'update', 'delete'].includes(action)) {
577
+ return { success: true, displayMessage: i18n.t('assistant.confirmActionPrompt', "Veuillez confirmer l'action suivante :"), confirmationRequest: parsedResponse };
578
+ }
579
+
580
+ // Action finale pour afficher un message
581
+ if (action === 'displayMessage') {
582
+ return {success: true, displayMessage: params.message};
583
+ }
584
+
585
+ return false; // Action non reconnue par cet écouteur
586
+ }
587
+
606
588
 
607
589
  export async function onInit(engine) {
608
590
  logger = engine.getComponent(Logger);
609
591
 
610
592
  const {middlewareAuthenticator, userInitiator} = await import('../user.js');
611
593
 
594
+ Event.Listen('OnSystemPrompt', (user) => {
595
+ return createSystemPrompt([], user.lang || 'en');
596
+ }, 'event', 'user');
597
+ // Enregistre le gestionnaire central pour les actions finales du chat.
598
+ // D'autres modules pourraient également écouter cet événement pour étendre les fonctionnalités.
599
+ Event.Listen('OnChatAction', handleFinalChatAction,"event", "user");
600
+
612
601
  engine.post('/api/assistant/chat', [middlewareAuthenticator, userInitiator, assistantGlobalLimiter, generateLimiter], async (req, res) => {
613
602
  // On récupère TOUTES les propriétés du body, y compris l'action confirmée
614
603
  const {message, history, provider, context, confirmedAction} = req.fields;
@@ -630,7 +619,7 @@ export async function onInit(engine) {
630
619
  }
631
620
 
632
621
  try {
633
- const result = await handleChatRequest(message, history, provider, context, req.me, confirmedAction);
622
+ const result = await handleChatRequest(req.fields, req.me);
634
623
  res.json(result);
635
624
  } catch (error) {
636
625
  logger.error(`[Endpoint /api/assistant/chat] Erreur inattendue: ${error.message}`, error.stack);
@@ -8,7 +8,8 @@ export const providers = {
8
8
  key:"GOOGLE_API_KEY"
9
9
  },
10
10
  "DeepSeek": {
11
- key: "DEEPSEEK_API_KEY"
11
+ key: "DEEPSEEK_API_KEY",
12
+ defaultModel: 'deepseek-chat'
12
13
  },
13
14
  "Anthropic": {
14
15
  key:"ANTHROPIC_API_KEY"
@@ -163,7 +163,7 @@ export async function handleRevertToRevisionRequest(req, res) {
163
163
  */
164
164
  export async function handleGetHistoryRequest(req, res) {
165
165
  const { modelName, recordId } = req.params;
166
- const { limit = 10, page = 1 } = req.query;
166
+ const { limit = 10, page = 1, startDate, endDate } = req.query;
167
167
  const user = req.me; // Le middleware d'authentification attache l'utilisateur à req.me
168
168
 
169
169
  try {
@@ -181,24 +181,48 @@ export async function handleGetHistoryRequest(req, res) {
181
181
 
182
182
  // 3. Récupération des données depuis la collection 'history'
183
183
  const historyCollection = getCollection('history');
184
- const filter = {
185
- documentId: new ObjectId(recordId),
186
- model: modelName
187
- };
184
+ const filterConditions = [
185
+ { "$eq": ["$documentId", new ObjectId(recordId)]},
186
+ { "$eq": ["$model", modelName]}
187
+ ];
188
+
189
+ // Ajout du filtre par plage de dates
190
+ if (startDate) {
191
+ try {
192
+ filterConditions.push({ $gte: ["$timestamp", new Date(startDate)] });
193
+ } catch (e) { /* ignore invalid date */ }
194
+ }
195
+ if (endDate) {
196
+ try {
197
+ const endOfDay = new Date(endDate);
198
+ endOfDay.setUTCHours(23, 59, 59, 999); // Inclusif pour toute la journée de fin
199
+ filterConditions.push({ $lte: ["$timestamp", endOfDay] });
200
+ } catch (e) { /* ignore invalid date */ }
201
+ }
202
+
203
+ const filter = { "$and": filterConditions };
188
204
 
189
205
  const limitInt = parseInt(limit, 10);
190
206
  const pageInt = parseInt(page, 10);
191
207
  const skip = (pageInt - 1) * limitInt;
192
208
 
193
- const [totalCount, historyData] = await Promise.all([
194
- historyCollection.countDocuments(filter),
195
- historyCollection.find(filter)
209
+ // Utiliser une agrégation pour le comptage afin de s'assurer que le filtre $expr est correctement appliqué.
210
+ const countPipeline = [
211
+ { $match: { $expr: filter } },
212
+ { $count: "count" }
213
+ ];
214
+
215
+ const [countResult, historyData] = await Promise.all([
216
+ historyCollection.aggregate(countPipeline).toArray(),
217
+ historyCollection.aggregate([{$match: {$expr: filter}}])
196
218
  .sort({ version: -1 })
197
219
  .skip(skip)
198
220
  .limit(limitInt)
199
221
  .toArray()
200
222
  ]);
201
223
 
224
+ const totalCount = countResult.length > 0 ? countResult[0].count : 0;
225
+
202
226
  // 4. Transformation des données pour correspondre au format attendu par le composant HistoryDialog.jsx
203
227
  const opMap = {
204
228
  create: 'i',