/** * 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'); // Modules d'exécution const { generateSimple } = require('../selective-enhancement/SelectiveUtils'); const { applySelectiveLayer } = require('../selective-enhancement/SelectiveCore'); const { applyPredefinedStack: applySelectiveStack } = require('../selective-enhancement/SelectiveLayers'); 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.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 // 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); } 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; 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 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 '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 }); } /** * 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.metadata = { startTime: null, endTime: null, totalDuration: 0, personality: null }; } /** * ✅ 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('