smart-thinking-mcp
Version:
Un serveur MCP avancé pour le raisonnement multi-dimensionnel, adaptatif et collaboratif
642 lines • 28.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.VerificationMemory = void 0;
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const config_1 = require("./config");
const path_utils_1 = require("./utils/path-utils");
const persistence_utils_1 = require("./utils/persistence-utils");
const isTestEnvironment = process.env.NODE_ENV === 'test';
/**
* Classe qui gère la mémoire des vérifications avec recherche vectorielle efficace
* pour assurer leur persistance à travers les différentes étapes du raisonnement
* AMÉLIORÉ: Meilleure gestion des similarités et persistance des vérifications
*/
class VerificationMemory {
static instance = null;
similarityEngine;
// Structure pour stocker les vérifications avec index pour recherche efficace
verifications = new Map();
// Index par session pour accès rapide
sessionIndex = new Map();
// NOUVEAU: Cache de similarité pour éviter de recalculer les similitudes entre les mêmes textes
similarityCache = new Map();
cleanupTimers = [];
dataDir;
storageFilePath;
initialization;
initialized = false;
emit(level, ...args) {
if (isTestEnvironment) {
return;
}
console[level](...args);
}
/**
* Méthode statique pour implémenter le singleton
*
* @returns L'instance unique de VerificationMemory
*/
static getInstance() {
if (!VerificationMemory.instance) {
VerificationMemory.instance = new VerificationMemory();
}
return VerificationMemory.instance;
}
static resetInstance() {
if (VerificationMemory.instance) {
VerificationMemory.instance.stopCleanupTasks();
VerificationMemory.instance = null;
}
}
/**
* Constructeur privé pour empêcher l'instanciation directe
*/
constructor() {
this.dataDir = path_utils_1.PathUtils.getDataDirectory();
this.storageFilePath = path_1.default.join(this.dataDir, 'verifications.json');
this.initialization = this.loadFromStorage()
.catch((error) => {
this.emit('error', 'VerificationMemory: Erreur lors du chargement du stockage persistant', error);
})
.finally(() => {
this.initialized = true;
});
if (!isTestEnvironment) {
// Configurer le nettoyage périodique des entrées expirées uniquement hors tests pour éviter les handles ouverts
this.cleanupTimers.push(setInterval(() => this.cleanExpiredEntries(), config_1.VerificationConfig.MEMORY.CACHE_EXPIRATION / 2));
// Nettoyer également le cache de similarité périodiquement pour éviter les fuites de mémoire
this.cleanupTimers.push(setInterval(() => this.cleanSimilarityCache(), config_1.VerificationConfig.MEMORY.CACHE_EXPIRATION));
}
this.emit('log', 'VerificationMemory: Système de mémoire de vérification initialisé');
}
stopCleanupTasks() {
for (const timer of this.cleanupTimers) {
clearInterval(timer);
}
this.cleanupTimers = [];
}
async ensureInitialized() {
if (this.initialized) {
return;
}
try {
await this.initialization;
}
catch (error) {
this.emit('error', 'VerificationMemory: Erreur lors de l\'initialisation', error);
}
finally {
this.initialized = true;
}
}
async loadFromStorage() {
await path_utils_1.PathUtils.ensureDirectoryExists(this.dataDir);
const exists = await fs_1.promises.stat(this.storageFilePath)
.then(() => true)
.catch(() => false);
if (!exists) {
return;
}
const content = await fs_1.promises.readFile(this.storageFilePath, 'utf8');
const parsed = JSON.parse(content);
const entries = Array.isArray(parsed)
? parsed
: Array.isArray(parsed.verifications)
? (parsed.verifications)
: [];
this.verifications.clear();
this.sessionIndex.clear();
for (const rawEntry of entries) {
const sanitized = (0, persistence_utils_1.sanitizeVerificationEntry)(rawEntry, {
defaultSessionId: config_1.SystemConfig.DEFAULT_SESSION_ID,
defaultTtlMs: config_1.VerificationConfig.MEMORY.DEFAULT_SESSION_TTL,
});
if (!sanitized) {
continue;
}
this.verifications.set(sanitized.id, sanitized);
if (!this.sessionIndex.has(sanitized.sessionId)) {
this.sessionIndex.set(sanitized.sessionId, new Set());
}
this.sessionIndex.get(sanitized.sessionId).add(sanitized.id);
}
if (this.verifications.size > 0) {
this.emit('log', `VerificationMemory: ${this.verifications.size} entrées restaurées depuis le stockage`);
}
}
async persistToStorage() {
const payload = {
version: 1,
updatedAt: new Date().toISOString(),
verifications: Array.from(this.verifications.values()).map((entry) => (0, persistence_utils_1.prepareVerificationForStorage)(entry)),
};
await path_utils_1.PathUtils.ensureDirectoryExists(this.dataDir);
await fs_1.promises.writeFile(this.storageFilePath, JSON.stringify(payload, null, 2), 'utf8');
}
requestPersist() {
Promise.resolve(this.initialization)
.catch(() => undefined)
.then(() => this.persistToStorage())
.catch((error) => {
this.emit('error', 'VerificationMemory: Erreur lors de la sauvegarde du stockage persistant', error);
});
}
/**
* Définit le moteur de similarité à utiliser pour la recherche sémantique
*
* @param similarityEngine Moteur de similarité local
*/
setSimilarityEngine(similarityEngine) {
this.similarityEngine = similarityEngine;
this.emit('log', 'VerificationMemory: SimilarityEngine configuré');
}
/**
* Ajoute une nouvelle vérification à la mémoire
* AMÉLIORÉ: Meilleure détection des duplicatas avec cache de similarité
*
* @param text Texte de l'information vérifiée
* @param status Statut de vérification
* @param confidence Niveau de confiance
* @param sources Sources utilisées pour la vérification
* @param sessionId Identifiant de la session
* @param ttl Durée de vie en millisecondes (optionnel)
* @returns Identifiant de la vérification ajoutée
*/
async addVerification(text, status, confidence, sources = [], sessionId = config_1.SystemConfig.DEFAULT_SESSION_ID, ttl = config_1.VerificationConfig.MEMORY.DEFAULT_SESSION_TTL) {
await this.ensureInitialized();
this.emit('error', `VerificationMemory: Ajout d'une vérification avec statut ${status}, confiance ${confidence.toFixed(2)}`);
// Vérifier si une entrée très similaire existe déjà dans cette session
const existingEntry = await this.findExactDuplicate(text, sessionId);
if (existingEntry) {
this.emit('error', `VerificationMemory: Entrée similaire trouvée, mise à jour plutôt que création`);
// Mettre à jour l'entrée existante au lieu d'en créer une nouvelle
this.verifications.set(existingEntry.id, {
...existingEntry,
status,
confidence,
sources,
timestamp: new Date(),
expiresAt: new Date(Date.now() + ttl)
});
this.requestPersist();
return existingEntry.id;
}
// Générer un identifiant unique
const id = `verification-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
// Calculer la date d'expiration
const expiresAt = new Date(Date.now() + ttl);
// Créer l'entrée
const entry = {
id,
text,
status,
confidence,
sources,
timestamp: new Date(),
sessionId,
expiresAt
};
// Ajouter à la mémoire des vérifications
this.verifications.set(id, entry);
// Mettre à jour l'index par session
if (!this.sessionIndex.has(sessionId)) {
this.sessionIndex.set(sessionId, new Set());
}
this.sessionIndex.get(sessionId).add(id);
this.emit('error', `VerificationMemory: Vérification ajoutée avec succès, ID: ${id}`);
this.requestPersist();
return id;
}
/**
* Recherche une vérification existante identique pour éviter les doublons
* AMÉLIORÉ: Utilisation du cache de similarité pour des recherches plus rapides
*
* @param text Texte à rechercher
* @param sessionId ID de session
* @returns L'entrée existante si trouvée, null sinon
*/
async findExactDuplicate(text, sessionId) {
await this.ensureInitialized();
this.emit('error', `VerificationMemory: Recherche de duplicata pour "${text.substring(0, 30)}..."`);
const sessionEntries = this.getSessionEntriesArray(sessionId);
if (sessionEntries.length === 0) {
this.emit('error', 'VerificationMemory: Aucun duplicata trouvé');
return null;
}
const exactMatch = sessionEntries.find(entry => entry.text === text);
if (exactMatch) {
this.emit('error', 'VerificationMemory: Duplicata exact trouvé');
return exactMatch;
}
if (!this.similarityEngine) {
this.emit('error', 'VerificationMemory: SimilarityEngine indisponible, impossible de comparer sémantiquement');
return null;
}
try {
const candidateTexts = sessionEntries.map(entry => entry.text);
const results = await this.similarityEngine.findSimilarTexts(text, candidateTexts, 1, config_1.VerificationConfig.SIMILARITY.MEDIUM_SIMILARITY);
if (results.length > 0) {
const bestMatch = sessionEntries.find(entry => entry.text === results[0].text);
if (bestMatch) {
this.setCachedSimilarity(`${text.substring(0, 50)}_${bestMatch.id}`, results[0].score);
this.emit('error', `VerificationMemory: Duplicata trouvé avec similarité ${results[0].score.toFixed(3)}`);
return bestMatch;
}
}
}
catch (error) {
this.emit('error', 'VerificationMemory: Erreur lors de la recherche de duplicata via SimilarityEngine:', error);
}
this.emit('error', 'VerificationMemory: Aucun duplicata trouvé');
return null;
}
/**
* NOUVEAU: Obtient une similarité mise en cache
*
* @param key Clé du cache
* @returns Similarité mise en cache ou undefined si non trouvée
*/
getCachedSimilarity(key) {
for (const [prefix, similarities] of this.similarityCache.entries()) {
if (key.startsWith(prefix)) {
return similarities.get(key);
}
}
return undefined;
}
/**
* NOUVEAU: Définit une similarité dans le cache
*
* @param key Clé du cache
* @param similarity Valeur de similarité à mettre en cache
*/
setCachedSimilarity(key, similarity) {
const prefix = key.split('_')[0];
if (!this.similarityCache.has(prefix)) {
this.similarityCache.set(prefix, new Map());
}
this.similarityCache.get(prefix).set(key, similarity);
}
/**
* Recherche une vérification existante similaire à l'information fournie
* AMÉLIORÉ: Recherche plus efficace avec seuils de similarité ajustés
*
* @param text Texte de l'information à rechercher
* @param sessionId Identifiant de la session
* @param similarityThreshold Seuil de similarité
* @returns La vérification trouvée, ou null si aucune correspondance
*/
async findVerification(text, sessionId = config_1.SystemConfig.DEFAULT_SESSION_ID, similarityThreshold = config_1.VerificationConfig.SIMILARITY.LOW_SIMILARITY * 0.9 // Réduction supplémentaire de 10%
) {
await this.ensureInitialized();
this.emit('error', `VerificationMemory: Recherche de vérification pour "${text.substring(0, 30)}..." (seuil: ${similarityThreshold})`);
// Obtenir les ID des vérifications pour cette session
const sessionIds = this.sessionIndex.get(sessionId);
if (!sessionIds || sessionIds.size === 0) {
this.emit('error', `VerificationMemory: Aucune vérification pour la session ${sessionId}`);
return null;
}
this.emit('error', `VerificationMemory: ${sessionIds.size} vérifications disponibles pour cette session`);
const sessionEntries = this.getSessionEntriesArray(sessionId);
if (!this.similarityEngine) {
this.emit('error', 'VerificationMemory: SimilarityEngine indisponible, utilisation de la recherche textuelle');
return this.fallbackToTextSearch(text, sessionId);
}
try {
const candidateTexts = sessionEntries.map(entry => entry.text);
const results = await this.similarityEngine.findSimilarTexts(text, candidateTexts, candidateTexts.length, similarityThreshold);
if (results.length === 0) {
this.emit('error', 'VerificationMemory: Aucune correspondance dépassant le seuil via SimilarityEngine');
return this.fallbackToTextSearch(text, sessionId);
}
const bestMatchText = results[0].text;
const bestEntry = sessionEntries.find(entry => entry.text === bestMatchText);
if (bestEntry) {
this.setCachedSimilarity(`${text.substring(0, 50)}_${bestEntry.id}`, results[0].score);
this.emit('error', `VerificationMemory: Vérification trouvée avec similarité ${results[0].score.toFixed(3)}`);
return {
id: bestEntry.id,
status: bestEntry.status,
confidence: bestEntry.confidence,
sources: bestEntry.sources,
timestamp: bestEntry.timestamp,
similarity: results[0].score,
text: bestEntry.text
};
}
}
catch (error) {
this.emit('error', 'VerificationMemory: Erreur lors de la recherche via SimilarityEngine:', error);
return this.fallbackToTextSearch(text, sessionId);
}
this.emit('error', 'VerificationMemory: Aucune vérification trouvée');
return null;
}
/**
* Méthode de secours pour la recherche basée sur le texte
* AMÉLIORÉ: Recherche textuelle plus flexible
*
* @param text Texte à rechercher
* @param sessionId ID de session
* @returns Résultat de recherche ou null
*/
fallbackToTextSearch(text, sessionId) {
this.emit('error', 'VerificationMemory: Utilisation de la recherche textuelle');
// Obtenir les entrées pour cette session
const sessionEntries = this.getSessionEntriesArray(sessionId);
// Recherche exacte par texte
const exactMatch = sessionEntries.find(entry => entry.text === text);
if (exactMatch) {
this.emit('error', 'VerificationMemory: Correspondance exacte trouvée');
return {
id: exactMatch.id,
status: exactMatch.status,
confidence: exactMatch.confidence,
sources: exactMatch.sources,
timestamp: exactMatch.timestamp,
similarity: 1.0,
text: exactMatch.text
};
}
// AMÉLIORÉ: Normaliser les textes pour une meilleure correspondance
const normalizedText = this.normalizeText(text);
// AMÉLIORÉ: Recherche par inclusion de mots-clés significatifs
const keywordsMatches = sessionEntries.map(entry => {
const normalizedEntry = this.normalizeText(entry.text);
// Extraire les mots significatifs (plus de 3 caractères)
const textWords = new Set(normalizedText.split(/\s+/).filter(w => w.length > 3));
const entryWords = new Set(normalizedEntry.split(/\s+/).filter(w => w.length > 3));
// Compter les mots en commun et les mots uniques
const commonWords = Array.from(textWords).filter(word => entryWords.has(word)).length;
const totalUniqueWords = new Set([...textWords, ...entryWords]).size;
// Calculer similitude Jaccard (intersection/union)
const similarity = totalUniqueWords > 0 ? commonWords / totalUniqueWords : 0;
// NOUVEAU: Bonus pour séquences communes
let sequenceBonus = 0;
// Chercher des séquences de 3+ mots consécutifs identiques
const textChunks = normalizedText.split(/[.!?;]/).filter(s => s.trim().length > 0);
const entryChunks = normalizedEntry.split(/[.!?;]/).filter(s => s.trim().length > 0);
for (const chunk of textChunks) {
if (entryChunks.some(ec => ec.includes(chunk) && chunk.split(/\s+/).length >= 3)) {
sequenceBonus = 0.2; // Bonus pour séquences communes significatives
break;
}
}
return {
entry,
similarity: Math.min(similarity + sequenceBonus, 0.95) // Plafond à 0.95
};
});
// Trier par similarité décroissante
keywordsMatches.sort((a, b) => b.similarity - a.similarity);
// AMÉLIORÉ: Seuil de similarité pour les correspondances textuelles
const textSimilarityThreshold = config_1.VerificationConfig.SIMILARITY.TEXT_MATCH * 0.9; // Seuil légèrement réduit
if (keywordsMatches.length > 0 && keywordsMatches[0].similarity >= textSimilarityThreshold) {
const bestMatch = keywordsMatches[0].entry;
this.emit('error', `VerificationMemory: Correspondance textuelle trouvée avec similarité ${keywordsMatches[0].similarity.toFixed(3)}`);
return {
id: bestMatch.id,
status: bestMatch.status,
confidence: bestMatch.confidence,
sources: bestMatch.sources,
timestamp: bestMatch.timestamp,
similarity: keywordsMatches[0].similarity,
text: bestMatch.text
};
}
this.emit('error', 'VerificationMemory: Aucune correspondance textuelle trouvée');
return null;
}
/**
* NOUVEAU: Normalise un texte pour la recherche textuelle
*
* @param text Texte à normaliser
* @returns Texte normalisé
*/
normalizeText(text) {
// Préserver les expressions mathématiques en les remplaçant par des tokens
const mathExpressions = [];
const tokenizedText = text.replace(/(\d+(?:[.,]\d+)?(?:\s*[\+\-\*\/\^]\s*\d+(?:[.,]\d+)?)+)/g, (match) => {
mathExpressions.push(match);
return `__MATH_${mathExpressions.length - 1}__`;
});
const normalized = tokenizedText
.toLowerCase()
.replace(/[^\w\s]|_/g, ' ') // Remplacer ponctuation et underscore par espaces
.replace(/\s+/g, ' ') // Normaliser les espaces
.replace(/\d+/g, 'NUM') // Normaliser les nombres
.trim();
// Réintégrer les expressions mathématiques
return mathExpressions.reduce((text, expr, idx) => {
return text.replace(`__math_${idx}__`, expr);
}, normalized);
}
/**
* Récupère les entrées pour une session donnée
*
* @param sessionId ID de session
* @returns Tableau des entrées pour cette session
*/
getSessionEntriesArray(sessionId) {
this.emit('error', `VerificationMemory: Récupération des entrées pour la session ${sessionId}`);
const sessionIds = this.sessionIndex.get(sessionId);
if (!sessionIds) {
this.emit('error', `VerificationMemory: Aucune entrée pour cette session`);
return [];
}
const entries = [];
for (const id of sessionIds) {
const entry = this.verifications.get(id);
if (entry) {
entries.push(entry);
}
}
this.emit('error', `VerificationMemory: ${entries.length} entrées récupérées`);
return entries;
}
/**
* Récupère toutes les vérifications pour une session donnée
* avec pagination et filtrage
*
* @param sessionId Identifiant de la session
* @param offset Position de départ (pour pagination)
* @param limit Nombre maximum de résultats
* @param statusFilter Filtre sur le statut (optionnel)
* @returns Tableau des vérifications pour cette session
*/
getSessionVerifications(sessionId = config_1.SystemConfig.DEFAULT_SESSION_ID, offset = 0, limit = 100, statusFilter) {
// Obtenir les entrées pour cette session
let sessionEntries = this.getSessionEntriesArray(sessionId);
// Appliquer le filtre de statut si fourni
if (statusFilter) {
sessionEntries = sessionEntries.filter(entry => entry.status === statusFilter);
}
// Trier par date (plus récent d'abord)
sessionEntries.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
// Appliquer pagination
return sessionEntries
.slice(offset, offset + limit)
.map(entry => ({
id: entry.id,
text: entry.text,
status: entry.status,
confidence: entry.confidence,
sources: entry.sources,
timestamp: entry.timestamp
}));
}
/**
* Recherche des vérifications par similarité vectorielle
*
* @param text Texte de référence
* @param sessionId ID de session
* @param limit Nombre maximum de résultats
* @param minSimilarity Seuil minimal de similarité
* @returns Liste de résultats triés par similarité
*/
async searchSimilarVerifications(text, sessionId = config_1.SystemConfig.DEFAULT_SESSION_ID, limit = 5, minSimilarity = config_1.VerificationConfig.SIMILARITY.MEDIUM_SIMILARITY) {
await this.ensureInitialized();
if (!this.similarityEngine) {
return [];
}
try {
const sessionEntries = this.getSessionEntriesArray(sessionId);
if (sessionEntries.length === 0) {
return [];
}
const candidateTexts = sessionEntries.map(entry => entry.text);
const results = await this.similarityEngine.findSimilarTexts(text, candidateTexts, candidateTexts.length, minSimilarity);
return results
.map(result => {
const entry = sessionEntries.find(item => item.text === result.text);
if (!entry)
return null;
return {
id: entry.id,
status: entry.status,
confidence: entry.confidence,
sources: entry.sources,
timestamp: entry.timestamp,
similarity: result.score,
text: entry.text
};
})
.filter((item) => item !== null)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, limit);
}
catch (error) {
this.emit('error', 'Erreur lors de la recherche de vérifications similaires via SimilarityEngine:', error);
return [];
}
}
/**
* Nettoie les vérifications d'une session spécifique
*
* @param sessionId Identifiant de la session à nettoyer
*/
clearSession(sessionId) {
const sessionIds = this.sessionIndex.get(sessionId);
if (!sessionIds)
return;
// Supprimer chaque entrée
for (const id of sessionIds) {
this.verifications.delete(id);
}
// Supprimer l'index de session
this.sessionIndex.delete(sessionId);
this.emit('error', `VerificationMemory: Session ${sessionId} nettoyée`);
this.requestPersist();
}
/**
* Nettoie les entrées expirées
*/
cleanExpiredEntries() {
const now = new Date();
const expiredIds = new Set();
// Identifier les entrées expirées
for (const [id, entry] of this.verifications.entries()) {
if (entry.expiresAt < now) {
expiredIds.add(id);
}
}
if (expiredIds.size === 0)
return;
// Supprimer les entrées expirées
for (const id of expiredIds) {
const entry = this.verifications.get(id);
if (entry) {
// Mettre à jour l'index de session
const sessionIds = this.sessionIndex.get(entry.sessionId);
if (sessionIds) {
sessionIds.delete(id);
// Si la session est vide, supprimer son index
if (sessionIds.size === 0) {
this.sessionIndex.delete(entry.sessionId);
}
}
// Supprimer l'entrée
this.verifications.delete(id);
}
}
this.emit('error', `VerificationMemory: ${expiredIds.size} entrées expirées supprimées`);
this.requestPersist();
}
/**
* NOUVEAU: Nettoie le cache de similarité
*/
cleanSimilarityCache() {
const cacheSize = Array.from(this.similarityCache.values())
.reduce((total, map) => total + map.size, 0);
if (cacheSize === 0)
return;
// Vider le cache
this.similarityCache.clear();
this.emit('error', `VerificationMemory: Cache de similarité nettoyé (${cacheSize} entrées)`);
}
/**
* Nettoie toutes les vérifications (utilisé pour les tests)
*/
clearAll() {
this.verifications.clear();
this.sessionIndex.clear();
this.similarityCache.clear();
this.emit('error', 'VerificationMemory: Mémoire de vérification entièrement nettoyée');
this.requestPersist();
}
/**
* Obtient des statistiques sur la mémoire de vérification
*/
getStats() {
// Initialiser le compteur avec tous les statuts possibles
const entriesByStatus = {};
// Définir tous les types de statut possibles avec une valeur initiale de 0
const allStatuses = [
'verified', 'partially_verified', 'unverified', 'contradicted',
'inconclusive', 'absence_of_information', 'uncertain', 'contradictory'
];
// Initialiser tous les compteurs à 0
allStatuses.forEach(status => {
entriesByStatus[status] = 0;
});
// Compter les entrées par statut
for (const entry of this.verifications.values()) {
entriesByStatus[entry.status]++;
}
// Calculer la taille du cache
const cacheSize = Array.from(this.similarityCache.values())
.reduce((total, map) => total + map.size, 0);
return {
totalEntries: this.verifications.size,
sessionCount: this.sessionIndex.size,
cacheSize,
entriesByStatus
};
}
}
exports.VerificationMemory = VerificationMemory;
//# sourceMappingURL=verification-memory.js.map