seo-generator-server/lib/pipeline/PipelineDefinition.js
StillHammer 471058f731 Add flexible pipeline system with per-module LLM configuration
- New modular pipeline architecture allowing custom workflow combinations
- Per-step LLM provider configuration (Claude, OpenAI, Gemini, Deepseek, Moonshot, Mistral)
- Visual pipeline builder and runner interfaces with drag-and-drop
- 10 predefined pipeline templates (minimal-test to originality-bypass)
- Pipeline CRUD operations via ConfigManager and REST API
- Fix variable resolution in instructions (HTML tags were breaking {{variables}})
- Fix hardcoded LLM providers in AdversarialCore
- Add TESTS_LLM_PROVIDER.md documentation with validation results
- Update dashboard to disable legacy config editor

API Endpoints:
- POST /api/pipeline/save, execute, validate, estimate
- GET /api/pipeline/list, modules, templates

Backward compatible with legacy modular workflow system.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 14:01:52 +08:00

375 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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');
/**
* Providers LLM disponibles
*/
const AVAILABLE_LLM_PROVIDERS = [
{ id: 'claude', name: 'Claude (Anthropic)', default: true },
{ id: 'openai', name: 'OpenAI GPT-4' },
{ id: 'gemini', name: 'Google Gemini' },
{ id: 'deepseek', name: 'Deepseek' },
{ id: 'moonshot', name: 'Moonshot' },
{ id: 'mistral', name: 'Mistral AI' }
];
/**
* 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',
parameters: {
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'claude' }
}
},
selective: {
name: 'Selective Enhancement',
description: 'Amélioration sélective par couches',
modes: [
'lightEnhancement',
'standardEnhancement',
'fullEnhancement',
'personalityFocus',
'fluidityFocus',
'adaptive'
],
defaultIntensity: 1.0,
defaultLLM: 'openai',
parameters: {
layers: { type: 'array', description: 'Couches spécifiques à appliquer' },
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'openai' }
}
},
adversarial: {
name: 'Adversarial Generation',
description: 'Techniques anti-détection',
modes: ['none', 'light', 'standard', 'heavy', 'adaptive'],
defaultIntensity: 1.0,
defaultLLM: 'gemini',
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' }
}
},
human: {
name: 'Human Simulation',
description: 'Simulation comportement humain',
modes: [
'none',
'lightSimulation',
'standardSimulation',
'heavySimulation',
'adaptiveSimulation',
'personalityFocus',
'temporalFocus'
],
defaultIntensity: 1.0,
defaultLLM: 'mistral',
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' }
}
},
pattern: {
name: 'Pattern Breaking',
description: 'Cassage patterns LLM',
modes: [
'none',
'lightPatternBreaking',
'standardPatternBreaking',
'heavyPatternBreaking',
'adaptivePatternBreaking',
'syntaxFocus',
'connectorsFocus'
],
defaultIntensity: 1.0,
defaultLLM: 'deepseek',
parameters: {
focus: { type: 'string', enum: ['syntax', 'connectors', 'both'], default: 'both' },
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'deepseek' }
}
}
};
/**
* 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,
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
};