/** * PipelineExecutor.js * * Moteur d'exécution des pipelines modulaires flexibles. * Orchestre l'exécution séquentielle des modules avec gestion d'état. */ const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { PipelineDefinition } = require('./PipelineDefinition'); const { getPersonalities, readInstructionsData, selectPersonalityWithAI } = require('../BrainConfig'); const { extractElements, buildSmartHierarchy } = require('../ElementExtraction'); const { generateMissingKeywords, generateMissingSheetVariables } = require('../MissingKeywords'); const { injectGeneratedContent } = require('../ContentAssembly'); const { saveGeneratedArticleOrganic } = require('../ArticleStorage'); const fs = require('fs').promises; const path = require('path'); // Modules d'exécution const { generateSimple } = require('../selective-enhancement/SelectiveUtils'); const { applySelectiveLayer } = require('../selective-enhancement/SelectiveCore'); const { applyPredefinedStack: applySelectiveStack } = require('../selective-enhancement/SelectiveLayers'); const { SmartTouchCore } = require('../selective-smart-touch/SmartTouchCore'); // ✅ NOUVEAU: SelectiveSmartTouch const { applyAdversarialLayer } = require('../adversarial-generation/AdversarialCore'); const { applyPredefinedStack: applyAdversarialStack } = require('../adversarial-generation/AdversarialLayers'); const { applyHumanSimulationLayer } = require('../human-simulation/HumanSimulationCore'); const { applyPredefinedSimulation } = require('../human-simulation/HumanSimulationLayers'); const { applyPatternBreakingLayer } = require('../pattern-breaking/PatternBreakingCore'); const { applyPatternBreakingStack } = require('../pattern-breaking/PatternBreakingLayers'); /** * Classe PipelineExecutor */ class PipelineExecutor { constructor() { this.currentContent = null; this.executionLog = []; this.checkpoints = []; this.versionHistory = []; // ✅ Historique des versions sauvegardées this.parentArticleId = null; // ✅ ID parent pour versioning this.csvData = null; // ✅ Données CSV pour sauvegarde this.finalElements = null; // ✅ Éléments extraits pour assemblage this.versionPaths = []; // ✅ NOUVEAU: Chemins des versions JSON locales this.metadata = { startTime: null, endTime: null, totalDuration: 0, personality: null }; } /** * Exécute un pipeline complet */ async execute(pipelineConfig, rowNumber, options = {}) { return tracer.run('PipelineExecutor.execute', async () => { logSh(`🚀 Démarrage pipeline "${pipelineConfig.name}" (${pipelineConfig.pipeline.length} étapes)`, 'INFO'); // Validation const validation = PipelineDefinition.validate(pipelineConfig); if (!validation.valid) { throw new Error(`Pipeline invalide: ${validation.errors.join(', ')}`); } this.metadata.startTime = Date.now(); this.executionLog = []; this.checkpoints = []; this.versionHistory = []; // ✅ Reset version history this.parentArticleId = null; // ✅ Reset parent ID this.versionPaths = []; // ✅ Reset version paths // ✅ NOUVEAU: Créer outputDir si saveAllVersions activé if (options.saveAllVersions && options.outputDir) { await fs.mkdir(options.outputDir, { recursive: true }); logSh(`📁 Dossier versions créé: ${options.outputDir}`, 'DEBUG'); } // Charger les données const csvData = await this.loadData(rowNumber); this.csvData = csvData; // ✅ Stocker pour sauvegarde // Exécuter les étapes const enabledSteps = pipelineConfig.pipeline.filter(s => s.enabled !== false); for (let i = 0; i < enabledSteps.length; i++) { const step = enabledSteps[i]; try { logSh(`▶ Étape ${step.step}/${pipelineConfig.pipeline.length}: ${step.module} (${step.mode})`, 'INFO'); const stepStartTime = Date.now(); const result = await this.executeStep(step, csvData, options); const stepDuration = Date.now() - stepStartTime; // Log l'étape this.executionLog.push({ step: step.step, module: step.module, mode: step.mode, intensity: step.intensity, duration: stepDuration, modifications: result.modifications || 0, success: true, timestamp: new Date().toISOString() }); // Mise à jour du contenu if (result.content) { this.currentContent = result.content; } // Checkpoint si demandé if (step.saveCheckpoint) { this.checkpoints.push({ step: step.step, content: this.currentContent, timestamp: new Date().toISOString() }); logSh(`💾 Checkpoint sauvegardé (étape ${step.step})`, 'DEBUG'); } // ✅ Sauvegarde Google Sheets si activée if (options.saveIntermediateSteps && this.currentContent) { await this.saveStepVersion(step, result.modifications || 0, pipelineConfig.name); } // ✅ NOUVEAU: Sauvegarde JSON locale si saveAllVersions activé if (options.saveAllVersions && options.outputDir && this.currentContent) { await this.saveVersionJSON(step, i, options.outputDir); } logSh(`✔ Étape ${step.step} terminée (${stepDuration}ms, ${result.modifications || 0} modifs)`, 'INFO'); } catch (error) { logSh(`✖ Erreur étape ${step.step}: ${error.message}`, 'ERROR'); this.executionLog.push({ step: step.step, module: step.module, mode: step.mode, success: false, error: error.message, timestamp: new Date().toISOString() }); // Propager l'erreur ou continuer selon options if (options.stopOnError !== false) { throw error; } } } this.metadata.endTime = Date.now(); this.metadata.totalDuration = this.metadata.endTime - this.metadata.startTime; // ✅ NOUVEAU: Sauvegarder version finale v2.0 si saveAllVersions activé if (options.saveAllVersions && options.outputDir && this.currentContent) { const finalVersionPath = path.join(options.outputDir, 'v2.0.json'); await fs.writeFile(finalVersionPath, JSON.stringify(this.currentContent, null, 2), 'utf8'); this.versionPaths.push(finalVersionPath); logSh(`💾 Version finale v2.0 sauvegardée: ${finalVersionPath}`, 'DEBUG'); } logSh(`✅ Pipeline terminé: ${this.metadata.totalDuration}ms`, 'INFO'); return { success: true, finalContent: this.currentContent, executionLog: this.executionLog, checkpoints: this.checkpoints, versionHistory: this.versionHistory, // ✅ Inclure version history versionPaths: this.versionPaths, // ✅ NOUVEAU: Chemins des versions JSON metadata: { ...this.metadata, pipelineName: pipelineConfig.name, totalSteps: enabledSteps.length, successfulSteps: this.executionLog.filter(l => l.success).length } }; }, { pipelineName: pipelineConfig.name, rowNumber }); } /** * Charge les données depuis Google Sheets */ async loadData(rowNumber) { return tracer.run('PipelineExecutor.loadData', async () => { const csvData = await readInstructionsData(rowNumber); // Charger personnalité si besoin const personalities = await getPersonalities(); const personality = await selectPersonalityWithAI( csvData.mc0, csvData.t0, personalities ); csvData.personality = personality; this.metadata.personality = personality.nom; logSh(`📊 Données chargées: ${csvData.mc0}, personnalité: ${personality.nom}`, 'DEBUG'); return csvData; }, { rowNumber }); } /** * Exécute une étape individuelle */ async executeStep(step, csvData, options) { return tracer.run(`PipelineExecutor.executeStep.${step.module}`, async () => { switch (step.module) { case 'generation': return await this.runGeneration(step, csvData); case 'selective': return await this.runSelective(step, csvData); case 'smarttouch': // ✅ NOUVEAU: SelectiveSmartTouch return await this.runSmartTouch(step, csvData); case 'adversarial': return await this.runAdversarial(step, csvData); case 'human': return await this.runHumanSimulation(step, csvData); case 'pattern': return await this.runPatternBreaking(step, csvData); default: throw new Error(`Module inconnu: ${step.module}`); } }, { step: step.step, module: step.module, mode: step.mode }); } /** * Exécute la génération initiale */ async runGeneration(step, csvData) { return tracer.run('PipelineExecutor.runGeneration', async () => { if (this.currentContent) { logSh('⚠️ Contenu déjà généré, génération ignorée', 'WARN'); return { content: this.currentContent, modifications: 0 }; } // 🆕 Étape 0: Générer les variables Google Sheets manquantes (MC+1_5, T+1_6, etc.) logSh('🔄 Vérification variables Google Sheets...', 'DEBUG'); const updatedCsvData = await generateMissingSheetVariables(csvData.xmlTemplate, csvData); // Mettre à jour csvData pour les étapes suivantes Object.assign(csvData, updatedCsvData); // Étape 1: Extraire les éléments depuis le template XML (avec csvData complet) const elements = await extractElements(csvData.xmlTemplate, csvData); logSh(`✓ Extraction: ${elements.length} éléments extraits`, 'DEBUG'); // Étape 2: Générer les mots-clés manquants (titres, textes, FAQ) const finalElements = await generateMissingKeywords(elements, csvData); this.finalElements = finalElements; // ✅ Stocker pour sauvegarde // Étape 3: Construire la hiérarchie const elementsArray = Array.isArray(finalElements) ? finalElements : (finalElements && typeof finalElements === 'object') ? Object.values(finalElements) : []; const hierarchy = await buildSmartHierarchy(elementsArray); logSh(`✓ Hiérarchie: ${Object.keys(hierarchy).length} sections`, 'DEBUG'); // Étape 4: Génération simple avec LLM configurable const llmProvider = step.parameters?.llmProvider || 'claude-sonnet-4-5'; const result = await generateSimple(hierarchy, csvData, { llmProvider }); logSh(`✓ Génération: ${Object.keys(result.content || {}).length} éléments créés avec ${llmProvider}`, 'DEBUG'); return { content: result.content, modifications: Object.keys(result.content || {}).length }; }, { mode: step.mode }); } /** * Exécute l'enhancement sélectif */ async runSelective(step, csvData) { return tracer.run('PipelineExecutor.runSelective', async () => { if (!this.currentContent) { throw new Error('Aucun contenu à améliorer. Génération requise avant selective enhancement'); } // Configuration de la couche const llmProvider = step.parameters?.llmProvider || 'gpt-4o-mini'; const config = { csvData, personality: csvData.personality, intensity: step.intensity || 1.0, llmProvider: llmProvider, ...step.parameters }; let result; // Utiliser le stack si c'est un mode prédéfini const predefinedStacks = ['lightEnhancement', 'standardEnhancement', 'fullEnhancement', 'personalityFocus', 'fluidityFocus', 'adaptive']; if (predefinedStacks.includes(step.mode)) { result = await applySelectiveStack(this.currentContent, step.mode, config); } else { // Sinon utiliser la couche directe result = await applySelectiveLayer(this.currentContent, config); } logSh(`✓ Selective: modifications appliquées avec ${llmProvider}`, 'DEBUG'); return { content: result.content || result, modifications: result.modifications || 0 // ✅ CORRIGÉ: modifications au lieu de modificationsCount }; }, { mode: step.mode, intensity: step.intensity }); } /** * ✅ NOUVEAU: Exécute SelectiveSmartTouch (Analyse→Améliorations ciblées) */ async runSmartTouch(step, csvData) { return tracer.run('PipelineExecutor.runSmartTouch', async () => { if (!this.currentContent) { throw new Error('Aucun contenu à améliorer. Génération requise avant SmartTouch'); } // ✅ Extraire llmProvider depuis parameters (comme les autres modules) const llmProvider = step.parameters?.llmProvider || 'gpt-4o-mini'; // Default gpt-4o-mini pour analyse objective logSh(`🧠 SMART TOUCH: Mode ${step.mode}, LLM: ${llmProvider}`, 'INFO'); // ✅ NOUVEAU: Charger personnalité spécifique si demandée let effectiveCsvData = { ...csvData }; if (step.parameters?.personalityName) { const personalities = await getPersonalities(); const requestedPersonality = personalities.find(p => p.nom === step.parameters.personalityName); if (requestedPersonality) { effectiveCsvData.personality = requestedPersonality; logSh(`🎭 Personnalité override: ${requestedPersonality.nom} (au lieu de ${csvData.personality?.nom})`, 'INFO'); } else { logSh(`⚠️ Personnalité "${step.parameters.personalityName}" non trouvée, utilisation de ${csvData.personality?.nom}`, 'WARN'); } } // Instancier SmartTouchCore const smartTouch = new SmartTouchCore(); // Configuration const config = { mode: step.mode || 'full', // full, analysis_only, technical_only, style_only, readability_only intensity: step.intensity || 1.0, csvData: effectiveCsvData, // ✅ Utiliser csvData avec personnalité potentiellement overridée llmProvider: llmProvider, // ✅ Passer le LLM choisi dans pipeline skipAnalysis: step.parameters?.skipAnalysis || false, layersOrder: step.parameters?.layersOrder || ['technical', 'style', 'readability'], charsPerExpression: step.parameters?.charsPerExpression || 4000 // ✅ NOUVEAU: Budget dynamique }; // Exécuter SmartTouch const result = await smartTouch.apply(this.currentContent, config); logSh(`✓ SmartTouch: ${result.modifications || 0} modifications appliquées avec ${llmProvider}`, 'DEBUG'); return { content: result.content || result, modifications: result.modifications || 0, analysisResults: result.analysisResults // Inclure analyse pour debugging }; }, { mode: step.mode, intensity: step.intensity }); } /** * Exécute l'adversarial generation */ async runAdversarial(step, csvData) { return tracer.run('PipelineExecutor.runAdversarial', async () => { if (!this.currentContent) { throw new Error('Aucun contenu à traiter. Génération requise avant adversarial'); } if (step.mode === 'none') { logSh('Adversarial mode = none, ignoré', 'DEBUG'); return { content: this.currentContent, modifications: 0 }; } const llmProvider = step.parameters?.llmProvider || 'gemini-pro'; const config = { csvData, detectorTarget: step.parameters?.detector || 'general', method: step.parameters?.method || 'regeneration', intensity: step.intensity || 1.0, llmProvider: llmProvider }; let result; // Mapper les noms user-friendly vers les vrais noms de stacks const stackMapping = { 'light': 'lightDefense', 'standard': 'standardDefense', 'heavy': 'heavyDefense', 'adaptive': 'adaptive' }; // Utiliser le stack si c'est un mode prédéfini if (stackMapping[step.mode]) { const stackName = stackMapping[step.mode]; if (stackName === 'adaptive') { // Mode adaptatif utilise la couche directe result = await applyAdversarialLayer(this.currentContent, config); } else { result = await applyAdversarialStack(this.currentContent, stackName, config); } } else { // Sinon utiliser la couche directe result = await applyAdversarialLayer(this.currentContent, config); } logSh(`✓ Adversarial: modifications appliquées avec ${llmProvider}`, 'DEBUG'); return { content: result.content || result, modifications: result.modifications || 0 // ✅ CORRIGÉ: modifications au lieu de modificationsCount }; }, { mode: step.mode, detector: step.parameters?.detector }); } /** * Exécute la simulation humaine */ async runHumanSimulation(step, csvData) { return tracer.run('PipelineExecutor.runHumanSimulation', async () => { if (!this.currentContent) { throw new Error('Aucun contenu à traiter. Génération requise avant human simulation'); } if (step.mode === 'none') { logSh('Human simulation mode = none, ignoré', 'DEBUG'); return { content: this.currentContent, modifications: 0 }; } const llmProvider = step.parameters?.llmProvider || 'mistral-small'; const config = { csvData, personality: csvData.personality, intensity: step.intensity || 1.0, fatigueLevel: step.parameters?.fatigueLevel || 0.5, errorRate: step.parameters?.errorRate || 0.3, llmProvider: llmProvider }; let result; // Utiliser le stack si c'est un mode prédéfini const predefinedModes = ['lightSimulation', 'standardSimulation', 'heavySimulation', 'adaptiveSimulation', 'personalityFocus', 'temporalFocus']; if (predefinedModes.includes(step.mode)) { result = await applyPredefinedSimulation(this.currentContent, step.mode, config); } else { // Sinon utiliser la couche directe result = await applyHumanSimulationLayer(this.currentContent, config); } logSh(`✓ Human Simulation: modifications appliquées avec ${llmProvider}`, 'DEBUG'); return { content: result.content || result, modifications: result.modifications || 0 // ✅ CORRIGÉ: modifications au lieu de modificationsCount }; }, { mode: step.mode, intensity: step.intensity }); } /** * Exécute le pattern breaking */ async runPatternBreaking(step, csvData) { return tracer.run('PipelineExecutor.runPatternBreaking', async () => { if (!this.currentContent) { throw new Error('Aucun contenu à traiter. Génération requise avant pattern breaking'); } if (step.mode === 'none') { logSh('Pattern breaking mode = none, ignoré', 'DEBUG'); return { content: this.currentContent, modifications: 0 }; } const llmProvider = step.parameters?.llmProvider || 'deepseek-chat'; const config = { csvData, personality: csvData.personality, intensity: step.intensity || 1.0, focus: step.parameters?.focus || 'both', llmProvider: llmProvider }; let result; // Utiliser le stack si c'est un mode prédéfini const predefinedModes = ['lightPatternBreaking', 'standardPatternBreaking', 'heavyPatternBreaking', 'adaptivePatternBreaking', 'syntaxFocus', 'connectorsFocus']; if (predefinedModes.includes(step.mode)) { result = await applyPatternBreakingStack(step.mode, this.currentContent, config); } else { // Sinon utiliser la couche directe result = await applyPatternBreakingLayer(this.currentContent, config); } logSh(`✓ Pattern Breaking: modifications appliquées avec ${llmProvider}`, 'DEBUG'); return { content: result.content || result, modifications: result.modifications || 0 // ✅ CORRIGÉ: modifications au lieu de modificationsCount }; }, { mode: step.mode, intensity: step.intensity }); } /** * Obtient le contenu actuel */ getCurrentContent() { return this.currentContent; } /** * Obtient le log d'exécution */ getExecutionLog() { return this.executionLog; } /** * Obtient les checkpoints sauvegardés */ getCheckpoints() { return this.checkpoints; } /** * Obtient les métadonnées d'exécution */ getMetadata() { return this.metadata; } /** * Reset l'état de l'executor */ reset() { this.currentContent = null; this.executionLog = []; this.checkpoints = []; this.versionHistory = []; this.parentArticleId = null; this.csvData = null; this.finalElements = null; this.versionPaths = []; // ✅ NOUVEAU: Reset version paths this.metadata = { startTime: null, endTime: null, totalDuration: 0, personality: null }; } /** * ✅ NOUVEAU: Sauvegarde une version JSON locale pour Pipeline Validator */ async saveVersionJSON(step, stepIndex, outputDir) { try { // Déterminer le nom de la version let versionName; if (step.module === 'generation') { versionName = 'v1.0'; // Version initiale après génération } else { versionName = `v1.${stepIndex + 1}`; // v1.1, v1.2, v1.3... } const versionPath = path.join(outputDir, `${versionName}.json`); // Sauvegarder le contenu actuel en JSON await fs.writeFile(versionPath, JSON.stringify(this.currentContent, null, 2), 'utf8'); // Ajouter au tableau des versions this.versionPaths.push(versionPath); logSh(`💾 Version ${versionName} sauvegardée: ${versionPath}`, 'DEBUG'); } catch (error) { logSh(`❌ Erreur sauvegarde version JSON: ${error.message}`, 'ERROR'); // Ne pas propager l'erreur pour ne pas bloquer l'exécution } } /** * ✅ Sauvegarde une version intermédiaire dans Google Sheets */ async saveStepVersion(step, modifications, pipelineName) { try { if (!this.csvData || !this.finalElements) { logSh('⚠️ Données manquantes pour sauvegarde, ignorée', 'WARN'); return; } // Déterminer la version basée sur le module et le nombre d'étapes const versionNumber = `v1.${step.step}`; const stageName = `${step.module}_${step.mode}`; logSh(`💾 Sauvegarde ${versionNumber}: ${stageName}`, 'INFO'); // Assemblage du contenu const xmlString = this.csvData.xmlTemplate.startsWith('