/** * PipelineDefinition.js * * Schemas et validation pour les pipelines modulaires flexibles. * Permet de définir des workflows custom avec n'importe quelle combinaison de modules. */ const { logSh } = require('../ErrorReporting'); const { getLLMProvidersList } = require('../LLMManager'); /** * Providers LLM disponibles (source unique depuis LLMManager) */ const AVAILABLE_LLM_PROVIDERS = getLLMProvidersList(); /** * Modules disponibles dans le pipeline */ const AVAILABLE_MODULES = { generation: { name: 'Generation', description: 'Génération initiale du contenu', modes: ['simple'], defaultIntensity: 1.0, defaultLLM: 'claude-sonnet-4-5', parameters: { llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'claude-sonnet-4-5' } } }, selective: { name: 'Selective Enhancement', description: 'Amélioration sélective par couches', modes: [ 'lightEnhancement', 'standardEnhancement', 'fullEnhancement', 'personalityFocus', 'fluidityFocus', 'adaptive' ], defaultIntensity: 1.0, defaultLLM: 'gpt-4o-mini', parameters: { layers: { type: 'array', description: 'Couches spécifiques à appliquer' }, llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'gpt-4o-mini' } } }, smarttouch: { name: 'SmartTouch (Analyse→Ciblé)', description: 'Analyse intelligente puis améliorations précises ciblées (nouvelle génération)', modes: [ 'full', // Analyse + Technical + Style + Readability 'analysis_only', // Analyse uniquement sans amélioration 'technical_only', // Améliorations techniques ciblées uniquement 'style_only', // Améliorations style ciblées uniquement 'readability_only' // Améliorations lisibilité ciblées uniquement ], defaultIntensity: 1.0, defaultLLM: 'gpt-4o-mini', parameters: { llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'gpt-4o-mini' }, skipAnalysis: { type: 'boolean', default: false, description: 'Passer l\'analyse (mode legacy)' }, layersOrder: { type: 'array', default: ['technical', 'style', 'readability'], description: 'Ordre d\'application des couches' }, charsPerExpression: { type: 'number', min: 1000, max: 10000, default: 4000, description: 'Caractères par expression familière (budget dynamique)' }, personalityName: { type: 'string', required: false, description: 'Nom de la personnalité à utiliser (ex: "Sophie", "Marc"). Si non spécifié, utilise celle du csvData.' } } }, adversarial: { name: 'Adversarial Generation', description: 'Techniques anti-détection', modes: ['none', 'light', 'standard', 'heavy', 'adaptive'], defaultIntensity: 1.0, defaultLLM: 'gemini-pro', parameters: { detector: { type: 'string', enum: ['general', 'gptZero', 'originality'], default: 'general' }, method: { type: 'string', enum: ['enhancement', 'regeneration', 'hybrid'], default: 'regeneration' }, llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'gemini-pro' } } }, human: { name: 'Human Simulation', description: 'Simulation comportement humain', modes: [ 'none', 'lightSimulation', 'standardSimulation', 'heavySimulation', 'adaptiveSimulation', 'personalityFocus', 'temporalFocus' ], defaultIntensity: 1.0, defaultLLM: 'mistral-small', parameters: { fatigueLevel: { type: 'number', min: 0, max: 1, default: 0.5 }, errorRate: { type: 'number', min: 0, max: 1, default: 0.3 }, llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'mistral-small' } } }, pattern: { name: 'Pattern Breaking', description: 'Cassage patterns LLM', modes: [ 'none', 'lightPatternBreaking', 'standardPatternBreaking', 'heavyPatternBreaking', 'adaptivePatternBreaking', 'syntaxFocus', 'connectorsFocus' ], defaultIntensity: 1.0, defaultLLM: 'deepseek-chat', parameters: { focus: { type: 'string', enum: ['syntax', 'connectors', 'both'], default: 'both' }, llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'deepseek-chat' } } } }; /** * Schema d'une étape de pipeline */ const STEP_SCHEMA = { step: { type: 'number', required: true, description: 'Numéro séquentiel de l\'étape' }, module: { type: 'string', required: true, enum: Object.keys(AVAILABLE_MODULES), description: 'Module à exécuter' }, mode: { type: 'string', required: true, description: 'Mode du module' }, intensity: { type: 'number', required: false, min: 0.1, max: 2.0, default: 1.0, description: 'Intensité d\'application' }, parameters: { type: 'object', required: false, default: {}, description: 'Paramètres spécifiques au module' }, saveCheckpoint: { type: 'boolean', required: false, default: false, description: 'Sauvegarder checkpoint après cette étape' }, enabled: { type: 'boolean', required: false, default: true, description: 'Activer/désactiver l\'étape' } }; /** * Schema complet d'un pipeline */ const PIPELINE_SCHEMA = { name: { type: 'string', required: true, minLength: 3, maxLength: 100 }, description: { type: 'string', required: false, maxLength: 500 }, pipeline: { type: 'array', required: true, minLength: 1, maxLength: 20 }, metadata: { type: 'object', required: false, properties: { author: { type: 'string' }, created: { type: 'string' }, version: { type: 'string' }, tags: { type: 'array' } } } }; /** * Classe PipelineDefinition */ class PipelineDefinition { constructor(definition = null) { this.definition = definition; } /** * Valide un pipeline complet */ static validate(pipeline) { const errors = []; // Validation schema principal if (!pipeline.name || typeof pipeline.name !== 'string' || pipeline.name.length < 3) { errors.push('Le nom du pipeline doit contenir au moins 3 caractères'); } if (!Array.isArray(pipeline.pipeline) || pipeline.pipeline.length === 0) { errors.push('Le pipeline doit contenir au moins une étape'); } if (pipeline.pipeline && pipeline.pipeline.length > 20) { errors.push('Le pipeline ne peut pas contenir plus de 20 étapes'); } // Validation des étapes if (Array.isArray(pipeline.pipeline)) { pipeline.pipeline.forEach((step, index) => { const stepErrors = PipelineDefinition.validateStep(step, index); errors.push(...stepErrors); }); // Vérifier séquence des steps const steps = pipeline.pipeline.map(s => s.step).sort((a, b) => a - b); for (let i = 0; i < steps.length; i++) { if (steps[i] !== i + 1) { errors.push(`Numérotation des étapes incorrecte: attendu ${i + 1}, trouvé ${steps[i]}`); break; } } } if (errors.length > 0) { logSh(`❌ Pipeline validation failed: ${errors.join(', ')}`, 'ERROR'); return { valid: false, errors }; } logSh(`✅ Pipeline "${pipeline.name}" validé: ${pipeline.pipeline.length} étapes`, 'DEBUG'); return { valid: true, errors: [] }; } /** * Valide une étape individuelle */ static validateStep(step, index) { const errors = []; // Step number if (typeof step.step !== 'number' || step.step < 1) { errors.push(`Étape ${index}: 'step' doit être un nombre >= 1`); } // Module if (!step.module || !AVAILABLE_MODULES[step.module]) { errors.push(`Étape ${index}: module '${step.module}' inconnu. Disponibles: ${Object.keys(AVAILABLE_MODULES).join(', ')}`); return errors; // Stop si module invalide } const moduleConfig = AVAILABLE_MODULES[step.module]; // Mode if (!step.mode) { errors.push(`Étape ${index}: 'mode' requis pour module ${step.module}`); } else if (!moduleConfig.modes.includes(step.mode)) { errors.push(`Étape ${index}: mode '${step.mode}' invalide pour ${step.module}. Disponibles: ${moduleConfig.modes.join(', ')}`); } // Intensity if (step.intensity !== undefined) { if (typeof step.intensity !== 'number' || step.intensity < 0.1 || step.intensity > 2.0) { errors.push(`Étape ${index}: intensity doit être entre 0.1 et 2.0`); } } // Parameters (validation basique) if (step.parameters && typeof step.parameters !== 'object') { errors.push(`Étape ${index}: parameters doit être un objet`); } return errors; } /** * Crée une étape de pipeline valide */ static createStep(stepNumber, module, mode, options = {}) { const moduleConfig = AVAILABLE_MODULES[module]; if (!moduleConfig) { throw new Error(`Module inconnu: ${module}`); } if (!moduleConfig.modes.includes(mode)) { throw new Error(`Mode ${mode} invalide pour module ${module}`); } return { step: stepNumber, module, mode, intensity: options.intensity ?? moduleConfig.defaultIntensity, parameters: options.parameters ?? {}, saveCheckpoint: options.saveCheckpoint ?? false, enabled: options.enabled ?? true }; } /** * Crée un pipeline vide */ static createEmpty(name, description = '') { return { name, description, pipeline: [], metadata: { author: 'system', created: new Date().toISOString(), version: '1.0', tags: [] } }; } /** * Clone un pipeline */ static clone(pipeline, newName = null) { const cloned = JSON.parse(JSON.stringify(pipeline)); if (newName) { cloned.name = newName; } cloned.metadata = { ...cloned.metadata, created: new Date().toISOString(), clonedFrom: pipeline.name }; return cloned; } /** * Estime la durée d'un pipeline */ static estimateDuration(pipeline) { // Durées moyennes par module (en secondes) const DURATIONS = { generation: 15, selective: 20, smarttouch: 25, // ✅ AJOUTÉ: smarttouch (analyse + améliorations ciblées) adversarial: 25, human: 15, pattern: 18 }; let totalSeconds = 0; pipeline.pipeline.forEach(step => { if (!step.enabled) return; const baseDuration = DURATIONS[step.module] || 20; const intensityFactor = step.intensity || 1.0; totalSeconds += baseDuration * intensityFactor; }); return { seconds: Math.round(totalSeconds), formatted: PipelineDefinition.formatDuration(totalSeconds) }; } /** * Formate une durée en secondes */ static formatDuration(seconds) { if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const secs = seconds % 60; return `${minutes}m ${secs}s`; } /** * Obtient les infos d'un module */ static getModuleInfo(moduleName) { return AVAILABLE_MODULES[moduleName] || null; } /** * Liste tous les modules disponibles */ static listModules() { return Object.entries(AVAILABLE_MODULES).map(([key, config]) => ({ id: key, ...config })); } /** * Génère un résumé lisible du pipeline */ static getSummary(pipeline) { const enabledSteps = pipeline.pipeline.filter(s => s.enabled !== false); const moduleCount = {}; enabledSteps.forEach(step => { moduleCount[step.module] = (moduleCount[step.module] || 0) + 1; }); const summary = Object.entries(moduleCount) .map(([module, count]) => `${module}×${count}`) .join(' → '); return { totalSteps: enabledSteps.length, summary, duration: PipelineDefinition.estimateDuration(pipeline) }; } } module.exports = { PipelineDefinition, AVAILABLE_MODULES, AVAILABLE_LLM_PROVIDERS, PIPELINE_SCHEMA, STEP_SCHEMA };