// ======================================== // ÉTAPE 1: GÉNÉRATION INITIALE ADVERSARIALE // Responsabilité: Créer le contenu de base avec Claude + anti-détection // LLM: Claude Sonnet (température 0.7) + Prompts adversariaux // ======================================== const { callLLM } = require('../LLMManager'); const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { createAdversarialPrompt } = require('./AdversarialPromptEngine'); const { DetectorStrategyManager } = require('./DetectorStrategies'); /** * MAIN ENTRY POINT - GÉNÉRATION INITIALE ADVERSARIALE * Input: { content: {}, csvData: {}, context: {}, adversarialConfig: {} } * Output: { content: {}, stats: {}, debug: {} } */ async function generateInitialContentAdversarial(input) { return await tracer.run('AdversarialInitialGeneration.generateInitialContentAdversarial()', async () => { const { hierarchy, csvData, context = {}, adversarialConfig = {} } = input; // Configuration adversariale par défaut const config = { detectorTarget: adversarialConfig.detectorTarget || 'general', intensity: adversarialConfig.intensity || 1.0, enableAdaptiveStrategy: adversarialConfig.enableAdaptiveStrategy || true, contextualMode: adversarialConfig.contextualMode !== false, ...adversarialConfig }; // Initialiser manager détecteur const detectorManager = new DetectorStrategyManager(config.detectorTarget); await tracer.annotate({ step: '1/4', llmProvider: 'claude', elementsCount: Object.keys(hierarchy).length, mc0: csvData.mc0 }); const startTime = Date.now(); logSh(`🎯 ÉTAPE 1/4 ADVERSARIAL: Génération initiale (Claude + ${config.detectorTarget})`, 'INFO'); logSh(` 📊 ${Object.keys(hierarchy).length} éléments à générer`, 'INFO'); try { // Collecter tous les éléments dans l'ordre XML const allElements = collectElementsInXMLOrder(hierarchy); // Séparer FAQ pairs et autres éléments const { faqPairs, otherElements } = separateElementTypes(allElements); // Générer en chunks pour éviter timeouts const results = {}; // 1. Générer éléments normaux avec prompts adversariaux if (otherElements.length > 0) { const normalResults = await generateNormalElementsAdversarial(otherElements, csvData, config, detectorManager); Object.assign(results, normalResults); } // 2. Générer paires FAQ adversariales si présentes if (faqPairs.length > 0) { const faqResults = await generateFAQPairsAdversarial(faqPairs, csvData, config, detectorManager); Object.assign(results, faqResults); } const duration = Date.now() - startTime; const stats = { processed: Object.keys(results).length, generated: Object.keys(results).length, faqPairs: faqPairs.length, duration }; logSh(`✅ ÉTAPE 1/4 TERMINÉE: ${stats.generated} éléments générés (${duration}ms)`, 'INFO'); await tracer.event(`Génération initiale terminée`, stats); return { content: results, stats, debug: { llmProvider: 'claude', step: 1, elementsGenerated: Object.keys(results), adversarialConfig: config, detectorTarget: config.detectorTarget, intensity: config.intensity } }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ ÉTAPE 1/4 ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR'); throw new Error(`InitialGeneration failed: ${error.message}`); } }, input); } /** * Générer éléments normaux avec prompts adversariaux en chunks */ async function generateNormalElementsAdversarial(elements, csvData, adversarialConfig, detectorManager) { logSh(`🎯 Génération éléments normaux adversariaux: ${elements.length} éléments`, 'DEBUG'); const results = {}; const chunks = chunkArray(elements, 4); // Chunks de 4 pour éviter timeouts for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; logSh(` 📦 Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG'); try { const basePrompt = createBatchPrompt(chunk, csvData); // Générer prompt adversarial const adversarialPrompt = createAdversarialPrompt(basePrompt, { detectorTarget: adversarialConfig.detectorTarget, intensity: adversarialConfig.intensity, elementType: getElementTypeFromChunk(chunk), personality: csvData.personality, contextualMode: adversarialConfig.contextualMode, csvData: csvData, debugMode: false }); const response = await callLLM('claude', adversarialPrompt, { temperature: 0.7, maxTokens: 2000 * chunk.length }, csvData.personality); const chunkResults = parseBatchResponse(response, chunk); Object.assign(results, chunkResults); logSh(` ✅ Chunk ${chunkIndex + 1}: ${Object.keys(chunkResults).length} éléments générés`, 'DEBUG'); // Délai entre chunks if (chunkIndex < chunks.length - 1) { await sleep(1500); } } catch (error) { logSh(` ❌ Chunk ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR'); throw error; } } return results; } /** * Générer paires FAQ adversariales cohérentes */ async function generateFAQPairsAdversarial(faqPairs, csvData, adversarialConfig, detectorManager) { logSh(`🎯 Génération paires FAQ adversariales: ${faqPairs.length} paires`, 'DEBUG'); const basePrompt = createFAQPairsPrompt(faqPairs, csvData); // Générer prompt adversarial spécialisé FAQ const adversarialPrompt = createAdversarialPrompt(basePrompt, { detectorTarget: adversarialConfig.detectorTarget, intensity: adversarialConfig.intensity * 1.1, // Intensité légèrement plus élevée pour FAQ elementType: 'faq_mixed', personality: csvData.personality, contextualMode: adversarialConfig.contextualMode, csvData: csvData, debugMode: false }); const response = await callLLM('claude', adversarialPrompt, { temperature: 0.8, maxTokens: 3000 }, csvData.personality); return parseFAQResponse(response, faqPairs); } /** * Créer prompt batch pour éléments normaux */ function createBatchPrompt(elements, csvData) { const personality = csvData.personality; let prompt = `=== GÉNÉRATION CONTENU INITIAL === Entreprise: Autocollant.fr - signalétique personnalisée Sujet: ${csvData.mc0} Rédacteur: ${personality.nom} (${personality.style}) ÉLÉMENTS À GÉNÉRER: `; elements.forEach((elementInfo, index) => { const cleanTag = elementInfo.tag.replace(/\|/g, ''); prompt += `${index + 1}. [${cleanTag}] - ${getElementDescription(elementInfo)}\n`; }); prompt += ` STYLE ${personality.nom.toUpperCase()}: - Vocabulaire: ${personality.vocabulairePref} - Phrases: ${personality.longueurPhrases} - Niveau: ${personality.niveauTechnique} CONSIGNES: - Contenu SEO optimisé pour ${csvData.mc0} - Style ${personality.style} naturel - Pas de références techniques dans contenu - RÉPONSE DIRECTE par le contenu FORMAT: [${elements[0].tag.replace(/\|/g, '')}] Contenu généré... [${elements[1] ? elements[1].tag.replace(/\|/g, '') : 'element2'}] Contenu généré...`; return prompt; } /** * Parser réponse batch */ function parseBatchResponse(response, elements) { const results = {}; const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs; let match; const parsedItems = {}; while ((match = regex.exec(response)) !== null) { const tag = match[1].trim(); const content = cleanGeneratedContent(match[2].trim()); parsedItems[tag] = content; } // Mapper aux vrais tags elements.forEach(element => { const cleanTag = element.tag.replace(/\|/g, ''); if (parsedItems[cleanTag] && parsedItems[cleanTag].length > 10) { results[element.tag] = parsedItems[cleanTag]; } else { results[element.tag] = `Contenu professionnel pour ${element.element.name || cleanTag}`; logSh(`⚠️ Fallback pour [${cleanTag}]`, 'WARNING'); } }); return results; } /** * Créer prompt pour paires FAQ */ function createFAQPairsPrompt(faqPairs, csvData) { const personality = csvData.personality; let prompt = `=== GÉNÉRATION PAIRES FAQ === Sujet: ${csvData.mc0} Rédacteur: ${personality.nom} (${personality.style}) PAIRES À GÉNÉRER: `; faqPairs.forEach((pair, index) => { const qTag = pair.question.tag.replace(/\|/g, ''); const aTag = pair.answer.tag.replace(/\|/g, ''); prompt += `${index + 1}. [${qTag}] + [${aTag}]\n`; }); prompt += ` CONSIGNES: - Questions naturelles de clients - Réponses expertes ${personality.style} - Couvrir: prix, livraison, personnalisation FORMAT: [${faqPairs[0].question.tag.replace(/\|/g, '')}] Question client naturelle ? [${faqPairs[0].answer.tag.replace(/\|/g, '')}] Réponse utile et rassurante.`; return prompt; } /** * Parser réponse FAQ */ function parseFAQResponse(response, faqPairs) { const results = {}; const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\n\[|$)/gs; let match; const parsedItems = {}; while ((match = regex.exec(response)) !== null) { const tag = match[1].trim(); const content = cleanGeneratedContent(match[2].trim()); parsedItems[tag] = content; } // Mapper aux paires FAQ faqPairs.forEach(pair => { const qCleanTag = pair.question.tag.replace(/\|/g, ''); const aCleanTag = pair.answer.tag.replace(/\|/g, ''); if (parsedItems[qCleanTag]) results[pair.question.tag] = parsedItems[qCleanTag]; if (parsedItems[aCleanTag]) results[pair.answer.tag] = parsedItems[aCleanTag]; }); return results; } // ============= HELPER FUNCTIONS ============= function collectElementsInXMLOrder(hierarchy) { const allElements = []; Object.keys(hierarchy).forEach(path => { const section = hierarchy[path]; if (section.title) { allElements.push({ tag: section.title.originalElement.originalTag, element: section.title.originalElement, type: section.title.originalElement.type }); } if (section.text) { allElements.push({ tag: section.text.originalElement.originalTag, element: section.text.originalElement, type: section.text.originalElement.type }); } section.questions.forEach(q => { allElements.push({ tag: q.originalElement.originalTag, element: q.originalElement, type: q.originalElement.type }); }); }); return allElements; } function separateElementTypes(allElements) { const faqPairs = []; const otherElements = []; const faqQuestions = {}; const faqAnswers = {}; // Collecter FAQ questions et answers allElements.forEach(element => { if (element.type === 'faq_question') { const numberMatch = element.tag.match(/(\d+)/); const faqNumber = numberMatch ? numberMatch[1] : '1'; faqQuestions[faqNumber] = element; } else if (element.type === 'faq_reponse') { const numberMatch = element.tag.match(/(\d+)/); const faqNumber = numberMatch ? numberMatch[1] : '1'; faqAnswers[faqNumber] = element; } else { otherElements.push(element); } }); // Créer paires FAQ Object.keys(faqQuestions).forEach(number => { const question = faqQuestions[number]; const answer = faqAnswers[number]; if (question && answer) { faqPairs.push({ number, question, answer }); } else if (question) { otherElements.push(question); } else if (answer) { otherElements.push(answer); } }); return { faqPairs, otherElements }; } function getElementDescription(elementInfo) { switch (elementInfo.type) { case 'titre_h1': return 'Titre principal accrocheur'; case 'titre_h2': return 'Titre de section'; case 'titre_h3': return 'Sous-titre'; case 'intro': return 'Introduction engageante'; case 'texte': return 'Paragraphe informatif'; default: return 'Contenu pertinent'; } } function cleanGeneratedContent(content) { if (!content) return content; // Supprimer préfixes indésirables content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?Titre_[HU]\d+_\d+[.,\s]*/gi, ''); content = content.replace(/\*\*[^*]+\*\*/g, ''); content = content.replace(/\s{2,}/g, ' '); content = content.trim(); return content; } function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Helper: Déterminer type d'élément dominant dans un chunk */ function getElementTypeFromChunk(chunk) { if (!chunk || chunk.length === 0) return 'generic'; // Compter les types dans le chunk const typeCounts = {}; chunk.forEach(element => { const type = element.type || 'generic'; typeCounts[type] = (typeCounts[type] || 0) + 1; }); // Retourner type le plus fréquent return Object.keys(typeCounts).reduce((a, b) => typeCounts[a] > typeCounts[b] ? a : b ); } module.exports = { generateInitialContentAdversarial, // ← MAIN ENTRY POINT ADVERSARIAL generateNormalElementsAdversarial, generateFAQPairsAdversarial, createBatchPrompt, parseBatchResponse, collectElementsInXMLOrder, separateElementTypes, getElementTypeFromChunk };