data-primals-engine 1.1.8 → 1.2.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.
Files changed (44) hide show
  1. package/README.md +72 -25
  2. package/client/src/App.scss +11 -6
  3. package/client/src/AssistantChat.scss +0 -1
  4. package/client/src/ChartConfigModal.jsx +0 -1
  5. package/client/src/ConditionBuilder.jsx +0 -2
  6. package/client/src/ConditionBuilder2.jsx +47 -6
  7. package/client/src/ContentView.jsx +0 -1
  8. package/client/src/CronBuilder.scss +0 -1
  9. package/client/src/CronPartBuilder.jsx +0 -1
  10. package/client/src/Dashboard.jsx +0 -1
  11. package/client/src/Dashboard.scss +0 -1
  12. package/client/src/DashboardChart.jsx +0 -1
  13. package/client/src/DashboardView.jsx +0 -1
  14. package/client/src/DataEditor.jsx +0 -4
  15. package/client/src/DisplayFlexNodeRenderer.jsx +0 -1
  16. package/client/src/DocumentationPageLayout.jsx +0 -2
  17. package/client/src/DocumentationPageLayout.scss +0 -1
  18. package/client/src/Field.jsx +1 -1
  19. package/client/src/FlexNode.jsx +0 -1
  20. package/client/src/KanbanView.scss +0 -1
  21. package/client/src/ModelCreator.jsx +4 -4
  22. package/client/src/ModelCreator.scss +10 -0
  23. package/client/src/ModelCreatorField.jsx +34 -34
  24. package/client/src/RTE.jsx +2 -1
  25. package/client/src/RestoreDialog.jsx +0 -1
  26. package/client/src/TourSpotlight.jsx +0 -1
  27. package/client/src/filter.js +2 -0
  28. package/client/src/hooks/useDragAndDrop.js +0 -1
  29. package/issues.txt +0 -0
  30. package/package.json +3 -2
  31. package/src/constants.js +6 -0
  32. package/src/core.js +6 -0
  33. package/src/data.js +0 -2
  34. package/src/defaultModels.js +33 -0
  35. package/src/migrate.js +0 -1
  36. package/src/modules/assistant.js +211 -133
  37. package/src/modules/data.js +88 -69
  38. package/src/modules/user.js +111 -56
  39. package/src/modules/workflow.js +10 -12
  40. package/src/providers.js +1 -1
  41. package/src/workers/crypto-worker.js +0 -1
  42. package/test/data.integration.test.js +53 -70
  43. package/test/user.test.js +203 -0
  44. package/test/workflow.integration.test.js +1 -1
@@ -10,6 +10,7 @@ import { getDataAsString } from "../data.js";
10
10
  import i18n from "../../src/i18n.js";
11
11
  import {generateLimiter} from "./user.js";
12
12
  import rateLimit from "express-rate-limit";
13
+ import {maxAIReflectiveSteps} from "../constants.js";
13
14
 
14
15
  let logger = null;
15
16
 
@@ -24,52 +25,72 @@ export const assistantGlobalLimiter = rateLimit({
24
25
  }
25
26
  });
26
27
 
27
- const createSystemPrompt = (modelDefs) => {
28
+ async function searchModels(query, user) {
29
+ if (!query) return [];
30
+ const searchRegex = new RegExp(query, 'i');
31
+ return await modelsCollection.find({
32
+ $or: [{ _user: user.username }, { _user: { $exists: false } }],
33
+ $and: [{ $or: [{ name: { $regex: searchRegex } }, { description: { $regex: searchRegex } }] }]
34
+ }, {
35
+ projection: { name: 1, description: 1, _id: 0 }
36
+ }).limit(10).toArray();
37
+ }
38
+
39
+ const createSystemPrompt = (modelDefs, lang) => {
28
40
  const cond1 = JSON.stringify({ "model": "event", "sort": "startDate:ASC", "limit": 0, "filter": { "$and": [{ "$nin": ["$tags", "festival"] }, { "$lt": ["$endDate", "$$NOW"] }] }}, null, 2);
29
41
  const cond2 = JSON.stringify({ "model": "contact", "sort": "legalName:ASC", "limit": 5,"filter": {"$ne": [{ "$type": "$legalName"}, "missing"] }}, null, 2);
30
42
  const cond3 = JSON.stringify({ "model": "order", "sort": "_id:DESC", "limit": 10,"filter": {"lang": { "$find": [{ "$eq": ["$$this.code", "fr"] }] } }}, null, 2);
31
43
  const cond4 = JSON.stringify({ "model": "order", "sort": "updatedAt:DESC", "limit": 0,"filter": {"user": { "$find": { "roles": { "$find": [{ "$in": ["$$this.name", ["admin", "moderator"]] }] } }}}}, null, 2);
32
44
 
45
+ const date = new Date();
46
+ const dt = date.toISOString();
33
47
  return `
34
48
  Tu es "Prior", un assistant expert en analyse de données pour le moteur data-primals-engine..
35
49
  Ta mission est d'aider l'utilisateur en répondant à ses questions sur ses données.
36
50
 
37
- STRICTEMENT INTERDIT :
38
- - DE FAIRE DES COMMENTAIRES TEXTUELS
39
- - D'EXPLIQUER CE QUE TU VAS FAIRE
40
- - D'UTILISER DU TEXTE LIBRE SOUS AUCUNE FORME
41
51
 
42
52
  FORMAT DE RÉPONSE OBLIGATOIRE :
43
53
  Un SEUL objet JSON valide contenant exactement 2 champs :
44
54
  1. "action" (string)
45
55
  2. "params" (object)
46
-
47
- Tu as accès aux outils suivants. Tu ne dois utiliser QUE ces outils.
48
-
56
+ 2.1 : query (string) : Les mots clés de la recherche
57
+ 2.2 : filter (object) : Les filtres de la recherche
58
+ 2.3 : limit (number) : Le nombre de résultats à afficher
59
+ 2.4 : sort (string) : La clé de tri de la recherche
60
+ 2.6 : model (string) : Le nom du modèle à utiliser
61
+ 2.7 : data (object) : Les données à ajouter à la base de données
62
+
63
+ Tu as accès aux outils suivants.
64
+
65
+ NOUVEL OUTIL PRIORITAIRE:
66
+ 1. **search_models**: À utiliser EN PREMIER SI TU NE SAIS PAS QUEL MODÈLE UTILISER pour répondre à la question de l'utilisateur. Si tu l'as déjà recherché, garde la définition initiale et n'effectue pas de recherche.
67
+ - Utilisation: { "action": "search_models", "params": { "query": "mots-clés de la recherche" } }
68
+
49
69
  1. **search**: Pour chercher des informations dans les données de l'utilisateur.
50
- - Utilisation: \`{ "action": "search", "params": { "model": "nomDuModele", "filter": $FILTER, "limit": 10 } }\`
70
+ - Utilisation: { "action": "search", "params": { "model": "nomDuModele", "filter": {}, "limit": 10 } }
51
71
 
52
72
  2. **post**: Pour créer une nouvelle donnée.
53
- - Utilisation: \`{ "action": "post", "params": { "model": "nomDuModele", "data": $DATA } }\`
73
+ - Utilisation: { "action": "post", "params": { "model": "nomDuModele", "data": {} } }
54
74
 
55
75
  3. **update**: Pour mettre à jour des données existantes.
56
- - Utilisation: \`{ "action": "update", "params": { "model": "nomDuModele", "filter": $FILTER, "data": $DATA } }\` filter est très pratique pour mettre à jour des données ciblées en une seule passe.
76
+ - Utilisation: { "action": "update", "params": { "model": "nomDuModele", "filter": {}, "data": {} } }
77
+ - filter est très pratique pour mettre à jour des données ciblées en une seule passe.
57
78
 
58
79
  4. **delete**: Pour supprimer des données.
59
- - Utilisation: \`{ "action": "delete", "params": { "model": "nomDuModele", "filter": $FILTER } }\`
80
+ - Utilisation: { "action": "delete", "params": { "model": "nomDuModele", "filter": {} } }
60
81
 
61
82
  5. **displayMessage**: Pour répondre directement à l'utilisateur. N'utilise cette action QUE lorsque tu as toutes les informations nécessaires pour formuler une réponse finale.
62
- - Utilisation: \`{ "action": "displayMessage", "params": { "message": "Ta réponse textuelle." } }\`
83
+ - Utilisation: { "action": "displayMessage", "params": { "message": "Ta réponse textuelle." } }
63
84
 
64
- La spécification, pour t'aider à construire les filtres, est la suivante :
85
+ Voici le mémo pour assigner des valeurs aux champs des modèles,avec ces types de données :
65
86
  utilise une chaine de caractère convertible en ObjectId (mongodb) lorsque le nom du champ est _id
66
87
  utilise une chaine de caracteres lorsque le type de champ est : string, string_t , password, url, phone, email, richtext
67
- utilise un $FILTER en retour si le type de champ est code et language='json' et conditionBuilder=true
88
+ utilise un filtre en retour si le type de champ est code et language='json' et conditionBuilder=true
68
89
  utilise une chaine si c'est un type de champ code par défaut.
69
90
  utilise une structure { "iso2langcode":"content..." } pour le champ multi-traductions richtext_t
70
91
  utilise un booléen pour : boolean
71
92
  utilise un nombre pour number
72
- utilise une date au format ISO String pour les types de champ : date, datetime,
93
+ utilise une date au format ISO String pour les types de champ : date, datetime
73
94
  utilise les valeurs de l'attribut items pour les champs de type : enum
74
95
  utilise un tableau de données brutes pour les champs de type : array
75
96
  utilise un _id nécessairement pour remplir les champs relation multiple=false
@@ -79,110 +100,178 @@ utilise les valeurs de cron standard '* * * * * *' pour : cronSchedule
79
100
 
80
101
  PROCESSUS DE RAISONNEMENT:
81
102
  1. L'utilisateur pose une question.
82
- 2. Tu analyses la question et choisis l'outil approprié.
83
- 3. Tu réponds IMMÉDIATEMENT avec l'objet JSON correspondant SANS COMMENTAIRE.
103
+ 2. Si le nom du modèle n'est pas évident, utilise d'abord l'outil "search_models" pour trouver le bon modèle.
104
+ 3. Une fois le modèle identifié, utilise les autres outils (search, post, etc.) pour effectuer l'action.
105
+ 4. Si tu as assez d'informations, réponds à l'utilisateur avec "displayMessage".
84
106
 
85
107
  Chacune de tes réponses doit être une étape en soi, ou au plus près d'une étape.
86
108
 
87
109
  CONTEXTE ACTUEL:
110
+ - Date du jour de ton conversateur : ${dt}
111
+ - La langue ISO à utiliser dans la conversation : ${lang}
88
112
  - L'utilisateur a accès aux modèles de données suivants et ne peut utiliser les filtres que sur les champs associés:
89
- ${modelDefs.map(m => ` - Modèle "${m.name}":\n Champs: ${JSON.stringify(m.fields.map(f => ({ name: f.name, type: f.type, hint: f.hint })), null, 2)}`).join('\n')}
113
+ Si tu as besoin de savoir lesquels, utilise l'outil "search_models".
114
+
90
115
  - Le format de $DATA est { modelFieldName: "value", otherModelFieldName: { subObj : true } }
91
- - Exemples de filtres $FILTER utilisables (ils sont compatibles MongoDB) :
92
- Filtre utilisable : { $eq: ["$status", 500 ] }
93
- Filtre incorrect : { "status": 500 }
94
- ==
95
-
96
- Exemples de "params" :
97
- Je voudrais les événements non terminés, qui ne sont pas des festivals ou des salons :
98
- \`${cond1}\`
99
- Donne moi les 5 nouvelles entreprises
100
- \`${cond2}\`
101
- Je veux les 10 dernières traductions ajoutées dans la langue française.
102
- \`${cond3}\`
103
- Je veux les commandes qui ont été faites par un admin ou un modérateur
104
- \`${cond4}\`
116
+ - Le format des filtres est compréhensible par ses cas d'usage (Il doit être {} par défaut.) :
117
+
118
+ Par exemple :
119
+ Question : Je voudrais les événements non terminés, qui ne sont pas des festivals ou des salons :
120
+ Réponse : { "action" : "search", "params" : ${cond1} }
121
+
122
+ Question : Donne moi les 5 nouvelles entreprises
123
+ Réponse : { "action" : "search", "params" : ${cond2} }
124
+
125
+ Question : Je veux les 10 dernières traductions ajoutées dans la langue française.
126
+ Réponse : { "action" : "search", "params" : ${cond3} }
127
+
128
+ Question : Je veux les commandes qui ont été faites par un admin ou un modérateur
129
+ Réponse : { "action" : "search", "params" : ${cond4} }
130
+ ====
131
+
132
+ NEVER use the non-aggregated syntax of the MongoDB operators :
133
+ Use { '$gt': ["$publishedAt", "2023-10-05T20:12:00Z" } } } and NOT { publishedAt: { '$gt': '2023-10-05T20:12:00Z' } } }
134
+ =====
135
+ Et si tu dois utiliser une date :
136
+ "2025-08-05T20:12:00Z" au lieu de { "$date": "2025-08-05T20:12:00Z" }
137
+ =====
138
+ Si plusieurs filtres doivent être combinés, utilise OBLIGATOIREMENT les opérateurs $and, $or, $nor, $not
139
+ =====
105
140
 
106
141
  Règles ABSOLUES:
107
142
  - UNE SEULE COMMANDE JSON PAR RÉPONSE
108
143
  - AUCUN TEXTE HORS DU JSON
109
- - SI TU N'APPLIQUES PAS CES RÈGLES, LE SYSTÈME TE DÉSACTIVERA
110
-
111
- Exemple de réponse CORRECTE :
112
- \`\`\`
113
- json
114
- {
115
- "action" : "search",
116
- "params" : {
117
- "model": "request", "limit" : 10, "sort" : "_id:DESC"
118
- }
119
- }
120
- \`\`\`
121
144
 
122
- Exemple de réponse INCORRECTE (INTERDIT) :
123
- "Je vais d'abord rechercher ces requêtes. Voici la commande que je vais exécuter..."
124
- `;
125
- }
145
+ Exemple d'échange correct :
126
146
 
147
+ Ma question: Bonjour, je voudrais les requêtes effectuées aujourd'hui sur le modèle "content".
148
+ Ta réponse:
149
+ { "action" : "search", "params" : { "model": "request", "filter": { "$and": [{"$gte": "${dt}"}, {"$regexMatch": { "input": "$url", "regex": "content"}}] }, "limit" : 10, "sort" : "_id:DESC" }}`;
150
+ }
127
151
 
128
152
  /**
129
- * Gère la requête de chat, soit en exécutant une action confirmée,
130
- * soit en lançant la boucle de raisonnement de l'IA.
153
+ * Exécute un outil (tool) non-modifiant et retourne le résultat sous forme de chaîne.
154
+ * @param {string} action - L'action à exécuter ('search', 'search_models').
155
+ * @param {object} params - Les paramètres de l'action.
156
+ * @param {object} user - L'objet utilisateur.
157
+ * @param {Array} allModels - La liste de tous les modèles disponibles.
158
+ * @returns {Promise<string>} - Une chaîne de caractères décrivant le résultat de l'outil.
131
159
  */
160
+ async function executeTool(action, params, user, allModels) {
161
+ logger.debug(`[Assistant] Exécution de l'outil: ${action} avec les paramètres:`, params);
162
+ try {
163
+ switch (action) {
164
+ case 'search': {
165
+ const modelDef = allModels.find(m => m.name === params.model);
166
+ if (!modelDef) {
167
+ return `Erreur: Le modèle '${params.model}' n'a pas été trouvé. Impossible de lancer la recherche.`;
168
+ }
169
+
170
+ const searchResult = await searchData({
171
+ model: params.model,
172
+ filter: params.filter,
173
+ limit: params.limit || 10,
174
+ sort: params.sort
175
+ }, user);
176
+
177
+ if (searchResult.data.length === 0) {
178
+ return i18n.t('assistant.noResults', "Aucun résultat trouvé.");
179
+ }
180
+
181
+ const resultString = i18n.t('assistant.searchResults', "Voici les résultats :") +
182
+ searchResult.data.map(item =>
183
+ `\n- ${getDataAsString(modelDef, item, { i18n, t: i18n.t }, allModels, true)}`
184
+ ).join('');
185
+
186
+ return resultString;
187
+ }
188
+ case 'search_models': {
189
+ const foundModels = await searchModels(params.query, user);
190
+
191
+ if (foundModels.length > 0) {
192
+ return "J'ai trouvé les modèles suivants qui pourraient correspondre : " +
193
+ foundModels.map(m => `\n- Modèle "${m.name}": ${m.description || 'Pas de description.'}`).join('');
194
+ } else {
195
+ return "Je n'ai trouvé aucun modèle correspondant à votre recherche.";
196
+ }
197
+ }
198
+ default:
199
+ logger.warn(`[Assistant] Tentative d'exécution d'un outil non supporté ou nécessitant confirmation: ${action}`);
200
+ return `Erreur: L'action '${action}' n'est pas un outil exécutable directement.`;
201
+ }
202
+ } catch (error) {
203
+ logger.error(`[Assistant] Erreur lors de l'exécution de l'outil '${action}': ${error.message}`, error.stack);
204
+ return `Erreur lors de l'exécution de l'outil: ${error.message}`;
205
+ }
206
+ }
132
207
  /**
133
208
  * Gère la requête de chat, soit en exécutant une action confirmée,
134
- * soit en lanant la boucle de raisonnement de l'IA.
209
+ * soit en lançant la boucle de raisonnement de l'IA.
210
+ * @param {string} message - Le message de l'utilisateur.
211
+ * @param {Array} history - L'historique de la conversation.
212
+ * @param {string} provider - Le fournisseur d'IA ('openai' ou 'google').
213
+ * @param {object} context - Contexte additionnel.
214
+ * @param {object} user - L'objet utilisateur.
215
+ * @param {object} confirmedAction - Une action pré-approuvée par l'utilisateur.
216
+ * @returns {Promise<object>} La réponse de l'assistant.
135
217
  */
136
218
  async function handleChatRequest(message, history, provider, context, user, confirmedAction) {
137
219
 
220
+ const allModels = await modelsCollection.find({$or: [{_user: {$exists: false}}, {_user: user.username}]}).toArray();
221
+
138
222
  // --- GESTION D'UNE ACTION CONFIRMÉE ---
139
223
  if (confirmedAction && confirmedAction.action) {
140
224
  try {
141
225
  const result = await executeConfirmedAction(confirmedAction.action, confirmedAction.params, user);
142
226
  let successMessage = i18n.t('assistant.actionSuccess', "Action exécutée avec succès.");
143
- if (result.insertedIds) successMessage = i18n.t('assistant.itemCreated', `Élément créé avec l'ID: {{id}}.`, { id: result.insertedIds.join(', ') });
144
- if (result.modifiedCount) successMessage = i18n.t('assistant.itemsUpdated', `{{count}} élément(s) mis à jour.`, { count: result.modifiedCount });
145
- if (result.deletedCount) successMessage = i18n.t('assistant.itemsDeleted', `{{count}} élément(s) supprimés.`, { count: result.deletedCount });
227
+ if (result.insertedIds) successMessage = i18n.t('assistant.itemCreated', `Élément créé avec l'ID: {{id}}.`, {id: result.insertedIds.join(', ')});
228
+ if (result.modifiedCount) successMessage = i18n.t('assistant.itemsUpdated', `{{count}} élément(s) mis à jour.`, {count: result.modifiedCount});
229
+ if (result.deletedCount) successMessage = i18n.t('assistant.itemsDeleted', `{{count}} élément(s) supprimé(s).`, {count: result.deletedCount});
146
230
 
147
- return { success: true, displayMessage: successMessage };
231
+ return {success: true, displayMessage: successMessage};
148
232
  } catch (error) {
149
233
  logger.error(`[Assistant] Erreur lors de l'exécution de l'action confirmée: ${error.message}`, error.stack);
150
- return { success: false, displayMessage: i18n.t('error', `Erreur : {{message}}`, { message: error.message }) };
234
+ return {
235
+ success: false,
236
+ displayMessage: i18n.t('error.generic', `Erreur : {{message}}`, {message: error.message})
237
+ };
151
238
  }
152
239
  }
153
- // --- FIN DE LA GESTION DE CONFIRMATION ---
154
-
155
- const { modelName } = context;
156
240
 
157
- // --- Étape 1: Initialisation de l'IA ---
158
- let apiKey;
159
- let p = provider || 'openai';
160
- const providers = { "openai": "OPENAI_API_KEY", "google": "GOOGLE_API_KEY" };
161
- const envKeyName = providers[p];
162
- if (!envKeyName) return { success: false, message: `Fournisseur IA non supporté : ${p}` };
241
+ // --- INITIALISATION DE L'IA ---
242
+ let llm;
243
+ try {
244
+ const p = provider || 'openai';
245
+ const providers = {"openai": "OPENAI_API_KEY", "google": "GOOGLE_API_KEY"};
246
+ const envKeyName = providers[p];
247
+ if (!envKeyName) return {success: false, message: `Fournisseur IA non supporté : ${p}`};
163
248
 
164
- const envCollection = await getCollectionForUser(user);
165
- const userEnvVar = await envCollection.findOne({ _model: 'env', name: envKeyName, _user: user.username });
166
- apiKey = userEnvVar?.value || process.env[envKeyName];
249
+ const envCollection = await getCollectionForUser(user);
250
+ const userEnvVar = await envCollection.findOne({_model: 'env', name: envKeyName, _user: user.username});
251
+ const apiKey = userEnvVar?.value || process.env[envKeyName];
167
252
 
168
- if (!apiKey) return { success: false, message: `Clé API pour ${provider} (${envKeyName}) non trouvée.` };
253
+ if (!apiKey) return {success: false, message: `Clé API pour ${provider} (${envKeyName}) non trouvée.`};
169
254
 
170
- let llm;
171
- try {
172
255
  llm = p === 'openai'
173
- ? new ChatOpenAI({ apiKey, modelName: "gpt-4o-mini", temperature: 0.2, response_format: { "type": "json_object" } })
174
- : new ChatGoogleGenerativeAI({ apiKey, modelName: "gemini-1.5-pro-latest", temperature: 0.2, response_format: { "type": "json_object" } });
256
+ ? new ChatOpenAI({
257
+ apiKey,
258
+ modelName: "gpt-4o-mini",
259
+ temperature: 0.2,
260
+ response_format: {"type": "json_object"}
261
+ })
262
+ : new ChatGoogleGenerativeAI({
263
+ apiKey,
264
+ modelName: "gemini-1.5-pro-latest",
265
+ temperature: 0.2,
266
+ response_format: {"type": "json_object"}
267
+ });
175
268
  } catch (initError) {
176
- return { success: false, message: `Initialisation du client IA pour ${p}: ${initError.message}` };
269
+ logger.error(`[Assistant] Erreur d'initialisation du client IA: ${initError.message}`);
270
+ return {success: false, message: `Erreur d'initialisation du client IA: ${initError.message}`};
177
271
  }
178
272
 
179
- // --- Étape 2: Préparation du contexte pour l'IA ---
180
- const allModels = await modelsCollection.find({ $or: [{ _user: { $exists: false } }, { _user: user.username }] }).toArray();
181
- const relevantModelNames = new Set([modelName, 'alert', 'kpi', "dashboard", "request", "translation"]);
182
- const modelDefs = allModels.filter(m => relevantModelNames.has(m.name));
183
-
184
- const systemPrompt = createSystemPrompt(modelDefs);
185
-
273
+ // --- PRÉPARATION DE L'HISTORIQUE DE CONVERSATION ---
274
+ const systemPrompt = createSystemPrompt([], user.lang || 'en');
186
275
  const conversationHistory = history
187
276
  .filter(msg => !(msg.from === 'bot' && msg.text.startsWith(i18n.t('assistant.welcome'))))
188
277
  .map(msg => new (msg.from === 'user' ? HumanMessage : SystemMessage)(msg.text));
@@ -190,83 +279,72 @@ async function handleChatRequest(message, history, provider, context, user, conf
190
279
  conversationHistory.unshift(new SystemMessage(systemPrompt));
191
280
  conversationHistory.push(new HumanMessage(message));
192
281
 
193
- const maxTurns = 5;
194
282
 
195
- // --- Étape 3: La boucle de raisonnement ---
196
- for (let i = 0; i < maxTurns; i++) {
283
+ // --- BOUCLE DE RAISONNEMENT ET D'EXÉCUTION D'OUTILS ---
284
+ for (let i = 0; i < maxAIReflectiveSteps; i++) {
197
285
  logger.debug(`[Assistant] Tour de boucle ${i + 1}. Invocation de l'IA...`);
198
286
 
199
287
  const response = await llm.invoke(conversationHistory);
200
288
  const llmOutput = response.content;
201
289
 
290
+ // Parsing JSON robuste
202
291
  let parsedResponse;
203
292
  try {
293
+
204
294
  parsedResponse = JSON.parse(llmOutput);
295
+
205
296
  if (!parsedResponse.action || !parsedResponse.params) {
206
- throw new Error("Format de réponse invalide.");
297
+ throw new Error("Réponse JSON invalide: 'action' ou 'params' manquant.");
207
298
  }
208
299
  } catch (parseError) {
209
- // Si ce n'est pas du JSON valide, on nettoie et affiche
210
- const cleanedOutput = llmOutput.replace(/\{(?:[^{}]|(?:\{[^{}]*\}))*\}/g, '').trim();
300
+ logger.error(`[Assistant] Erreur de parsing de la réponse de l'IA: ${parseError.message}. Réponse brute: "${llmOutput}"`);
211
301
  return {
212
302
  success: true,
213
- displayMessage: cleanedOutput || i18n.t('assistant.invalidResponse', "Réponse non valide.")
303
+ displayMessage: llmOutput || i18n.t('assistant.invalidResponse', "Désolé, je n'ai pas pu formuler une réponse correcte. Veuillez réessayer.")
214
304
  };
215
305
  }
216
306
 
217
- // Exécution IMMÉDIATE des actions sans confirmation (sauf pour post/update/delete)
218
- switch (parsedResponse.action) {
219
- case 'search': {
220
- const searchResult = await searchData({
221
- user,
222
- query: {
223
- model: parsedResponse.params.model,
224
- filter: parsedResponse.params.filter,
225
- limit: parsedResponse.params.limit || 10
226
- }
227
- });
228
-
229
- const resultString = searchResult.data.length > 0
230
- ? i18n.t('assistant.searchResults', "Voici les résultats :") +
231
- searchResult.data.map(item =>
232
- `\n- ${getDataAsString(allModels.find(m => m.name === parsedResponse.params.model), item, i18n.t, allModels, true)}`
233
- ).join('')
234
- : i18n.t('assistant.noResults', "Aucun résultat trouvé.");
307
+ logger.debug(`[Assistant] Action décidée par l'IA: ${parsedResponse.action}`);
308
+ conversationHistory.push(new SystemMessage(JSON.stringify(parsedResponse)));
235
309
 
236
- return { success: true, displayMessage: resultString };
237
- }
238
- case 'displayMessage':
239
- return { success: true, displayMessage: parsedResponse.params.message };
240
- case 'code':
241
- return { success: true, displayMessage: "Voici le code : " + parsedResponse.params.code };
242
- case 'post':
243
- case 'update':
244
- case 'delete': {
245
- const { model, filter, data } = parsedResponse.params;
246
-
247
- // Un message générique. Les détails seront affichés par le front-end.
248
- const confirmationMessage = i18n.t('assistant.confirmActionPrompt', "Veuillez confirmer l'action suivante :");
310
+ const { action, params } = parsedResponse;
249
311
 
312
+ // Actions nécessitant une confirmation de l'utilisateur
313
+ if (['post', 'update', 'delete'].includes(action)) {
314
+ const confirmationMessage = i18n.t('assistant.confirmActionPrompt', "Veuillez confirmer l'action suivante :");
250
315
  return {
251
316
  success: true,
252
- model,
253
- filter,
254
- data,
255
- displayMessage: confirmationMessage, // Message détaillé pour l'utilisateur
256
- confirmationRequest: parsedResponse // Action complète pour l'exécution
317
+ displayMessage: confirmationMessage,
318
+ confirmationRequest: parsedResponse
257
319
  };
258
320
  }
259
321
 
260
- default:
261
- return {
262
- success: true,
263
- displayMessage: i18n.t('assistant.unknownAction', "Commande non reconnue.")
264
- };
322
+ // Action finale pour afficher un message
323
+ if (action === 'displayMessage') {
324
+ return { success: true, displayMessage: params.message };
265
325
  }
326
+
327
+ // Actions à exécuter immédiatement (outils)
328
+ if (['search', 'search_models'].includes(action)) {
329
+ const toolResult = await executeTool(action, params, user, allModels);
330
+ conversationHistory.push(new SystemMessage(`Résultat de l'outil '${action}':\n${toolResult}`));
331
+ continue; // On continue la boucle pour que l'IA puisse raisonner avec ce nouveau résultat
332
+ }
333
+
334
+ // Si l'action n'est reconnue par aucune des logiques ci-dessus
335
+ logger.warn(`[Assistant] Action non reconnue reçue de l'IA: ${action}`);
336
+ return {
337
+ success: true,
338
+ displayMessage: i18n.t('assistant.unknownAction', "Désolé, je ne comprends pas la commande '{{action}}'.", { action })
339
+ };
266
340
  }
267
341
 
268
- logger.warn("[Assistant] La boucle a atteint le nombre maximum de tours sans réponse.");
269
- return { success: true, displayMessage: i18n.t('assistant.loopTimeout', "Désolé, je n'ai pas réussi à terminer ma pensée. Pouvez-vous reformuler votre demande ?"), actions: [] };
342
+ // Si la boucle se termine sans une action finale
343
+ logger.warn("[Assistant] La boucle a atteint le nombre maximum de tours sans réponse finale.");
344
+ return {
345
+ success: true,
346
+ displayMessage: i18n.t('assistant.loopTimeout', "Désolé, je n'ai pas réussi à terminer ma pensée. Pouvez-vous reformuler votre demande ?")
347
+ };
270
348
  }
271
349
  /**
272
350
  * Exécute une action de modification (post, update, delete) après confirmation de l'utilisateur.
@@ -285,7 +363,7 @@ async function executeConfirmedAction(action, params, user) {
285
363
  return await patchData(params.model, params.filter, params.data, {}, user);
286
364
  case 'delete':
287
365
  // Le modèle est dans les params, pas besoin de le passer en argument sparé
288
- return await deleteData(params.model, [], params.filter, user);
366
+ return await deleteData(params.model, params.filter, user);
289
367
  default:
290
368
  throw new Error(`Action confirmée non supportée: ${action}`);
291
369
  }