data-primals-engine 1.1.8 → 1.2.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/README.md +65 -22
- package/client/src/AssistantChat.scss +0 -1
- package/client/src/ChartConfigModal.jsx +0 -1
- package/client/src/ConditionBuilder.jsx +0 -2
- package/client/src/ContentView.jsx +0 -1
- package/client/src/CronBuilder.scss +0 -1
- package/client/src/CronPartBuilder.jsx +0 -1
- package/client/src/Dashboard.jsx +0 -1
- package/client/src/Dashboard.scss +0 -1
- package/client/src/DashboardChart.jsx +0 -1
- package/client/src/DashboardView.jsx +0 -1
- package/client/src/DataEditor.jsx +0 -4
- package/client/src/DisplayFlexNodeRenderer.jsx +0 -1
- package/client/src/DocumentationPageLayout.jsx +0 -2
- package/client/src/DocumentationPageLayout.scss +0 -1
- package/client/src/FlexNode.jsx +0 -1
- package/client/src/KanbanView.scss +0 -1
- package/client/src/RestoreDialog.jsx +0 -1
- package/client/src/TourSpotlight.jsx +0 -1
- package/client/src/hooks/useDragAndDrop.js +0 -1
- package/issues.txt +0 -0
- package/package.json +1 -1
- package/src/constants.js +6 -0
- package/src/data.js +0 -2
- package/src/migrate.js +0 -1
- package/src/modules/assistant.js +211 -133
- package/src/modules/data.js +88 -69
- package/src/modules/workflow.js +10 -12
- package/src/providers.js +1 -1
- package/src/workers/crypto-worker.js +0 -1
- package/test/data.integration.test.js +53 -70
- package/test/workflow.integration.test.js +1 -1
package/src/modules/assistant.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
73
|
+
- Utilisation: { "action": "post", "params": { "model": "nomDuModele", "data": {} } }
|
|
54
74
|
|
|
55
75
|
3. **update**: Pour mettre à jour des données existantes.
|
|
56
|
-
- Utilisation:
|
|
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:
|
|
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:
|
|
83
|
+
- Utilisation: { "action": "displayMessage", "params": { "message": "Ta réponse textuelle." } }
|
|
63
84
|
|
|
64
|
-
|
|
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
|
|
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.
|
|
83
|
-
3.
|
|
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
|
-
|
|
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
|
-
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Je veux les commandes qui ont été faites par un admin ou un modérateur
|
|
104
|
-
|
|
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
|
|
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
|
-
*
|
|
130
|
-
*
|
|
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
|
|
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}}.`, {
|
|
144
|
-
if (result.modifiedCount) successMessage = i18n.t('assistant.itemsUpdated', `{{count}} élément(s) mis à jour.`, {
|
|
145
|
-
if (result.deletedCount) successMessage = i18n.t('assistant.itemsDeleted', `{{count}} élément(s)
|
|
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 {
|
|
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 {
|
|
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
|
-
// ---
|
|
158
|
-
let
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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({
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
// ---
|
|
180
|
-
const
|
|
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
|
-
// ---
|
|
196
|
-
for (let i = 0; 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("
|
|
297
|
+
throw new Error("Réponse JSON invalide: 'action' ou 'params' manquant.");
|
|
207
298
|
}
|
|
208
299
|
} catch (parseError) {
|
|
209
|
-
|
|
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:
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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,
|
|
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
|
}
|