data-primals-engine 1.6.2-rc1 → 1.6.3
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 +61 -20
- package/client/src/Dashboard.jsx +1 -1
- package/client/src/WorkflowEditor.jsx +101 -0
- package/client/src/WorkflowEditor.scss +29 -4
- package/package.json +145 -142
- package/src/client.js +4 -0
- package/src/defaultModels +1628 -0
- package/src/defaultModels.js +14 -0
- package/src/filter.js +170 -1
- package/src/modules/assistant/assistant.js +38 -56
- package/src/modules/assistant/providers.js +38 -0
- package/src/modules/data/data.history.js +7 -6
- package/src/modules/data/data.operations.js +20 -5
- package/src/modules/user.js +57 -25
- package/src/modules/workflow.js +53 -178
- package/src/providers.js +1 -1
- package/test/assistant.test.js +207 -0
- package/test/config.test.js +41 -0
- package/test/data.history.integration.test.js +144 -20
- package/test/user.test.js +195 -278
- package/test/workflow.integration.test.js +8 -0
package/src/defaultModels.js
CHANGED
|
@@ -14,6 +14,14 @@ export const defaultModels = {
|
|
|
14
14
|
{
|
|
15
15
|
"name": "description",
|
|
16
16
|
"type": "richtext"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "filter",
|
|
20
|
+
"type": "code",
|
|
21
|
+
"language": "json",
|
|
22
|
+
"conditionBuilder": true,
|
|
23
|
+
"required": false,
|
|
24
|
+
"hint": "Filtre JSON qui restreint la portée de cette permission. Le modèle cible est déduit du nom de la permission (ex: 'product.edit')."
|
|
17
25
|
}
|
|
18
26
|
]
|
|
19
27
|
},
|
|
@@ -1111,6 +1119,12 @@ export const defaultModels = {
|
|
|
1111
1119
|
hint: "The current status of the workflow execution."
|
|
1112
1120
|
},
|
|
1113
1121
|
{ name: 'stepExecutionsCount', type: 'object' },
|
|
1122
|
+
{
|
|
1123
|
+
name: 'history',
|
|
1124
|
+
type: 'array',
|
|
1125
|
+
itemsType: 'object',
|
|
1126
|
+
hint: "Stores the detailed execution history of each step and action."
|
|
1127
|
+
},
|
|
1114
1128
|
{ name: 'currentStep', type: 'relation', relation: 'workflowStep', hint: "The step currently being executed or waited on." },
|
|
1115
1129
|
{ name: 'owner', type: 'relation', relation: 'user', required: false },
|
|
1116
1130
|
{ name: 'startedAt', type: 'datetime', required: true, hint: "Timestamp when the workflow run began." },
|
package/src/filter.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {getCollectionForUser, isObjectId} from "./modules/mongodb.js";
|
|
2
|
+
import {getModel} from "./modules/data/index.js";
|
|
3
|
+
import {ObjectId} from "mongodb";
|
|
4
|
+
import {safeAssignObject} from "./core.js";
|
|
1
5
|
|
|
2
6
|
let safeRegex = null; // Par défaut, aucune fonction de validation n'est définie.
|
|
3
7
|
|
|
@@ -9,6 +13,42 @@ export function setSafeRegex(validator) {
|
|
|
9
13
|
safeRegex = validator;
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Récupère une valeur imbriquée dans un objet en utilisant une chaîne de chemin.
|
|
18
|
+
* Gère les tableaux et les objets. Retourne undefined si le chemin n'est pas trouvé.
|
|
19
|
+
* Exemple: getNestedValue({ a: { b: [ { c: 1 } ] } }, 'a.b.0.c') -> 1
|
|
20
|
+
*
|
|
21
|
+
* @param {object} obj L'objet source.
|
|
22
|
+
* @param {string} path La chaîne de chemin (ex: 'user.address.city').
|
|
23
|
+
* @returns {*} La valeur trouvée ou undefined.
|
|
24
|
+
*/
|
|
25
|
+
function getNestedValue(obj, path) {
|
|
26
|
+
// Vérifie si l'objet ou le chemin est invalide
|
|
27
|
+
if (!obj || typeof path !== 'string') {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
// Sépare le chemin en clés individuelles (ex: 'a.b.0.c' -> ['a', 'b', '0', 'c'])
|
|
31
|
+
const keys = path.split('.');
|
|
32
|
+
let current = obj; // Commence à la racine de l'objet
|
|
33
|
+
|
|
34
|
+
// Parcourt chaque clé dans le chemin
|
|
35
|
+
for (const key of keys) {
|
|
36
|
+
// Si à un moment donné on atteint null ou undefined, le chemin est invalide
|
|
37
|
+
if (current === null || current === undefined) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
// Récupère la valeur pour la clé actuelle
|
|
41
|
+
const value = current[key];
|
|
42
|
+
// Si la valeur est undefined, le chemin est invalide
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
// Passe au niveau suivant de l'objet/tableau
|
|
47
|
+
current = value;
|
|
48
|
+
}
|
|
49
|
+
// Retourne la valeur finale trouvée
|
|
50
|
+
return current;
|
|
51
|
+
}
|
|
12
52
|
/**
|
|
13
53
|
* Evaluates a single condition against form data.
|
|
14
54
|
* @param {object} currentModelDef - The definition of the current model.
|
|
@@ -272,4 +312,133 @@ export const isConditionMet = (model, cond, formData, allModels, user,checkRegex
|
|
|
272
312
|
}
|
|
273
313
|
|
|
274
314
|
return evaluateSingleCondition(model, condition, formData, allModels, user, checkRegex);
|
|
275
|
-
};
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Remplace les placeholders dans un template (string, object, array) par des valeurs du contextData.
|
|
321
|
+
* Version améliorée avec support des chemins complexes via resolvePathValue.
|
|
322
|
+
*/
|
|
323
|
+
export async function substituteVariables(template, contextData, user) {
|
|
324
|
+
// 1. Retourner les types non substituables tels quels
|
|
325
|
+
if (template === null || (typeof template !== 'string' && typeof template !== 'object')) {
|
|
326
|
+
return template;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 2. Gérer les tableaux de manière récursive
|
|
330
|
+
if (Array.isArray(template)) {
|
|
331
|
+
return Promise.all(template.map(item => substituteVariables(item, contextData, user)));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 3. Gérer les objets de manière récursive
|
|
335
|
+
if (typeof template === 'object') {
|
|
336
|
+
const newObj = {};
|
|
337
|
+
for (const key in template) {
|
|
338
|
+
if (Object.prototype.hasOwnProperty.call(template, key)) {
|
|
339
|
+
const val = await substituteVariables(template[key], contextData, user);
|
|
340
|
+
safeAssignObject(newObj, key, val);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return newObj;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// --- À partir d'ici, nous savons que `template` est une chaîne de caractères ---
|
|
347
|
+
|
|
348
|
+
// 4. Construire le contexte complet pour la substitution
|
|
349
|
+
const dbCollection = await getCollectionForUser(user);
|
|
350
|
+
const userEnvVars = await dbCollection.find({ _model: 'env', _user: user.username }).toArray();
|
|
351
|
+
const userEnv = userEnvVars.reduce((acc, v) => ({ ...acc, [v.name]: v.value }), {});
|
|
352
|
+
|
|
353
|
+
// `contextToSearch` contient toutes les données disponibles à sa racine
|
|
354
|
+
const contextToSearch = { ...contextData, env: userEnv };
|
|
355
|
+
|
|
356
|
+
// 5. Logique de résolution de valeur améliorée avec resolvePathValue
|
|
357
|
+
const findValue = async (key) => {
|
|
358
|
+
let path = key.trim();
|
|
359
|
+
if (path.startsWith('context.')) {
|
|
360
|
+
path = path.substring('context.'.length);
|
|
361
|
+
}
|
|
362
|
+
if (path.endsWith('._id')) {
|
|
363
|
+
const basePath = path.slice(0, -4);
|
|
364
|
+
const value = await findValue(basePath);
|
|
365
|
+
return value?._id?.toString(); // Convertit l'ObjectId en string
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Gérer les valeurs dynamiques spéciales
|
|
369
|
+
if (path === 'now') {
|
|
370
|
+
return new Date().toISOString();
|
|
371
|
+
} else if (path === 'randomUUID') {
|
|
372
|
+
return crypto.randomUUID();
|
|
373
|
+
} else if( path === "baseUrl" ){
|
|
374
|
+
return process.env.NODE_ENV === 'production' ? 'https://'+getHost()+'/' : 'http://localhost:/'+port;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Détecter si le chemin est complexe (contient plus d'un point)
|
|
378
|
+
if (path.split('.').length > 1) {
|
|
379
|
+
try {
|
|
380
|
+
// Essayer de résoudre le chemin avec resolvePathValue
|
|
381
|
+
const [root, ...rest] = path.split('.');
|
|
382
|
+
// On vérifie si la racine du chemin (ex: 'triggerData') existe dans notre contexte
|
|
383
|
+
if (contextToSearch[root]) {
|
|
384
|
+
const resolvedValue = await resolvePathValue(
|
|
385
|
+
rest.join('.'),
|
|
386
|
+
contextToSearch[root], // On passe le bon objet de départ (ex: l'objet triggerData)
|
|
387
|
+
user
|
|
388
|
+
);
|
|
389
|
+
if (resolvedValue !== undefined) {
|
|
390
|
+
return resolvedValue;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.warn(`Erreur lors de la résolution du chemin "${path}":`, error.message);
|
|
395
|
+
// On continue avec la méthode normale si la résolution échoue
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Fallback: chercher le chemin dans l'objet de contexte normal
|
|
400
|
+
return getNestedValue(contextToSearch, path);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// CAS A : La chaîne est un unique placeholder (ex: "{context.triggerData.product.price}")
|
|
404
|
+
const singlePlaceholderMatch = template.match(/^\{([^}]+)\}$/);
|
|
405
|
+
if (singlePlaceholderMatch) {
|
|
406
|
+
const key = singlePlaceholderMatch[1];
|
|
407
|
+
const value = await findValue(key);
|
|
408
|
+
|
|
409
|
+
if (value === undefined) {
|
|
410
|
+
return template; // Placeholder not found, return as is.
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// If the resolved value is a string, it might contain more placeholders.
|
|
414
|
+
// We recursively call substituteVariables on it, but only if it's different
|
|
415
|
+
// from the original template to prevent infinite loops.
|
|
416
|
+
if (typeof value === 'string' && value !== template) {
|
|
417
|
+
return substituteVariables(value, contextData, user);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// For non-string values or if value is same as template, return the value.
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// CAS B : La chaîne contient plusieurs placeholders ou mix texte/variables
|
|
425
|
+
const placeholderRegex = /\{([^}]+)\}/g;
|
|
426
|
+
const placeholders = [...template.matchAll(placeholderRegex)];
|
|
427
|
+
|
|
428
|
+
// Si aucun placeholder trouvé, retourner la chaîne telle quelle
|
|
429
|
+
if (placeholders.length === 0) {
|
|
430
|
+
return template;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Remplacer chaque placeholder de manière asynchrone
|
|
434
|
+
let result = template;
|
|
435
|
+
for (const [match, key] of placeholders) {
|
|
436
|
+
const value = await findValue(key);
|
|
437
|
+
const replacement = value !== undefined
|
|
438
|
+
? (value === null ? 'null' : typeof value === 'object' ? JSON.stringify(value) : String(value))
|
|
439
|
+
: match;
|
|
440
|
+
result = result.replace(match, replacement);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getCollectionForUser, modelsCollection } from "../mongodb.js";
|
|
2
2
|
import { Logger } from "../../gameObject.js";
|
|
3
3
|
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
4
|
-
import {searchData,
|
|
4
|
+
import { searchData, patchData, deleteData, insertData } from "../data/index.js";
|
|
5
|
+
import { getAIProvider } from "./providers.js";
|
|
5
6
|
import {providers} from "./constants.js";
|
|
6
7
|
import {Config} from "../../config.js";
|
|
7
8
|
import { Event } from "../../events.js";
|
|
@@ -13,38 +14,6 @@ import {parseSafeJSON} from "../../core.js";
|
|
|
13
14
|
|
|
14
15
|
let logger = null;
|
|
15
16
|
|
|
16
|
-
export const getAIProvider= async (aiProvider, aiModel, apiKey)=>{
|
|
17
|
-
const maxTokens = Config.Get('assistant.maxTokens', assistantConfig.maxTokens);
|
|
18
|
-
try {
|
|
19
|
-
switch (aiProvider) {
|
|
20
|
-
case 'OpenAI': {
|
|
21
|
-
const { ChatOpenAI } = await import("@langchain/openai");
|
|
22
|
-
return new ChatOpenAI({apiKey, model: aiModel, temperature: 0.7, maxTokens});
|
|
23
|
-
}
|
|
24
|
-
case 'Google': {
|
|
25
|
-
const { ChatGoogleGenerativeAI } = await import("@langchain/google-genai");
|
|
26
|
-
return new ChatGoogleGenerativeAI({apiKey, model: aiModel, temperature: 0.7, maxTokens});
|
|
27
|
-
}
|
|
28
|
-
case 'DeepSeek': {
|
|
29
|
-
const { ChatDeepSeek } = await import("@langchain/deepseek");
|
|
30
|
-
return new ChatDeepSeek({apiKey, model: aiModel, temperature: 0.7, maxTokens});
|
|
31
|
-
}
|
|
32
|
-
case 'Anthropic': {
|
|
33
|
-
const { ChatAnthropic } = await import("@langchain/anthropic");
|
|
34
|
-
return new ChatAnthropic({apiKey, model: aiModel, temperature: 0.7, maxTokens});
|
|
35
|
-
}
|
|
36
|
-
default:
|
|
37
|
-
throw new Error(`Unsupported AI provider: ${aiProvider}`);
|
|
38
|
-
}
|
|
39
|
-
} catch (e) {
|
|
40
|
-
if (e.code === 'ERR_MODULE_NOT_FOUND') {
|
|
41
|
-
logger.error(`[Assistant] The package for the '${aiProvider}' provider is not installed. Please run 'npm install @langchain/${aiProvider.toLowerCase()}' to use this provider.`);
|
|
42
|
-
throw new Error(`The AI provider '${aiProvider}' is not installed. Please ask the administrator to install the corresponding package.`);
|
|
43
|
-
}
|
|
44
|
-
logger.error(`[Assistant] Error initializing AI provider '${aiProvider}': ${e.message}`);
|
|
45
|
-
throw e; // Re-throw other errors
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
17
|
export const assistantGlobalLimiter = rateLimit({
|
|
49
18
|
windowMs: 15 * 60 * 1000, // Fenêtre de 15 minutes
|
|
50
19
|
max: 100, // Limite à 100 requêtes globales pour l'assistant pendant la fenêtre (vous pouvez ajuster cette valeur)
|
|
@@ -101,7 +70,7 @@ const createSystemPrompt = (modelDefs, lang) => {
|
|
|
101
70
|
|
|
102
71
|
const date = new Date();
|
|
103
72
|
const dt = date.toISOString();
|
|
104
|
-
return `
|
|
73
|
+
return Config.Get('systemPrompt', `
|
|
105
74
|
Tu es "Prior", un assistant expert en analyse de données pour le moteur data-primals-engine..
|
|
106
75
|
Ta mission est d'aider l'utilisateur en répondant à ses questions sur ses données.
|
|
107
76
|
|
|
@@ -333,7 +302,7 @@ Ma question: Bonjour, je voudrais les requêtes effectuées aujourd'hui sur le m
|
|
|
333
302
|
Ta réponse: [
|
|
334
303
|
{ "action" : "search_models", "params": { "query": "request" } },
|
|
335
304
|
{ "action" : "search", "params" : { "model": "request", "filter": { "$and": [{"$gte": ["$createdAt", "${dt}"]}, {"$regexMatch": { "input": "$url", "regex": "content"}}] }, "limit" : 10, "sort" : "_id:DESC" } }
|
|
336
|
-
]
|
|
305
|
+
]`);
|
|
337
306
|
}
|
|
338
307
|
|
|
339
308
|
/**
|
|
@@ -475,7 +444,7 @@ export async function handleChatRequest(params, user, sendEvent = null) {
|
|
|
475
444
|
return errorResult;
|
|
476
445
|
}
|
|
477
446
|
|
|
478
|
-
const systemPrompt = await Event.Trigger('OnSystemPrompt', 'event', 'user', user);
|
|
447
|
+
const systemPrompt = (await Event.Trigger('OnSystemPrompt', 'event', 'user', user)) || createSystemPrompt([], user.lang || 'en');
|
|
479
448
|
|
|
480
449
|
const conversationHistory = (history || [])
|
|
481
450
|
.filter(msg => msg.text && !(msg.from === 'bot' && msg.text.startsWith(i18n.t('assistant.welcome'))))
|
|
@@ -559,11 +528,12 @@ export async function handleChatRequest(params, user, sendEvent = null) {
|
|
|
559
528
|
return result;
|
|
560
529
|
}
|
|
561
530
|
|
|
562
|
-
|
|
531
|
+
const toolResults = [];
|
|
532
|
+
let finalActionResult = null;
|
|
533
|
+
|
|
563
534
|
for (const command of commands) {
|
|
564
535
|
logger.debug(`[Assistant] Action décidée par l'IA: ${command.action}`, command);
|
|
565
536
|
if (sendEvent) sendEvent('action', { action: command.action, params: command.params });
|
|
566
|
-
conversationHistory.push(new SystemMessage(JSON.stringify(command)));
|
|
567
537
|
|
|
568
538
|
const { action, params: parsedParams } = command;
|
|
569
539
|
|
|
@@ -576,40 +546,52 @@ export async function handleChatRequest(params, user, sendEvent = null) {
|
|
|
576
546
|
// Outils pour le raisonnement interne de l'IA
|
|
577
547
|
if (['search_models'].includes(action)) {
|
|
578
548
|
const toolResult = await executeTool(action, parsedParams, user, allModels);
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
hasContinued = true; // On indique qu'on doit continuer la boucle principale
|
|
582
|
-
continue; // On passe à la commande suivante dans la liste de l'IA
|
|
549
|
+
toolResults.push({ action, result: toolResult });
|
|
550
|
+
continue; // Passe à la commande suivante sans vérifier si c'est une action finale
|
|
583
551
|
}
|
|
584
552
|
|
|
585
553
|
// Actions finales
|
|
586
|
-
const res = await Event.Trigger('OnChatAction', 'event', 'user', action,
|
|
554
|
+
const res = await Event.Trigger('OnChatAction', 'event', 'user', action, parsedParams, command, llmOptions, user, params);
|
|
587
555
|
|
|
588
|
-
if (
|
|
556
|
+
if (res) {
|
|
557
|
+
// On a trouvé une action finale. On la stocke et on arrête de chercher.
|
|
558
|
+
finalActionResult = res;
|
|
559
|
+
break; // Sort de la boucle des commandes
|
|
560
|
+
} else {
|
|
589
561
|
// Si l'action n'est reconnue par aucune des logiques ci-dessus
|
|
590
562
|
logger.warn(`[Assistant] Action non reconnue reçue de l'IA: ${action}`);
|
|
591
|
-
|
|
563
|
+
finalActionResult = {
|
|
592
564
|
success: true,
|
|
593
565
|
displayMessage: i18n.t('assistant.unknownAction', "Désolé, je ne comprends pas la commande '{{action}}'.", { action })
|
|
594
566
|
};
|
|
595
|
-
|
|
596
|
-
sendEvent('final_result', result);
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
return result;
|
|
567
|
+
break; // Sortir de la boucle des commandes
|
|
600
568
|
}
|
|
601
|
-
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Si une action finale a été exécutée, on retourne son résultat.
|
|
572
|
+
if (finalActionResult) {
|
|
602
573
|
if (sendEvent) {
|
|
603
|
-
sendEvent('final_result',
|
|
574
|
+
sendEvent('final_result', finalActionResult);
|
|
604
575
|
return;
|
|
605
576
|
}
|
|
606
|
-
return
|
|
577
|
+
return finalActionResult;
|
|
607
578
|
}
|
|
608
579
|
|
|
609
|
-
// Si on a
|
|
610
|
-
if (
|
|
580
|
+
// Si on a uniquement des résultats d'outils, on les ajoute à l'historique et on continue la boucle.
|
|
581
|
+
if (toolResults.length > 0) {
|
|
582
|
+
let toolResponse = "";
|
|
583
|
+
for (const tool of toolResults) {
|
|
584
|
+
if (sendEvent) sendEvent('tool_result', { action: tool.action, result: tool.result });
|
|
585
|
+
toolResponse += `Résultat de l'outil '${tool.action}':\n${tool.result}\n\n`;
|
|
586
|
+
}
|
|
587
|
+
conversationHistory.push(new SystemMessage(toolResponse));
|
|
611
588
|
continue;
|
|
612
589
|
}
|
|
590
|
+
|
|
591
|
+
// Si l'IA ne retourne ni outil ni action finale reconnue, on arrête.
|
|
592
|
+
if (commands.length > 0) {
|
|
593
|
+
logger.warn(`[Assistant] L'IA a retourné des commandes mais aucune n'était un outil ou une action finale reconnue. Commandes:`, commands);
|
|
594
|
+
}
|
|
613
595
|
}
|
|
614
596
|
|
|
615
597
|
// Si la boucle se termine sans une action finale
|
|
@@ -656,7 +638,7 @@ async function executeConfirmedAction(action, params, user) {
|
|
|
656
638
|
* @param {object} user - L'objet utilisateur.
|
|
657
639
|
* @returns {Promise<object|boolean>} - L'objet de réponse final ou false si l'action n'est pas reconnue.
|
|
658
640
|
*/
|
|
659
|
-
async function handleFinalChatAction(action, params, parsedResponse, user) {
|
|
641
|
+
async function handleFinalChatAction(action, params, parsedResponse, llmOptions, user) {
|
|
660
642
|
// Action de génération de graphique, gérée par le front-end
|
|
661
643
|
if (action === 'generateChart') {
|
|
662
644
|
return {success: true, chartConfig: params};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Config } from "../../config.js";
|
|
2
|
+
import { assistantConfig } from "../../constants.js";
|
|
3
|
+
import { Logger } from "../../gameObject.js";
|
|
4
|
+
|
|
5
|
+
const logger = new Logger("AssistantProviders");
|
|
6
|
+
|
|
7
|
+
export const getAIProvider = async (aiProvider, aiModel, apiKey) => {
|
|
8
|
+
const maxTokens = Config.Get('assistant.maxTokens', assistantConfig.maxTokens);
|
|
9
|
+
try {
|
|
10
|
+
switch (aiProvider) {
|
|
11
|
+
case 'OpenAI': {
|
|
12
|
+
const { ChatOpenAI } = await import("@langchain/openai");
|
|
13
|
+
return new ChatOpenAI({ apiKey, model: aiModel, temperature: 0.7, maxTokens });
|
|
14
|
+
}
|
|
15
|
+
case 'Google': {
|
|
16
|
+
const { ChatGoogleGenerativeAI } = await import("@langchain/google-genai");
|
|
17
|
+
return new ChatGoogleGenerativeAI({ apiKey, model: aiModel, temperature: 0.7, maxTokens });
|
|
18
|
+
}
|
|
19
|
+
case 'DeepSeek': {
|
|
20
|
+
const { ChatDeepSeek } = await import("@langchain/deepseek");
|
|
21
|
+
return new ChatDeepSeek({ apiKey, model: aiModel, temperature: 0.7, maxTokens });
|
|
22
|
+
}
|
|
23
|
+
case 'Anthropic': {
|
|
24
|
+
const { ChatAnthropic } = await import("@langchain/anthropic");
|
|
25
|
+
return new ChatAnthropic({ apiKey, model: aiModel, temperature: 0.7, maxTokens });
|
|
26
|
+
}
|
|
27
|
+
default:
|
|
28
|
+
throw new Error(`Unsupported AI provider: ${aiProvider}`);
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {
|
|
31
|
+
if (e.code === 'ERR_MODULE_NOT_FOUND') {
|
|
32
|
+
logger.error(`[Assistant] The package for the '${aiProvider}' provider is not installed. Please run 'npm install @langchain/${aiProvider.toLowerCase()}' to use this provider.`);
|
|
33
|
+
throw new Error(`The AI provider '${aiProvider}' is not installed. Please ask the administrator to install the corresponding package.`);
|
|
34
|
+
}
|
|
35
|
+
logger.error(`[Assistant] Error initializing AI provider '${aiProvider}': ${e.message}`);
|
|
36
|
+
throw e; // Re-throw other errors
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -323,17 +323,14 @@ export function onInit(defaultEngine) {
|
|
|
323
323
|
engine.post('/api/data/history/:modelName/:recordId/revert/:version', [middlewareAuthenticator, userInitiator], handleRevertToRevisionRequest);
|
|
324
324
|
|
|
325
325
|
// --- Écouteur pour la CRÉATION de données (Version 1 - Snapshot) ---
|
|
326
|
-
Event.Listen("OnDataAdded", async (engine, { modelName,
|
|
326
|
+
Event.Listen("OnDataAdded", async (engine, { modelName, insertedDocs, user }) => {
|
|
327
327
|
try {
|
|
328
328
|
const model = await getModel(modelName, user);
|
|
329
329
|
if (!model?.history?.enabled) return;
|
|
330
330
|
|
|
331
|
-
const dataCollection = await getCollectionForUser(user);
|
|
332
331
|
const historyCollection = getCollection('history');
|
|
333
332
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
for (const doc of newDocs) {
|
|
333
|
+
for (const doc of insertedDocs) {
|
|
337
334
|
await historyCollection.insertOne({
|
|
338
335
|
documentId: doc._id,
|
|
339
336
|
model: modelName,
|
|
@@ -394,7 +391,11 @@ export function onInit(defaultEngine) {
|
|
|
394
391
|
}
|
|
395
392
|
|
|
396
393
|
// If this is the first history record for this document, add a snapshot of the 'before' state.
|
|
397
|
-
|
|
394
|
+
// --- FIX ---
|
|
395
|
+
// The check should be based on the calculated version number, not on the existence of lastVersionDoc,
|
|
396
|
+
// which could be affected by race conditions in a busy environment. If we are creating version 1,
|
|
397
|
+
// it means no prior history exists for this document, so we must add the snapshot.
|
|
398
|
+
if (newVersion === 1) {
|
|
398
399
|
historyEntry.snapshot = beforeDoc;
|
|
399
400
|
logger.debug(`History v${newVersion} (update with initial snapshot) created for ${modelName} document ${afterDoc._id}`);
|
|
400
401
|
} else {
|
|
@@ -817,7 +817,7 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
|
|
|
817
817
|
}
|
|
818
818
|
|
|
819
819
|
// System specific event
|
|
820
|
-
const eventPayload = {modelName,
|
|
820
|
+
const eventPayload = {modelName, insertedDocs, user};
|
|
821
821
|
await Event.Trigger("OnDataAdded", "event", "system", engine, eventPayload);
|
|
822
822
|
|
|
823
823
|
// User specific event
|
|
@@ -829,7 +829,10 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
|
|
|
829
829
|
// --- CORRECTION ---
|
|
830
830
|
// On renvoie l'objet complet du premier document inséré (cas le plus courant pour l'undo)
|
|
831
831
|
// et la liste complète des IDs pour les cas de bulk insert.
|
|
832
|
-
|
|
832
|
+
let firstInsertedDoc = insertedDocs.length > 0 ? { ...insertedDocs[0] } : null;
|
|
833
|
+
if (firstInsertedDoc?._id) {
|
|
834
|
+
firstInsertedDoc._id = firstInsertedDoc._id.toString();
|
|
835
|
+
}
|
|
833
836
|
return {success: true, data: firstInsertedDoc, insertedIds: insertedIds.map(id => id.toString())};
|
|
834
837
|
|
|
835
838
|
} catch (error) { // Attrape les erreurs de permission ou de pushDataUnsecure
|
|
@@ -1419,6 +1422,12 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
|
|
|
1419
1422
|
before: existingDocs, // Documents avant la modification
|
|
1420
1423
|
after: updatedDocs // Documents après la modification
|
|
1421
1424
|
});
|
|
1425
|
+
await Event.Trigger("OnDataEdited", "event", "user", engine, {
|
|
1426
|
+
modelName,
|
|
1427
|
+
user,
|
|
1428
|
+
before: existingDocs, // Documents avant la modification
|
|
1429
|
+
after: updatedDocs // Documents après la modification
|
|
1430
|
+
});
|
|
1422
1431
|
}
|
|
1423
1432
|
|
|
1424
1433
|
// 11. Tâches post-mise à jour (schedules, workflows) (inchangé)
|
|
@@ -1670,9 +1679,15 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
|
|
|
1670
1679
|
logger.info(`[deleteData] No documents to delete for user ${user?.username} after permission checks or matching criteria.`);
|
|
1671
1680
|
}
|
|
1672
1681
|
|
|
1673
|
-
const res = {success: true, deletedCount}
|
|
1674
|
-
|
|
1675
|
-
|
|
1682
|
+
const res = { success: true, deletedCount };
|
|
1683
|
+
|
|
1684
|
+
// --- CORRECTION ---
|
|
1685
|
+
// The event payload must match what the history listener expects: { modelName, user, before }.
|
|
1686
|
+
// 'documentsToDelete' contains the full documents before they were deleted.
|
|
1687
|
+
const eventPayload = { modelName: modelNameToInvalidate, user, before: documentsToDelete };
|
|
1688
|
+
const plugin = await Event.Trigger("OnDataDeleted", "event", "system", engine, eventPayload);
|
|
1689
|
+
await Event.Trigger("OnDataDeleted", "event", "user", eventPayload);
|
|
1690
|
+
|
|
1676
1691
|
return plugin || res;
|
|
1677
1692
|
|
|
1678
1693
|
} catch (error) {
|