// ======================================== // SMART ANALYSIS LAYER - Analyse intelligente avant amélioration // Responsabilité: Analyser contenu et identifier améliorations précises nécessaires // LLM: GPT-4o-mini (objectivité, température basse) // Architecture: Phase 1 de SelectiveSmartTouch (Analyse → Amélioration ciblée) // ======================================== const { callLLM } = require('../LLMManager'); const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); /** * SMART ANALYSIS LAYER * Analyse objective du contenu pour identifier améliorations précises */ class SmartAnalysisLayer { constructor() { this.name = 'SmartAnalysis'; this.defaultLLM = 'gpt-4o-mini'; } /** * ANALYSE COMPLÈTE D'UN ÉLÉMENT * @param {string} content - Contenu à analyser * @param {object} context - Contexte (mc0, personality, llmProvider, etc.) * @returns {object} - Analyse JSON structurée */ async analyzeElement(content, context = {}) { return await tracer.run('SmartAnalysis.analyzeElement()', async () => { const { mc0, personality, llmProvider } = context; // ✅ Utiliser LLM fourni dans context, sinon fallback sur defaultLLM const llmToUse = llmProvider || this.defaultLLM; await tracer.annotate({ smartAnalysis: true, contentLength: content.length, hasMc0: !!mc0, hasPersonality: !!personality, llmProvider: llmToUse }); const startTime = Date.now(); logSh(`🔍 SMART ANALYSIS: Analyse d'un élément (${content.length} chars) avec ${llmToUse}`, 'DEBUG'); try { const prompt = this.createAnalysisPrompt(content, context); const response = await callLLM(llmToUse, prompt, { temperature: 0.2, // Basse température = objectivité maxTokens: 1500 }); // Parser JSON de l'analyse const analysis = this.parseAnalysisResponse(response); const duration = Date.now() - startTime; logSh(`✅ Analyse terminée: ${analysis.improvements.length} améliorations identifiées (${duration}ms)`, 'DEBUG'); await tracer.event('Smart Analysis terminée', { duration, improvementsCount: analysis.improvements.length, needsImprovement: analysis.overallScore < 0.7 }); return analysis; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ SMART ANALYSIS ÉCHOUÉE (${duration}ms): ${error.message}`, 'ERROR'); // Fallback: analyse basique algorithmique return this.fallbackAnalysis(content, context); } }, { contentLength: content.length, context }); } /** * ANALYSE BATCH DE PLUSIEURS ÉLÉMENTS */ async analyzeBatch(contentMap, context = {}) { return await tracer.run('SmartAnalysis.analyzeBatch()', async () => { const startTime = Date.now(); const results = {}; logSh(`🔍 SMART ANALYSIS BATCH: ${Object.keys(contentMap).length} éléments`, 'INFO'); for (const [tag, content] of Object.entries(contentMap)) { try { results[tag] = await this.analyzeElement(content, context); logSh(` ✅ [${tag}]: ${results[tag].improvements.length} améliorations`, 'DEBUG'); } catch (error) { logSh(` ❌ [${tag}]: Analyse échouée - ${error.message}`, 'ERROR'); results[tag] = this.fallbackAnalysis(content, context); } // Petit délai pour éviter rate limiting await new Promise(resolve => setTimeout(resolve, 500)); } const duration = Date.now() - startTime; logSh(`✅ SMART ANALYSIS BATCH terminé: ${Object.keys(results).length} éléments analysés (${duration}ms)`, 'INFO'); return results; }, { elementsCount: Object.keys(contentMap).length }); } /** * DÉTECTION CONTEXTUELLE (B2C vs B2B, niveau technique) * Permet d'adapter les améliorations au public cible */ detectContentContext(content, personality = null) { logSh('🔍 Détection contexte contenu...', 'DEBUG'); const context = { audience: 'unknown', // B2C, B2B, mixed techLevel: 'medium', // low, medium, high, too_high contentType: 'unknown', // ecommerce, service, informative, technical sector: 'general' }; // === DÉTECTION AUDIENCE === const b2cSignals = [ 'acheter', 'votre maison', 'chez vous', 'personnalisé', 'facile', 'simple', 'idéal pour vous', 'particulier', 'famille', 'clients' ]; const b2bSignals = [ 'entreprise', 'professionnel', 'solution industrielle', 'cahier des charges', 'conformité', 'optimisation des processus' ]; const b2cCount = b2cSignals.filter(s => content.toLowerCase().includes(s)).length; const b2bCount = b2bSignals.filter(s => content.toLowerCase().includes(s)).length; if (b2cCount > b2bCount && b2cCount > 2) context.audience = 'B2C'; else if (b2bCount > b2cCount && b2bCount > 2) context.audience = 'B2B'; else if (b2cCount > 0 && b2bCount > 0) context.audience = 'mixed'; // === DÉTECTION NIVEAU TECHNIQUE === const jargonWords = [ 'norme', 'coefficient', 'résistance', 'ISO', 'ASTM', 'certifié', 'EN ', 'DIN', 'conforme', 'spécification', 'standard', 'référence' ]; const technicalSpecs = (content.match(/\d+\s*(mm|cm|kg|°C|%|watt|lumen|J\/cm²|K⁻¹)/g) || []).length; const jargonCount = jargonWords.filter(w => content.includes(w)).length; if (jargonCount > 5 || technicalSpecs > 8) { context.techLevel = 'too_high'; } else if (jargonCount > 2 || technicalSpecs > 4) { context.techLevel = 'high'; } else if (jargonCount === 0 && technicalSpecs < 2) { context.techLevel = 'low'; } // === DÉTECTION TYPE CONTENU === const ecommerceKeywords = ['prix', 'acheter', 'commander', 'livraison', 'stock', 'produit']; const serviceKeywords = ['prestation', 'accompagnement', 'conseil', 'expertise', 'service']; if (ecommerceKeywords.filter(k => content.toLowerCase().includes(k)).length > 2) { context.contentType = 'ecommerce'; } else if (serviceKeywords.filter(k => content.toLowerCase().includes(k)).length > 2) { context.contentType = 'service'; } else if (jargonCount > 3) { context.contentType = 'technical'; } else { context.contentType = 'informative'; } // === INTÉGRATION DONNÉES PERSONALITY === if (personality) { if (personality.targetAudience?.toLowerCase().includes('grand public')) { context.audience = 'B2C'; } if (personality.secteur) { context.sector = personality.secteur; } if (personality.tone?.includes('accessible') || personality.tone?.includes('simple')) { context.preferSimple = true; } } logSh(`✅ Contexte détecté: audience=${context.audience}, techLevel=${context.techLevel}, type=${context.contentType}`, 'DEBUG'); return context; } /** * ANALYSE PAR SEGMENTS (découpe + score individuel) * Permet de sélectionner seulement les segments les plus faibles */ analyzeBySegments(content, context = {}) { logSh('🔍 Analyse par segments...', 'DEBUG'); // Découper en phrases (simpliste mais efficace) const sentences = content.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 10); const segments = sentences.map((sentence, index) => { // Score algorithmique rapide de chaque phrase const wordCount = sentence.split(/\s+/).length; const hasNumbers = /\d+/.test(sentence); const genericWords = ['nos solutions', 'notre expertise', 'qualité', 'service'].filter(w => sentence.toLowerCase().includes(w)).length; const jargonWords = ['norme', 'coefficient', 'ISO', 'certifié'].filter(w => sentence.includes(w)).length; let score = 0.5; // Score de base // Pénalités if (wordCount > 30) score -= 0.2; // Trop longue if (genericWords > 0) score -= 0.15 * genericWords; // Vocabulaire générique if (jargonWords > 1) score -= 0.1 * jargonWords; // Trop de jargon // Bonus if (hasNumbers && wordCount < 20) score += 0.1; // Concise avec données score = Math.max(0.0, Math.min(1.0, score)); // Clamp entre 0 et 1 return { index, content: sentence, score, wordCount, issues: [ wordCount > 30 ? 'trop_longue' : null, genericWords > 0 ? 'vocabulaire_générique' : null, jargonWords > 1 ? 'trop_technique' : null ].filter(Boolean) }; }); logSh(`✅ ${segments.length} segments analysés`, 'DEBUG'); return segments; } /** * SÉLECTION DES X% SEGMENTS LES PLUS FAIBLES * Retourne indices des segments à améliorer */ selectWeakestSegments(segments, percentage = 0.1) { // Trier par score croissant (plus faibles d'abord) const sortedSegments = [...segments].sort((a, b) => a.score - b.score); // Calculer combien de segments à prendre const countToSelect = Math.max(1, Math.ceil(segments.length * percentage)); // Prendre les N segments les plus faibles const selectedSegments = sortedSegments.slice(0, countToSelect); logSh(`📊 Sélection: ${selectedSegments.length}/${segments.length} segments les plus faibles (${(percentage * 100).toFixed(0)}%)`, 'INFO'); // Retourner dans l'ordre original (par index) return selectedSegments.sort((a, b) => a.index - b.index); } /** * CRÉER PROMPT D'ANALYSE (générique, multi-secteur) */ createAnalysisPrompt(content, context) { const { mc0, personality } = context; return `MISSION: Analyse OBJECTIVE de ce contenu et identifie les améliorations précises nécessaires. CONTENU À ANALYSER: "${content}" ${mc0 ? `CONTEXTE SUJET: ${mc0}` : 'CONTEXTE: Générique'} ${personality ? `PERSONNALITÉ CIBLE: ${personality.nom} (${personality.style})` : ''} ANALYSE CES DIMENSIONS: 1. DIMENSION TECHNIQUE: - Manque-t-il des informations factuelles concrètes ? - Le contenu est-il trop générique ou vague ? - Y a-t-il besoin de données chiffrées, dimensions, spécifications ? 2. DIMENSION STYLE: - Le ton est-il cohérent ? ${personality ? `- Le style correspond-il à "${personality.style}" ?` : '- Le style est-il professionnel ?'} - Y a-t-il des expressions trop génériques ("nos solutions", "notre expertise") ? 3. DIMENSION LISIBILITÉ: - Les phrases sont-elles trop longues ou complexes ? - Les connecteurs sont-ils répétitifs ? - La structure est-elle fluide ? 4. DIMENSION VOCABULAIRE: - Mots génériques à remplacer par termes spécifiques ? - Vocabulaire adapté au sujet ? IMPORTANT: Sois OBJECTIF et SÉLECTIF. Ne liste QUE les améliorations réellement nécessaires. Si le contenu est déjà bon sur une dimension, indique "needed: false" pour cette dimension. RETOURNE UN JSON (et UNIQUEMENT du JSON valide): { "technical": { "needed": true/false, "score": 0.0-1.0, "missing": ["élément précis manquant 1", "élément précis manquant 2"], "issues": ["problème identifié"] }, "style": { "needed": true/false, "score": 0.0-1.0, "toneIssues": ["problème de ton"], "genericPhrases": ["expression générique à personnaliser"] }, "readability": { "needed": true/false, "score": 0.0-1.0, "complexSentences": [numéro_ligne], "repetitiveConnectors": ["connecteur répété"] }, "vocabulary": { "needed": true/false, "score": 0.0-1.0, "genericWords": ["mot générique"], "suggestions": ["terme spécifique suggéré"] }, "improvements": [ "Amélioration précise 1", "Amélioration précise 2" ], "overallScore": 0.0-1.0 }`; } /** * PARSER RÉPONSE JSON DE L'ANALYSE */ parseAnalysisResponse(response) { try { // Nettoyer la réponse (supprimer markdown, etc.) let cleanResponse = response.trim(); // Supprimer balises markdown JSON cleanResponse = cleanResponse.replace(/```json\s*/gi, ''); cleanResponse = cleanResponse.replace(/```\s*/g, ''); // Parser JSON const analysis = JSON.parse(cleanResponse); // Valider structure if (!analysis.technical || !analysis.style || !analysis.readability || !analysis.vocabulary) { throw new Error('Structure JSON incomplète'); } // Valider que improvements est un array if (!Array.isArray(analysis.improvements)) { analysis.improvements = []; } return analysis; } catch (error) { logSh(`⚠️ Parsing JSON échoué: ${error.message}, tentative de récupération...`, 'WARNING'); // Tentative d'extraction partielle try { const jsonMatch = response.match(/\{[\s\S]*\}/); if (jsonMatch) { return JSON.parse(jsonMatch[0]); } } catch (e) { // Ignore } throw new Error(`Impossible de parser l'analyse JSON: ${error.message}`); } } /** * ANALYSE FALLBACK ALGORITHMIQUE (si LLM échoue) */ fallbackAnalysis(content, context) { logSh(`🔄 Fallback: Analyse algorithmique de base`, 'DEBUG'); const wordCount = content.split(/\s+/).length; const sentenceCount = content.split(/[.!?]+/).filter(s => s.trim().length > 5).length; const avgSentenceLength = wordCount / Math.max(sentenceCount, 1); // Analyse basique const genericWords = ['nos solutions', 'notre expertise', 'qualité', 'service', 'professionnel']; const foundGeneric = genericWords.filter(word => content.toLowerCase().includes(word)); const improvements = []; let technicalScore = 0.5; let styleScore = 0.5; let readabilityScore = 0.5; // Détection phrases trop longues if (avgSentenceLength > 25) { improvements.push('Raccourcir les phrases trop longues (> 25 mots)'); readabilityScore = 0.4; } // Détection vocabulaire générique if (foundGeneric.length > 2) { improvements.push(`Remplacer expressions génériques: ${foundGeneric.join(', ')}`); styleScore = 0.4; } // Détection manque de données concrètes const hasNumbers = /\d+/.test(content); if (!hasNumbers && wordCount > 50) { improvements.push('Ajouter données concrètes (chiffres, dimensions, pourcentages)'); technicalScore = 0.4; } return { technical: { needed: technicalScore < 0.6, score: technicalScore, missing: hasNumbers ? [] : ['données chiffrées'], issues: [] }, style: { needed: styleScore < 0.6, score: styleScore, toneIssues: [], genericPhrases: foundGeneric }, readability: { needed: readabilityScore < 0.6, score: readabilityScore, complexSentences: avgSentenceLength > 25 ? [1] : [], repetitiveConnectors: [] }, vocabulary: { needed: foundGeneric.length > 0, score: foundGeneric.length > 2 ? 0.3 : 0.6, genericWords: foundGeneric, suggestions: [] }, improvements, overallScore: (technicalScore + styleScore + readabilityScore) / 3, fallbackUsed: true }; } /** * RÉSUMER ANALYSES BATCH */ summarizeBatchAnalysis(analysisResults) { const summary = { totalElements: Object.keys(analysisResults).length, needsImprovement: 0, averageScore: 0, commonIssues: { technical: 0, style: 0, readability: 0, vocabulary: 0 }, totalImprovements: 0 }; let totalScore = 0; Object.values(analysisResults).forEach(analysis => { totalScore += analysis.overallScore; if (analysis.overallScore < 0.7) { summary.needsImprovement++; } if (analysis.technical.needed) summary.commonIssues.technical++; if (analysis.style.needed) summary.commonIssues.style++; if (analysis.readability.needed) summary.commonIssues.readability++; if (analysis.vocabulary.needed) summary.commonIssues.vocabulary++; summary.totalImprovements += analysis.improvements.length; }); summary.averageScore = totalScore / summary.totalElements; return summary; } } module.exports = { SmartAnalysisLayer };