confluent/ConfluentTranslator/promptBuilder.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

224 lines
6.3 KiB
JavaScript

/**
* Prompt Builder - Génère des prompts contextuels optimisés
*
* Fonctionnalités:
* 1. Templates de base (règles linguistiques sans lexique massif)
* 2. Injection de vocabulaire ciblé
* 3. Fallback racines
* 4. Formatage optimisé pour le LLM
*/
const fs = require('fs');
const path = require('path');
/**
* Charge le template de prompt de base depuis les fichiers
* @param {string} variant - 'proto' ou 'ancien'
* @returns {string} - Template de prompt
*/
function loadBaseTemplate(variant) {
const templatePath = path.join(__dirname, 'prompts', `${variant}-system.txt`);
if (!fs.existsSync(templatePath)) {
throw new Error(`Template not found: ${templatePath}`);
}
return fs.readFileSync(templatePath, 'utf-8');
}
/**
* Génère la section vocabulaire pour le prompt
* Format compact et structuré
* @param {Array} entries - Entrées du lexique pertinentes
* @returns {string} - Section vocabulaire formatée
*/
function formatVocabularySection(entries) {
if (!entries || entries.length === 0) {
return '';
}
const lines = ['\n# VOCABULAIRE PERTINENT POUR CETTE TRADUCTION\n'];
// Grouper par type
const byType = {
racine_sacree: [],
racine: [],
verbe: [],
nom: [],
autre: []
};
entries.forEach(entry => {
if (entry.traductions && entry.traductions.length > 0) {
const trad = entry.traductions[0];
const type = trad.type || 'autre';
const key = type === 'racine_sacree' ? 'racine_sacree' :
type === 'racine' ? 'racine' :
type === 'verbe' ? 'verbe' :
type === 'nom' ? 'nom' : 'autre';
byType[key].push({
fr: entry.mot_francais,
conf: trad.confluent,
forme_liee: trad.forme_liee || trad.confluent,
domaine: trad.domaine || '',
note: trad.note || ''
});
}
});
// Formatter par type
if (byType.racine_sacree.length > 0) {
lines.push('## Racines sacrées (voyelle initiale)\n');
byType.racine_sacree.forEach(item => {
lines.push(`- ${item.conf} (${item.fr}) [forme liée: ${item.forme_liee}]`);
});
lines.push('');
}
if (byType.racine.length > 0) {
lines.push('## Racines standards\n');
byType.racine.forEach(item => {
lines.push(`- ${item.conf} (${item.fr}) [forme liée: ${item.forme_liee}]`);
});
lines.push('');
}
if (byType.verbe.length > 0) {
lines.push('## Verbes\n');
byType.verbe.forEach(item => {
lines.push(`- ${item.fr}${item.conf}`);
});
lines.push('');
}
if (byType.nom.length > 0) {
lines.push('## Noms et concepts\n');
byType.nom.forEach(item => {
lines.push(`- ${item.fr}${item.conf}`);
});
lines.push('');
}
if (byType.autre.length > 0) {
lines.push('## Autres\n');
byType.autre.forEach(item => {
lines.push(`- ${item.fr}${item.conf}`);
});
lines.push('');
}
return lines.join('\n');
}
/**
* Génère la section de fallback avec toutes les racines
* @param {Array} roots - Liste des racines
* @returns {string} - Section racines formatée
*/
function formatRootsFallback(roots) {
if (!roots || roots.length === 0) {
return '';
}
const lines = ['\n# RACINES DISPONIBLES (à composer)\n'];
lines.push('⚠️ Les mots demandés ne sont pas dans le lexique. Compose-les à partir des racines ci-dessous.\n');
const sacrees = roots.filter(r => r.sacree);
const standards = roots.filter(r => !r.sacree);
if (sacrees.length > 0) {
lines.push(`## Racines sacrées (${sacrees.length})\n`);
sacrees.forEach(r => {
lines.push(`- ${r.confluent} (${r.mot_francais}) [forme liée: ${r.forme_liee}] - ${r.domaine}`);
});
lines.push('');
}
if (standards.length > 0) {
lines.push(`## Racines standards (${standards.length})\n`);
standards.forEach(r => {
lines.push(`- ${r.confluent} (${r.mot_francais}) [forme liée: ${r.forme_liee}] - ${r.domaine}`);
});
lines.push('');
}
lines.push('IMPORTANT: Utilise les liaisons sacrées pour composer les mots manquants.\n');
return lines.join('\n');
}
/**
* Construit un prompt contextuel complet
* @param {Object} contextResult - Résultat de analyzeContext()
* @param {string} variant - 'proto' ou 'ancien'
* @returns {string} - Prompt complet optimisé
*/
function buildContextualPrompt(contextResult, variant = 'ancien') {
// Charger le template de base
const basePrompt = loadBaseTemplate(variant);
// Si fallback, injecter toutes les racines
if (contextResult.useFallback) {
const rootsSection = formatRootsFallback(contextResult.rootsFallback);
return basePrompt + '\n' + rootsSection;
}
// Sinon, injecter uniquement le vocabulaire pertinent
const vocabularySection = formatVocabularySection(contextResult.entries);
return basePrompt + '\n' + vocabularySection;
}
/**
* Construit le prompt de base sans aucun lexique (pour useLexique=false)
* @param {string} variant - 'proto' ou 'ancien'
* @returns {string} - Prompt de base uniquement
*/
function getBasePrompt(variant = 'ancien') {
return loadBaseTemplate(variant);
}
/**
* Estime le nombre de tokens dans un texte
* Estimation simple : ~1 token pour 4 caractères
* @param {string} text - Texte à estimer
* @returns {number} - Nombre de tokens estimé
*/
function estimateTokens(text) {
return Math.ceil(text.length / 4);
}
/**
* Génère des statistiques sur le prompt généré
* @param {string} prompt - Prompt généré
* @param {Object} contextResult - Résultat du contexte
* @returns {Object} - Statistiques
*/
function getPromptStats(prompt, contextResult) {
const promptTokens = estimateTokens(prompt);
const fullLexiqueTokens = contextResult.metadata.tokensFullLexique;
const saved = fullLexiqueTokens - promptTokens;
const savingsPercent = Math.round((saved / fullLexiqueTokens) * 100);
return {
promptTokens,
fullLexiqueTokens,
tokensSaved: saved,
savingsPercent,
entriesUsed: contextResult.metadata.entriesUsed,
useFallback: contextResult.useFallback,
wordsFound: contextResult.metadata.wordsFound.length,
wordsNotFound: contextResult.metadata.wordsNotFound.length
};
}
module.exports = {
loadBaseTemplate,
formatVocabularySection,
formatRootsFallback,
buildContextualPrompt,
getBasePrompt,
estimateTokens,
getPromptStats
};