443 lines
14 KiB
JavaScript
443 lines
14 KiB
JavaScript
// ========================================
|
|
// TECHNICAL LAYER - COUCHE TECHNIQUE MODULAIRE
|
|
// Responsabilité: Amélioration technique modulaire réutilisable
|
|
// LLM: GPT-4o-mini (précision technique optimale)
|
|
// ========================================
|
|
|
|
const { callLLM } = require('../LLMManager');
|
|
const { logSh } = require('../ErrorReporting');
|
|
const { tracer } = require('../trace');
|
|
const { chunkArray, sleep } = require('./SelectiveUtils');
|
|
|
|
/**
|
|
* COUCHE TECHNIQUE MODULAIRE
|
|
*/
|
|
class TechnicalLayer {
|
|
constructor() {
|
|
this.name = 'TechnicalEnhancement';
|
|
this.defaultLLM = 'openai';
|
|
this.priority = 1; // Haute priorité - appliqué en premier généralement
|
|
}
|
|
|
|
/**
|
|
* MAIN METHOD - Appliquer amélioration technique
|
|
*/
|
|
async apply(content, config = {}) {
|
|
return await tracer.run('TechnicalLayer.apply()', async () => {
|
|
const {
|
|
llmProvider = this.defaultLLM,
|
|
intensity = 1.0, // 0.0-2.0 intensité d'amélioration
|
|
analysisMode = true, // Analyser avant d'appliquer
|
|
csvData = null,
|
|
preserveStructure = true,
|
|
targetTerms = null // Termes techniques ciblés
|
|
} = config;
|
|
|
|
await tracer.annotate({
|
|
technicalLayer: true,
|
|
llmProvider,
|
|
intensity,
|
|
elementsCount: Object.keys(content).length,
|
|
mc0: csvData?.mc0
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
logSh(`⚙️ TECHNICAL LAYER: Amélioration technique (${llmProvider})`, 'INFO');
|
|
logSh(` 📊 ${Object.keys(content).length} éléments | Intensité: ${intensity}`, 'INFO');
|
|
|
|
try {
|
|
let enhancedContent = {};
|
|
let elementsProcessed = 0;
|
|
let elementsEnhanced = 0;
|
|
|
|
if (analysisMode) {
|
|
// 1. Analyser éléments nécessitant amélioration technique
|
|
const analysis = await this.analyzeTechnicalNeeds(content, csvData, targetTerms);
|
|
|
|
logSh(` 📋 Analyse: ${analysis.candidates.length}/${Object.keys(content).length} éléments candidats`, 'DEBUG');
|
|
|
|
if (analysis.candidates.length === 0) {
|
|
logSh(`✅ TECHNICAL LAYER: Aucune amélioration nécessaire`, 'INFO');
|
|
return {
|
|
content,
|
|
stats: {
|
|
processed: Object.keys(content).length,
|
|
enhanced: 0,
|
|
analysisSkipped: true,
|
|
duration: Date.now() - startTime
|
|
}
|
|
};
|
|
}
|
|
|
|
// 2. Améliorer les éléments sélectionnés
|
|
const improvedResults = await this.enhanceTechnicalElements(
|
|
analysis.candidates,
|
|
csvData,
|
|
{ llmProvider, intensity, preserveStructure }
|
|
);
|
|
|
|
// 3. Merger avec contenu original
|
|
enhancedContent = { ...content };
|
|
Object.keys(improvedResults).forEach(tag => {
|
|
if (improvedResults[tag] !== content[tag]) {
|
|
enhancedContent[tag] = improvedResults[tag];
|
|
elementsEnhanced++;
|
|
}
|
|
});
|
|
|
|
elementsProcessed = analysis.candidates.length;
|
|
|
|
} else {
|
|
// Mode direct : améliorer tous les éléments
|
|
enhancedContent = await this.enhanceAllElementsDirect(
|
|
content,
|
|
csvData,
|
|
{ llmProvider, intensity, preserveStructure }
|
|
);
|
|
|
|
elementsProcessed = Object.keys(content).length;
|
|
elementsEnhanced = this.countDifferences(content, enhancedContent);
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
const stats = {
|
|
processed: elementsProcessed,
|
|
enhanced: elementsEnhanced,
|
|
total: Object.keys(content).length,
|
|
enhancementRate: (elementsEnhanced / Math.max(elementsProcessed, 1)) * 100,
|
|
duration,
|
|
llmProvider,
|
|
intensity
|
|
};
|
|
|
|
logSh(`✅ TECHNICAL LAYER TERMINÉE: ${elementsEnhanced}/${elementsProcessed} améliorés (${duration}ms)`, 'INFO');
|
|
|
|
await tracer.event('Technical layer appliquée', stats);
|
|
|
|
return { content: enhancedContent, stats };
|
|
|
|
} catch (error) {
|
|
const duration = Date.now() - startTime;
|
|
logSh(`❌ TECHNICAL LAYER ÉCHOUÉE après ${duration}ms: ${error.message}`, 'ERROR');
|
|
throw error;
|
|
}
|
|
}, { content: Object.keys(content), config });
|
|
}
|
|
|
|
/**
|
|
* ANALYSER BESOINS TECHNIQUES
|
|
*/
|
|
async analyzeTechnicalNeeds(content, csvData, targetTerms = null) {
|
|
logSh(`🔍 Analyse besoins techniques`, 'DEBUG');
|
|
|
|
const analysis = {
|
|
candidates: [],
|
|
technicalTermsFound: [],
|
|
missingTerms: [],
|
|
globalScore: 0
|
|
};
|
|
|
|
// Définir termes techniques selon contexte
|
|
const contextualTerms = this.getContextualTechnicalTerms(csvData?.mc0, targetTerms);
|
|
|
|
// Analyser chaque élément
|
|
Object.entries(content).forEach(([tag, text]) => {
|
|
const elementAnalysis = this.analyzeTechnicalElement(text, contextualTerms, csvData);
|
|
|
|
if (elementAnalysis.needsImprovement) {
|
|
analysis.candidates.push({
|
|
tag,
|
|
content: text,
|
|
technicalTerms: elementAnalysis.foundTerms,
|
|
missingTerms: elementAnalysis.missingTerms,
|
|
score: elementAnalysis.score,
|
|
improvements: elementAnalysis.improvements
|
|
});
|
|
|
|
analysis.globalScore += elementAnalysis.score;
|
|
}
|
|
|
|
analysis.technicalTermsFound.push(...elementAnalysis.foundTerms);
|
|
});
|
|
|
|
analysis.globalScore = analysis.globalScore / Math.max(Object.keys(content).length, 1);
|
|
analysis.technicalTermsFound = [...new Set(analysis.technicalTermsFound)];
|
|
|
|
logSh(` 📊 Score global technique: ${analysis.globalScore.toFixed(2)}`, 'DEBUG');
|
|
|
|
return analysis;
|
|
}
|
|
|
|
/**
|
|
* AMÉLIORER ÉLÉMENTS TECHNIQUES SÉLECTIONNÉS
|
|
*/
|
|
async enhanceTechnicalElements(candidates, csvData, config) {
|
|
logSh(`🛠️ Amélioration ${candidates.length} éléments techniques`, 'DEBUG');
|
|
logSh(`🔍 Candidates reçus: ${JSON.stringify(candidates.map(c => c.tag))}`, 'DEBUG');
|
|
|
|
const results = {};
|
|
const chunks = chunkArray(candidates, 4); // Chunks de 4 pour GPT-4
|
|
|
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
const chunk = chunks[chunkIndex];
|
|
|
|
try {
|
|
logSh(` 📦 Chunk technique ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
|
|
|
|
const enhancementPrompt = this.createTechnicalEnhancementPrompt(chunk, csvData, config);
|
|
|
|
const response = await callLLM(config.llmProvider, enhancementPrompt, {
|
|
temperature: 0.4, // Précision technique
|
|
maxTokens: 3000
|
|
}, csvData?.personality);
|
|
|
|
const chunkResults = this.parseTechnicalResponse(response, chunk);
|
|
Object.assign(results, chunkResults);
|
|
|
|
logSh(` ✅ Chunk technique ${chunkIndex + 1}: ${Object.keys(chunkResults).length} améliorés`, 'DEBUG');
|
|
|
|
// Délai entre chunks
|
|
if (chunkIndex < chunks.length - 1) {
|
|
await sleep(1500);
|
|
}
|
|
|
|
} catch (error) {
|
|
logSh(` ❌ Chunk technique ${chunkIndex + 1} échoué: ${error.message}`, 'ERROR');
|
|
|
|
// Fallback: conserver contenu original
|
|
chunk.forEach(element => {
|
|
results[element.tag] = element.content;
|
|
});
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* AMÉLIORER TOUS ÉLÉMENTS MODE DIRECT
|
|
*/
|
|
async enhanceAllElementsDirect(content, csvData, config) {
|
|
const allElements = Object.entries(content).map(([tag, text]) => ({
|
|
tag,
|
|
content: text,
|
|
technicalTerms: [],
|
|
improvements: ['amélioration_générale_technique'],
|
|
missingTerms: [] // Ajout de la propriété manquante
|
|
}));
|
|
|
|
return await this.enhanceTechnicalElements(allElements, csvData, config);
|
|
}
|
|
|
|
// ============= HELPER METHODS =============
|
|
|
|
/**
|
|
* Analyser élément technique individuel
|
|
*/
|
|
analyzeTechnicalElement(text, contextualTerms, csvData) {
|
|
let score = 0;
|
|
const foundTerms = [];
|
|
const missingTerms = [];
|
|
const improvements = [];
|
|
|
|
// 1. Détecter termes techniques présents
|
|
contextualTerms.forEach(term => {
|
|
if (text.toLowerCase().includes(term.toLowerCase())) {
|
|
foundTerms.push(term);
|
|
} else if (text.length > 100) { // Seulement pour textes longs
|
|
missingTerms.push(term);
|
|
}
|
|
});
|
|
|
|
// 2. Évaluer manque de précision technique
|
|
if (foundTerms.length === 0 && text.length > 80) {
|
|
score += 0.4;
|
|
improvements.push('ajout_termes_techniques');
|
|
}
|
|
|
|
// 3. Détecter vocabulaire trop générique
|
|
const genericWords = ['produit', 'solution', 'service', 'offre', 'article'];
|
|
const genericCount = genericWords.filter(word =>
|
|
text.toLowerCase().includes(word)
|
|
).length;
|
|
|
|
if (genericCount > 1) {
|
|
score += 0.3;
|
|
improvements.push('spécialisation_vocabulaire');
|
|
}
|
|
|
|
// 4. Manque de données techniques (dimensions, etc.)
|
|
if (text.length > 50 && !(/\d+\s*(mm|cm|m|%|°|kg|g)/.test(text))) {
|
|
score += 0.2;
|
|
improvements.push('ajout_données_techniques');
|
|
}
|
|
|
|
// 5. Contexte métier spécifique
|
|
if (csvData?.mc0 && !text.toLowerCase().includes(csvData.mc0.toLowerCase().split(' ')[0])) {
|
|
score += 0.1;
|
|
improvements.push('intégration_contexte_métier');
|
|
}
|
|
|
|
return {
|
|
needsImprovement: score > 0.3,
|
|
score,
|
|
foundTerms,
|
|
missingTerms: missingTerms.slice(0, 3), // Limiter à 3 termes manquants
|
|
improvements
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Obtenir termes techniques contextuels
|
|
*/
|
|
getContextualTechnicalTerms(mc0, targetTerms) {
|
|
// Termes de base signalétique
|
|
const baseTerms = [
|
|
'dibond', 'aluminium', 'PMMA', 'acrylique', 'plexiglas',
|
|
'impression', 'gravure', 'découpe', 'fraisage', 'perçage',
|
|
'adhésif', 'fixation', 'visserie', 'support'
|
|
];
|
|
|
|
// Termes spécifiques selon contexte
|
|
const contextualTerms = [];
|
|
|
|
if (mc0) {
|
|
const mc0Lower = mc0.toLowerCase();
|
|
|
|
if (mc0Lower.includes('plaque')) {
|
|
contextualTerms.push('épaisseur 3mm', 'format standard', 'finition brossée', 'anodisation');
|
|
}
|
|
|
|
if (mc0Lower.includes('signalétique')) {
|
|
contextualTerms.push('norme ISO', 'pictogramme', 'contraste visuel', 'lisibilité');
|
|
}
|
|
|
|
if (mc0Lower.includes('personnalisée')) {
|
|
contextualTerms.push('découpe forme', 'impression numérique', 'quadrichromie', 'pantone');
|
|
}
|
|
}
|
|
|
|
// Ajouter termes ciblés si fournis
|
|
if (targetTerms && Array.isArray(targetTerms)) {
|
|
contextualTerms.push(...targetTerms);
|
|
}
|
|
|
|
return [...baseTerms, ...contextualTerms];
|
|
}
|
|
|
|
/**
|
|
* Créer prompt amélioration technique
|
|
*/
|
|
createTechnicalEnhancementPrompt(chunk, csvData, config) {
|
|
const personality = csvData?.personality;
|
|
|
|
let prompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces contenus.
|
|
|
|
CONTEXTE: ${csvData?.mc0 || 'Signalétique personnalisée'} - Secteur: impression/signalétique
|
|
${personality ? `PERSONNALITÉ: ${personality.nom} (${personality.style})` : ''}
|
|
INTENSITÉ: ${config.intensity} (0.5=léger, 1.0=standard, 1.5=intensif)
|
|
|
|
ÉLÉMENTS À AMÉLIORER TECHNIQUEMENT:
|
|
|
|
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
|
|
CONTENU: "${item.content}"
|
|
AMÉLIORATIONS: ${item.improvements.join(', ')}
|
|
${item.missingTerms.length > 0 ? `TERMES À INTÉGRER: ${item.missingTerms.join(', ')}` : ''}`).join('\n\n')}
|
|
|
|
CONSIGNES TECHNIQUES:
|
|
- GARDE exactement le même message et ton${personality ? ` ${personality.style}` : ''}
|
|
- AJOUTE précision technique naturelle et vocabulaire spécialisé
|
|
- INTÈGRE termes métier : matériaux, procédés, normes, dimensions
|
|
- REMPLACE vocabulaire générique par termes techniques appropriés
|
|
- ÉVITE jargon incompréhensible, reste accessible
|
|
- PRESERVE longueur approximative (±15%)
|
|
|
|
VOCABULAIRE TECHNIQUE RECOMMANDÉ:
|
|
- Matériaux: dibond, aluminium anodisé, PMMA coulé, PVC expansé
|
|
- Procédés: impression UV, gravure laser, découpe numérique, fraisage CNC
|
|
- Finitions: brossé, poli, texturé, laqué
|
|
- Fixations: perçage, adhésif double face, vis inox, plots de fixation
|
|
|
|
FORMAT RÉPONSE:
|
|
[1] Contenu avec amélioration technique précise
|
|
[2] Contenu avec amélioration technique précise
|
|
etc...
|
|
|
|
IMPORTANT: Réponse DIRECTE par les contenus améliorés, pas d'explication.`;
|
|
|
|
return prompt;
|
|
}
|
|
|
|
/**
|
|
* Parser réponse technique
|
|
*/
|
|
parseTechnicalResponse(response, chunk) {
|
|
const results = {};
|
|
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
|
|
let match;
|
|
let index = 0;
|
|
|
|
while ((match = regex.exec(response)) && index < chunk.length) {
|
|
let technicalContent = match[2].trim();
|
|
const element = chunk[index];
|
|
|
|
// Nettoyer contenu technique
|
|
technicalContent = this.cleanTechnicalContent(technicalContent);
|
|
|
|
if (technicalContent && technicalContent.length > 10) {
|
|
results[element.tag] = technicalContent;
|
|
logSh(`✅ Amélioré technique [${element.tag}]: "${technicalContent.substring(0, 60)}..."`, 'DEBUG');
|
|
} else {
|
|
results[element.tag] = element.content; // Fallback
|
|
logSh(`⚠️ Fallback technique [${element.tag}]: amélioration invalide`, 'WARNING');
|
|
}
|
|
|
|
index++;
|
|
}
|
|
|
|
// Compléter les manquants
|
|
while (index < chunk.length) {
|
|
const element = chunk[index];
|
|
results[element.tag] = element.content;
|
|
index++;
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Nettoyer contenu technique généré
|
|
*/
|
|
cleanTechnicalContent(content) {
|
|
if (!content) return content;
|
|
|
|
// Supprimer préfixes indésirables
|
|
content = content.replace(/^(voici\s+)?le\s+contenu\s+amélioré\s*[:.]?\s*/gi, '');
|
|
content = content.replace(/^(avec\s+)?amélioration\s+technique\s*[:.]?\s*/gi, '');
|
|
content = content.replace(/^(bon,?\s*)?(alors,?\s*)?pour\s+/gi, '');
|
|
|
|
// Nettoyer formatage
|
|
content = content.replace(/\*\*[^*]+\*\*/g, ''); // Gras markdown
|
|
content = content.replace(/\s{2,}/g, ' '); // Espaces multiples
|
|
content = content.trim();
|
|
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* Compter différences entre contenus
|
|
*/
|
|
countDifferences(original, enhanced) {
|
|
let count = 0;
|
|
|
|
Object.keys(original).forEach(tag => {
|
|
if (enhanced[tag] && enhanced[tag] !== original[tag]) {
|
|
count++;
|
|
}
|
|
});
|
|
|
|
return count;
|
|
}
|
|
}
|
|
|
|
module.exports = { TechnicalLayer }; |