// ======================================== // FICHIER: BrainConfig.js - Version Node.js // Description: Configuration cerveau + sélection personnalité IA // ======================================== require('dotenv').config(); const axios = require('axios'); const fs = require('fs').promises; const path = require('path'); // Import de la fonction logSh (assumant qu'elle existe dans votre projet Node.js) const { logSh } = require('./ErrorReporting'); // Configuration const CONFIG = { openai: { apiKey: process.env.OPENAI_API_KEY || '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', endpoint: 'https://api.openai.com/v1/chat/completions' }, dataSource: { type: process.env.DATA_SOURCE_TYPE || 'json', // 'json', 'csv', 'database' instructionsPath: './data/instructions.json', personalitiesPath: './data/personalities.json' } }; /** * FONCTION PRINCIPALE - Équivalent getBrainConfig() * @param {number|object} data - Numéro de ligne ou données directes * @returns {object} Configuration avec données CSV + personnalité */ async function getBrainConfig(data) { try { logSh("🧠 Début getBrainConfig Node.js", "INFO"); // 1. RÉCUPÉRER LES DONNÉES CSV let csvData; if (typeof data === 'number') { // Numéro de ligne fourni - lire depuis fichier csvData = await readInstructionsData(data); } else if (typeof data === 'object' && data.rowNumber) { csvData = await readInstructionsData(data.rowNumber); } else { // Données déjà fournies csvData = data; } logSh(`✅ CSV récupéré: ${csvData.mc0}`, "INFO"); // 2. RÉCUPÉRER LES PERSONNALITÉS const personalities = await getPersonalities(); logSh(`✅ ${personalities.length} personnalités chargées`, "INFO"); // 3. SÉLECTIONNER LA MEILLEURE PERSONNALITÉ VIA IA const selectedPersonality = await selectPersonalityWithAI( csvData.mc0, csvData.t0, personalities ); logSh(`✅ Personnalité sélectionnée: ${selectedPersonality.nom}`, "INFO"); return { success: true, data: { ...csvData, personality: selectedPersonality, timestamp: new Date().toISOString() } }; } catch (error) { logSh(`❌ Erreur getBrainConfig: ${error.message}`, "ERROR"); return { success: false, error: error.message }; } } /** * LIRE DONNÉES INSTRUCTIONS depuis Google Sheets DIRECTEMENT * @param {number} rowNumber - Numéro de ligne (2 = première ligne de données) * @returns {object} Données CSV parsées */ async function readInstructionsData(rowNumber = 2) { try { logSh(`📊 Lecture Google Sheet ligne ${rowNumber}...`, 'INFO'); // NOUVEAU : Lecture directe depuis Google Sheets const { google } = require('googleapis'); // Configuration auth Google Sheets - FORCE utilisation fichier JSON pour éviter problème TLS const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json'); const auth = new google.auth.GoogleAuth({ keyFile: keyFilePath, scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'] }); logSh('🔑 Utilisation fichier JSON pour contourner problème TLS OAuth', 'INFO'); const sheets = google.sheets({ version: 'v4', auth }); const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c'; // Récupérer la ligne spécifique (A à I au minimum) const response = await sheets.spreadsheets.values.get({ spreadsheetId: SHEET_ID, range: `Instructions!A${rowNumber}:I${rowNumber}` // Ligne spécifique A-I }); if (!response.data.values || response.data.values.length === 0) { throw new Error(`Ligne ${rowNumber} non trouvée dans Google Sheet`); } const row = response.data.values[0]; logSh(`✅ Ligne ${rowNumber} récupérée: ${row.length} colonnes`, 'INFO'); const xmlTemplateValue = row[8] || ''; let xmlTemplate = xmlTemplateValue; let xmlFileName = null; // Si c'est un nom de fichier, garder le nom ET utiliser un template par défaut if (xmlTemplateValue && xmlTemplateValue.endsWith('.xml') && xmlTemplateValue.length < 100) { logSh(`🔧 XML filename detected (${xmlTemplateValue}), keeping filename for Digital Ocean`, 'INFO'); xmlFileName = xmlTemplateValue; // Garder le nom du fichier pour Digital Ocean xmlTemplate = createDefaultXMLTemplate(); // Template par défaut pour le processing } return { rowNumber: rowNumber, slug: row[0] || '', // Colonne A t0: row[1] || '', // Colonne B mc0: row[2] || '', // Colonne C tMinus1: row[3] || '', // Colonne D lMinus1: row[4] || '', // Colonne E mcPlus1: row[5] || '', // Colonne F tPlus1: row[6] || '', // Colonne G lPlus1: row[7] || '', // Colonne H xmlTemplate: xmlTemplate, // XML template pour processing xmlFileName: xmlFileName // Nom fichier pour Digital Ocean (si applicable) }; } catch (error) { logSh(`❌ Erreur lecture Google Sheet: ${error.message}`, "ERROR"); throw error; } } /** * RÉCUPÉRER PERSONNALITÉS depuis l'onglet "Personnalites" du Google Sheet * @returns {Array} Liste des personnalités disponibles */ async function getPersonalities() { try { logSh('📊 Lecture personnalités depuis Google Sheet (onglet Personnalites)...', 'INFO'); // Configuration auth Google Sheets - FORCE utilisation fichier JSON pour éviter problème TLS const { google } = require('googleapis'); const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json'); const auth = new google.auth.GoogleAuth({ keyFile: keyFilePath, scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'] }); logSh('🔑 Utilisation fichier JSON pour contourner problème TLS OAuth (personnalités)', 'INFO'); const sheets = google.sheets({ version: 'v4', auth }); const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c'; // Récupérer toutes les personnalités (après la ligne d'en-tête) const response = await sheets.spreadsheets.values.get({ spreadsheetId: SHEET_ID, range: 'Personnalites!A2:O' // Colonnes A à O pour inclure les nouvelles colonnes IA }); if (!response.data.values || response.data.values.length === 0) { throw new Error('Aucune personnalité trouvée dans l\'onglet Personnalites'); } const personalities = []; // Traiter chaque ligne de personnalité response.data.values.forEach((row, index) => { if (row[0] && row[0].toString().trim() !== '') { // Si nom existe (colonne A) const personality = { nom: row[0]?.toString().trim() || '', description: row[1]?.toString().trim() || 'Expert généraliste', style: row[2]?.toString().trim() || 'professionnel', // Configuration avancée depuis colonnes Google Sheet motsClesSecteurs: parseCSVField(row[3]), vocabulairePref: parseCSVField(row[4]), connecteursPref: parseCSVField(row[5]), erreursTypiques: parseCSVField(row[6]), longueurPhrases: row[7]?.toString().trim() || 'moyennes', niveauTechnique: row[8]?.toString().trim() || 'moyen', ctaStyle: parseCSVField(row[9]), defautsSimules: parseCSVField(row[10]), // NOUVEAU: Configuration IA par étape depuis Google Sheets (colonnes L-O) aiEtape1Base: row[11]?.toString().trim().toLowerCase() || '', aiEtape2Technique: row[12]?.toString().trim().toLowerCase() || '', aiEtape3Transitions: row[13]?.toString().trim().toLowerCase() || '', aiEtape4Style: row[14]?.toString().trim().toLowerCase() || '', // Backward compatibility motsCles: parseCSVField(row[3] || '') // Utilise motsClesSecteurs }; personalities.push(personality); logSh(`✓ Personnalité chargée: ${personality.nom} (${personality.style})`, 'DEBUG'); } }); logSh(`📊 ${personalities.length} personnalités chargées depuis Google Sheet`, "INFO"); return personalities; } catch (error) { logSh(`❌ ÉCHEC: Impossible de récupérer les personnalités Google Sheets - ${error.message}`, "ERROR"); throw new Error(`FATAL: Personnalités Google Sheets inaccessibles - arrêt du workflow: ${error.message}`); } } /** * PARSER CHAMP CSV - Helper function * @param {string} field - Champ à parser * @returns {Array} Liste des éléments parsés */ function parseCSVField(field) { if (!field || field.toString().trim() === '') return []; return field.toString() .split(',') .map(item => item.trim()) .filter(item => item.length > 0); } /** * Sélectionner un sous-ensemble aléatoire de personnalités * @param {Array} allPersonalities - Liste complète des personnalités * @param {number} percentage - Pourcentage à garder (0.6 = 60%) * @returns {Array} Sous-ensemble aléatoire */ function selectRandomPersonalities(allPersonalities, percentage = 0.6) { const count = Math.ceil(allPersonalities.length * percentage); // Mélanger avec Fisher-Yates shuffle (meilleur que sort()) const shuffled = [...allPersonalities]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled.slice(0, count); } /** * NOUVELLE FONCTION: Sélection de 4 personnalités complémentaires pour le pipeline multi-AI * @param {string} mc0 - Mot-clé principal * @param {string} t0 - Titre principal * @param {Array} personalities - Liste des personnalités * @returns {Array} 4 personnalités sélectionnées pour chaque étape */ async function selectMultiplePersonalitiesWithAI(mc0, t0, personalities) { try { logSh(`🎭 Sélection MULTI-personnalités IA pour: ${mc0}`, "INFO"); // Sélection aléatoire de 80% des personnalités (plus large pour 4 choix) const randomPersonalities = selectRandomPersonalities(personalities, 0.8); const totalCount = personalities.length; const selectedCount = randomPersonalities.length; logSh(`🎲 Pool aléatoire: ${selectedCount}/${totalCount} personnalités disponibles`, "DEBUG"); logSh(`📋 Personnalités dans le pool: ${randomPersonalities.map(p => p.nom).join(', ')}`, "DEBUG"); const prompt = `Choisis 4 personnalités COMPLÉMENTAIRES pour générer du contenu sur "${mc0}": OBJECTIF: Créer une équipe de 4 rédacteurs avec styles différents mais cohérents PERSONNALITÉS DISPONIBLES: ${randomPersonalities.map(p => `- ${p.nom}: ${p.description} (Style: ${p.style})`).join('\n')} RÔLES À ATTRIBUER: 1. GÉNÉRATEUR BASE: Personnalité technique/experte pour la génération initiale 2. ENHANCER TECHNIQUE: Personnalité commerciale/précise pour améliorer les termes techniques 3. FLUIDITÉ: Personnalité créative/littéraire pour améliorer les transitions 4. STYLE FINAL: Personnalité terrain/accessible pour le style final CRITÈRES: - 4 personnalités aux styles DIFFÉRENTS mais complémentaires - Adapté au secteur: ${mc0} - Variabilité maximale pour anti-détection - Éviter les doublons de style FORMAT DE RÉPONSE (EXACTEMENT 4 noms séparés par des virgules): Nom1, Nom2, Nom3, Nom4`; const requestData = { model: "gpt-4o-mini", messages: [{"role": "user", "content": prompt}], max_tokens: 100, temperature: 1.0 }; const response = await axios.post(CONFIG.openai.endpoint, requestData, { headers: { 'Authorization': `Bearer ${CONFIG.openai.apiKey}`, 'Content-Type': 'application/json' }, timeout: 300000 }); const selectedNames = response.data.choices[0].message.content.trim() .split(',') .map(name => name.trim()); logSh(`🔍 Noms retournés par IA: ${selectedNames.join(', ')}`, "DEBUG"); // Mapper aux vraies personnalités const selectedPersonalities = []; selectedNames.forEach(name => { const personality = randomPersonalities.find(p => p.nom === name); if (personality) { selectedPersonalities.push(personality); } }); // Compléter si pas assez de personnalités trouvées (sécurité) while (selectedPersonalities.length < 4 && randomPersonalities.length > selectedPersonalities.length) { const remaining = randomPersonalities.filter(p => !selectedPersonalities.some(selected => selected.nom === p.nom) ); if (remaining.length > 0) { const randomIndex = Math.floor(Math.random() * remaining.length); selectedPersonalities.push(remaining[randomIndex]); } else { break; } } // Garantir exactement 4 personnalités const final4Personalities = selectedPersonalities.slice(0, 4); logSh(`✅ Équipe de 4 personnalités sélectionnée:`, "INFO"); final4Personalities.forEach((p, index) => { const roles = ['BASE', 'TECHNIQUE', 'FLUIDITÉ', 'STYLE']; logSh(` ${index + 1}. ${roles[index]}: ${p.nom} (${p.style})`, "INFO"); }); return final4Personalities; } catch (error) { logSh(`❌ FATAL: Sélection multi-personnalités échouée: ${error.message}`, "ERROR"); throw new Error(`FATAL: Sélection multi-personnalités IA impossible - arrêt du workflow: ${error.message}`); } } /** * FONCTION LEGACY: Sélection personnalité unique (maintenue pour compatibilité) * @param {string} mc0 - Mot-clé principal * @param {string} t0 - Titre principal * @param {Array} personalities - Liste des personnalités * @returns {object} Personnalité sélectionnée */ async function selectPersonalityWithAI(mc0, t0, personalities) { try { logSh(`🤖 Sélection personnalité IA UNIQUE pour: ${mc0}`, "DEBUG"); // Appeler la fonction multi et prendre seulement la première const multiPersonalities = await selectMultiplePersonalitiesWithAI(mc0, t0, personalities); const selectedPersonality = multiPersonalities[0]; logSh(`✅ Personnalité IA sélectionnée (mode legacy): ${selectedPersonality.nom}`, "INFO"); return selectedPersonality; } catch (error) { logSh(`❌ FATAL: Sélection personnalité par IA échouée: ${error.message}`, "ERROR"); throw new Error(`FATAL: Sélection personnalité IA inaccessible - arrêt du workflow: ${error.message}`); } } /** * CRÉER TEMPLATE XML PAR DÉFAUT quand colonne I contient un nom de fichier * Utilise les données CSV disponibles pour créer un template robuste */ function createDefaultXMLTemplate() { return `

|Titre_Principal{{T0}}{Rédige un titre H1 accrocheur de maximum 10 mots pour {{MC0}}. Style {{personality.style}}}|

|Introduction{{MC0}}{Rédige une introduction engageante de 2-3 phrases sur {{MC0}}. Ton {{personality.style}}, utilise {{personality.vocabulairePref}}}|

|Titre_H2_1{{MC+1_1}}{Crée un titre H2 informatif sur {{MC+1_1}}. Style {{personality.style}}}|

|Paragraphe_1{{MC+1_1}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_1}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|

|Titre_H2_2{{MC+1_2}}{Titre H2 pour {{MC+1_2}}. Mets en valeur les points forts. Ton {{personality.style}}}|

|Paragraphe_2{{MC+1_2}}{Paragraphe de 4-5 phrases sur {{MC+1_2}}. Détaille pourquoi c'est important pour {{MC0}}. Ton {{personality.style}}}|

|Titre_H2_3{{MC+1_3}}{Titre H2 sur les bénéfices de {{MC+1_3}}. Accrocheur et informatif}|

|Paragraphe_3{{MC+1_3}}{Explique en 4-5 phrases les avantages de {{MC+1_3}} pour {{MC0}}. Ton {{personality.style}}}|

`; } /** * CRÉER FICHIERS DE DONNÉES D'EXEMPLE * Fonction utilitaire pour initialiser les fichiers JSON */ async function createSampleDataFiles() { try { // Créer répertoire data s'il n'existe pas await fs.mkdir('./data', { recursive: true }); // Exemple instructions.json const sampleInstructions = [ { slug: "plaque-test", t0: "Plaque test signalétique", mc0: "plaque signalétique", "t-1": "Signalétique", "l-1": "/signaletique/", "mc+1": "plaque dibond, plaque aluminium, plaque PVC", "t+1": "Plaque dibond, Plaque alu, Plaque PVC", "l+1": "/plaque-dibond/, /plaque-aluminium/, /plaque-pvc/", xmlFileName: "template-plaque.xml" } ]; // Exemple personalities.json const samplePersonalities = [ { nom: "Marc", description: "Expert technique en signalétique", style: "professionnel et précis", motsClesSecteurs: "technique,dibond,aluminium,impression", vocabulairePref: "précision,qualité,expertise,performance", connecteursPref: "par ailleurs,en effet,notamment,cependant", erreursTypiques: "accord_proximite,repetition_legere", longueurPhrases: "moyennes", niveauTechnique: "élevé", ctaStyle: "découvrir,choisir,commander", defautsSimules: "fatigue_cognitive,hesitation_technique" }, { nom: "Sophie", description: "Passionnée de décoration et design", style: "familier et chaleureux", motsClesSecteurs: "décoration,design,esthétique,tendances", vocabulairePref: "joli,magnifique,tendance,style", connecteursPref: "du coup,en fait,sinon,au fait", erreursTypiques: "familiarite_excessive,expression_populaire", longueurPhrases: "courtes", niveauTechnique: "moyen", ctaStyle: "craquer,adopter,foncer", defautsSimules: "enthousiasme_variable,anecdote_personnelle" } ]; // Écrire les fichiers await fs.writeFile('./data/instructions.json', JSON.stringify(sampleInstructions, null, 2)); await fs.writeFile('./data/personalities.json', JSON.stringify(samplePersonalities, null, 2)); logSh('✅ Fichiers de données d\'exemple créés dans ./data/', "INFO"); } catch (error) { logSh(`❌ Erreur création fichiers exemple: ${error.message}`, "ERROR"); } } // ============= EXPORTS NODE.JS ============= module.exports = { getBrainConfig, getPersonalities, selectPersonalityWithAI, selectMultiplePersonalitiesWithAI, // NOUVEAU: Export de la fonction multi-personnalités selectRandomPersonalities, parseCSVField, readInstructionsData, createSampleDataFiles, createDefaultXMLTemplate, CONFIG }; // ============= TEST RAPIDE SI LANCÉ DIRECTEMENT ============= if (require.main === module) { (async () => { try { logSh('🧪 Test BrainConfig Node.js...', "INFO"); // Créer fichiers exemple si nécessaire try { await fs.access('./data/instructions.json'); } catch { await createSampleDataFiles(); } // Test de la fonction principale const result = await getBrainConfig(2); if (result.success) { logSh(`✅ Test réussi: ${result.data.personality.nom} pour ${result.data.mc0}`, "INFO"); } else { logSh(`❌ Test échoué: ${result.error}`, "ERROR"); } } catch (error) { logSh(`❌ Erreur test: ${error.message}`, "ERROR"); } })(); }