confluent/ConfluentTranslator/server.js
StillHammer 894645e640 Implémentation du système de prompt contextuel intelligent
Nouveau système qui analyse le texte français et génère des prompts optimisés en incluant uniquement le vocabulaire pertinent du lexique, réduisant drastiquement le nombre de tokens.

# Backend

- contextAnalyzer.js : Analyse contextuelle avec lemmatisation française
  - Tokenization avec normalisation des accents
  - Recherche intelligente (correspondances exactes, synonymes, formes conjuguées)
  - Calcul dynamique du nombre max d'entrées selon longueur (30/50/100)
  - Expansion sémantique niveau 1 (modulaire pour futur)
  - Fallback racines (309 racines si mots inconnus)

- promptBuilder.js : Génération de prompts optimisés
  - Templates de base sans lexique massif
  - Injection ciblée du vocabulaire pertinent
  - Formatage par type (racines sacrées, standards, verbes)
  - Support fallback avec toutes les racines

- server.js : Intégration API avec structure 3 layers
  - Layer 1: Traduction pure
  - Layer 2: Métadonnées contextuelles (mots trouvés, optimisation)
  - Layer 3: Explications du LLM (décomposition, notes)

- lexiqueLoader.js : Fusion du lexique simple data/lexique-francais-confluent.json
  - Charge 636 entrées (516 ancien + 120 merged)

# Frontend

- index.html : Interface 3 layers collapsibles
  - Layer 1 (toujours visible) : Traduction avec mise en valeur
  - Layer 2 (collapsible) : Contexte lexical + statistiques d'optimisation
  - Layer 3 (collapsible) : Explications linguistiques du LLM
  - Design dark complet (fix fond blanc + listes déroulantes)
  - Animations smooth pour expand/collapse

# Documentation

- docs/PROMPT_CONTEXTUEL_INTELLIGENT.md : Plan complet validé
  - Architecture technique détaillée
  - Cas d'usage et décisions de design
  - Métriques de succès

# Tests

- Tests exhaustifs avec validation exigeante
- Économie moyenne : 81% de tokens
- Économie minimale : 52% (même avec fallback)
- Context skimming opérationnel et validé

# Corrections

- ancien-confluent/lexique/02-racines-standards.json : Fix erreur JSON ligne 527

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 11:08:45 +08:00

320 lines
9.2 KiB
JavaScript

require('dotenv').config({ path: '../.env' });
const express = require('express');
const path = require('path');
const fs = require('fs');
const { Anthropic } = require('@anthropic-ai/sdk');
const OpenAI = require('openai');
const {
loadAllLexiques,
searchLexique,
generateLexiqueSummary,
buildReverseIndex
} = require('./lexiqueLoader');
const { analyzeContext } = require('./contextAnalyzer');
const { buildContextualPrompt, getBasePrompt, getPromptStats } = require('./promptBuilder');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use(express.static('public'));
// Load prompts
const protoPrompt = fs.readFileSync(path.join(__dirname, 'prompts', 'proto-system.txt'), 'utf-8');
const ancienPrompt = fs.readFileSync(path.join(__dirname, 'prompts', 'ancien-system.txt'), 'utf-8');
// Load lexiques dynamically from JSON files
const baseDir = path.join(__dirname, '..');
let lexiques = { proto: null, ancien: null };
let reverseIndexes = { proto: null, ancien: null };
function reloadLexiques() {
console.log('Loading lexiques...');
lexiques = loadAllLexiques(baseDir);
reverseIndexes = {
proto: buildReverseIndex(lexiques.proto),
ancien: buildReverseIndex(lexiques.ancien)
};
console.log('Lexiques loaded successfully');
}
// Initial load
reloadLexiques();
// Legacy lexique endpoint (for backward compatibility)
app.get('/lexique', (req, res) => {
// Return ancien-confluent by default (legacy behavior)
if (!lexiques.ancien) {
return res.status(500).json({ error: 'Lexique not loaded' });
}
res.json(lexiques.ancien);
});
// New lexique endpoints
app.get('/api/lexique/:variant', (req, res) => {
const { variant } = req.params;
if (variant !== 'proto' && variant !== 'ancien') {
return res.status(400).json({ error: 'Invalid variant. Use "proto" or "ancien"' });
}
if (!lexiques[variant]) {
return res.status(500).json({ error: `Lexique ${variant} not loaded` });
}
res.json(lexiques[variant]);
});
// Search endpoint
app.get('/api/search', (req, res) => {
const { q, variant = 'ancien', direction = 'fr2conf' } = req.query;
if (!q) {
return res.status(400).json({ error: 'Missing query parameter "q"' });
}
if (variant !== 'proto' && variant !== 'ancien') {
return res.status(400).json({ error: 'Invalid variant. Use "proto" or "ancien"' });
}
const results = searchLexique(lexiques[variant], q, direction);
res.json({ query: q, variant, direction, results });
});
// Stats endpoint
app.get('/api/stats', (req, res) => {
res.json({
proto: {
total_entries: lexiques.proto?.meta?.total_entries || 0,
files_loaded: lexiques.proto?.meta?.files_loaded?.length || 0,
loaded_at: lexiques.proto?.meta?.loaded_at
},
ancien: {
total_entries: lexiques.ancien?.meta?.total_entries || 0,
files_loaded: lexiques.ancien?.meta?.files_loaded?.length || 0,
loaded_at: lexiques.ancien?.meta?.loaded_at
}
});
});
// Reload endpoint (for development)
app.post('/api/reload', (req, res) => {
try {
reloadLexiques();
res.json({
success: true,
message: 'Lexiques reloaded',
stats: {
proto: lexiques.proto?.meta?.total_entries || 0,
ancien: lexiques.ancien?.meta?.total_entries || 0
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Build enhanced prompt with lexique data
function buildEnhancedPrompt(basePrompt, variant) {
const lexique = lexiques[variant];
if (!lexique) return basePrompt;
const summary = generateLexiqueSummary(lexique, 300);
return `${basePrompt}
# LEXIQUE COMPLET (${lexique.meta.total_entries} entrées)
${summary}
`;
}
// Translation endpoint (NOUVEAU SYSTÈME CONTEXTUEL)
app.post('/translate', async (req, res) => {
const { text, target, provider, model, useLexique = true } = req.body;
if (!text || !target || !provider || !model) {
return res.status(400).json({ error: 'Missing parameters' });
}
const variant = target === 'proto' ? 'proto' : 'ancien';
try {
let systemPrompt;
let contextMetadata = null;
// NOUVEAU: Analyse contextuelle et génération de prompt optimisé
if (useLexique) {
const contextResult = analyzeContext(text, lexiques[variant]);
systemPrompt = buildContextualPrompt(contextResult, variant);
// Générer métadonnées pour Layer 2
const promptStats = getPromptStats(systemPrompt, contextResult);
contextMetadata = {
wordsFound: contextResult.metadata.wordsFound,
wordsNotFound: contextResult.metadata.wordsNotFound,
entriesUsed: contextResult.metadata.entriesUsed,
totalLexiqueSize: contextResult.metadata.totalLexiqueSize,
tokensFullLexique: promptStats.fullLexiqueTokens,
tokensUsed: promptStats.promptTokens,
tokensSaved: promptStats.tokensSaved,
savingsPercent: promptStats.savingsPercent,
useFallback: contextResult.useFallback,
expansionLevel: contextResult.metadata.expansionLevel
};
} else {
systemPrompt = getBasePrompt(variant);
}
let translation;
let rawResponse;
if (provider === 'anthropic') {
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
const message = await anthropic.messages.create({
model: model,
max_tokens: 1024,
system: systemPrompt,
messages: [
{ role: 'user', content: text }
]
});
rawResponse = message.content[0].text;
translation = rawResponse;
} else if (provider === 'openai') {
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const completion = await openai.chat.completions.create({
model: model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: text }
]
});
rawResponse = completion.choices[0].message.content;
translation = rawResponse;
} else {
return res.status(400).json({ error: 'Unknown provider' });
}
// Parser la réponse pour extraire Layer 1 et Layer 3
const parsed = parseTranslationResponse(rawResponse);
// Construire la réponse avec les 3 layers
const response = {
// Layer 1: Traduction
layer1: {
translation: parsed.translation
},
// Layer 2: Contexte (COT hors LLM)
layer2: contextMetadata,
// Layer 3: Explications LLM
layer3: {
decomposition: parsed.decomposition,
notes: parsed.notes,
wordsCreated: parsed.wordsCreated || []
},
// Compatibilité avec ancien format
translation: parsed.translation
};
res.json(response);
} catch (error) {
console.error('Translation error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Parse la réponse du LLM pour extraire les différentes sections
* @param {string} response - Réponse brute du LLM
* @returns {Object} - Sections parsées
*/
function parseTranslationResponse(response) {
const lines = response.split('\n');
let translation = '';
let decomposition = '';
let notes = '';
let currentSection = null;
for (const line of lines) {
const trimmed = line.trim();
// Détecter les sections
if (trimmed.match(/^(Ancien )?Confluent:/i)) {
currentSection = 'translation';
continue;
}
if (trimmed.match(/^D[ée]composition:/i)) {
currentSection = 'decomposition';
continue;
}
if (trimmed.match(/^Notes?:/i) || trimmed.match(/^Explication:/i)) {
currentSection = 'notes';
continue;
}
// Ajouter le contenu à la section appropriée
if (currentSection === 'translation' && trimmed && !trimmed.match(/^---/)) {
translation += line + '\n';
} else if (currentSection === 'decomposition' && trimmed) {
decomposition += line + '\n';
} else if (currentSection === 'notes' && trimmed) {
notes += line + '\n';
} else if (!currentSection && trimmed && !trimmed.match(/^---/)) {
// Si pas de section détectée, c'est probablement la traduction
translation += line + '\n';
}
}
return {
translation: translation.trim() || response.trim(),
decomposition: decomposition.trim(),
notes: notes.trim()
};
}
// Batch translation endpoint
app.post('/api/translate/batch', async (req, res) => {
const { words, target = 'ancien' } = req.body;
if (!words || !Array.isArray(words)) {
return res.status(400).json({ error: 'Missing or invalid "words" array' });
}
const variant = target === 'proto' ? 'proto' : 'ancien';
const results = {};
for (const word of words) {
const found = searchLexique(lexiques[variant], word, 'fr2conf');
if (found.length > 0 && found[0].traductions?.length > 0) {
results[word] = {
found: true,
traduction: found[0].traductions[0].confluent,
all_traductions: found[0].traductions
};
} else {
results[word] = { found: false };
}
}
res.json({ target, results });
});
app.listen(PORT, () => {
console.log(`ConfluentTranslator running on http://localhost:${PORT}`);
console.log(`Loaded: ${lexiques.ancien?.meta?.total_entries || 0} ancien entries, ${lexiques.proto?.meta?.total_entries || 0} proto entries`);
});