UNPKG

smart-thinking-mcp

Version:

Un serveur MCP avancé pour le raisonnement multi-dimensionnel, adaptatif et collaboratif

1,071 lines 67.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ThoughtGraph = void 0; const events_1 = require("events"); const openrouter_client_1 = require("./utils/openrouter-client"); // Import LLM utility /** * Classe qui gère le graphe de pensées et ses opérations */ class ThoughtGraph { nodes = new Map(); hyperlinks = new Map(); sessionId; embeddingService; qualityEvaluator; eventEmitter; constructor(sessionId, embeddingService, qualityEvaluator) { this.sessionId = sessionId || this.generateUniqueId(); this.embeddingService = embeddingService; this.qualityEvaluator = qualityEvaluator; this.eventEmitter = new events_1.EventEmitter(); // Configurer les écouteurs d'événements this.setupEventListeners(); } /** * Configure les écouteurs d'événements pour la vérification continue */ setupEventListeners() { // Configurer ici des écouteurs si nécessaire } /** * Vérifie les calculs dans une pensée et les annote si nécessaire * * @param thoughtId L'identifiant de la pensée à vérifier * @param content Le contenu de la pensée */ async checkForCalculationsAndVerify(thoughtId, content) { if (!this.qualityEvaluator) return; // Détecter si la pensée contient des calculs avec des expressions régulières simples const hasSimpleCalculations = /\d+\s*[\+\-\*\/]\s*\d+\s*=/.test(content); const hasComplexCalculations = /calcul\s*(?:complexe|avancé)?\s*:?\s*([^=]+)=\s*\d+/.test(content); if (hasSimpleCalculations || hasComplexCalculations) { console.error(`Smart-Thinking: Détection en temps réel de calculs dans la pensée ${thoughtId}, vérification...`); try { // Vérifier les calculs de manière asynchrone const verifiedCalculations = await this.qualityEvaluator.detectAndVerifyCalculations(content); if (verifiedCalculations.length > 0) { // Mettre à jour le contenu de la pensée avec les annotations const updatedContent = this.qualityEvaluator.annotateThoughtWithVerifications(content, verifiedCalculations); // Mettre à jour la pensée sans déclencher à nouveau les vérifications const thought = this.nodes.get(thoughtId); if (thought) { thought.content = updatedContent; thought.metadata.calculationsVerified = true; thought.metadata.lastUpdated = new Date(); thought.metadata.verificationTimestamp = new Date(); } // Émettre un événement pour notifier que des calculs ont été vérifiés this.eventEmitter.emit('calculations-verified', { thoughtId, verifiedCalculations, updatedContent }); // Journaliser le résultat de la vérification console.error(`Smart-Thinking: ${verifiedCalculations.length} calcul(s) vérifié(s) dans la pensée ${thoughtId}`); } } catch (error) { console.error(`Smart-Thinking: Erreur lors de la vérification des calculs:`, error); } } } /** * Permet d'enregistrer un écouteur d'événement externe * * @param event Le nom de l'événement * @param listener La fonction de rappel à exécuter */ on(event, listener) { this.eventEmitter.on(event, listener); } /** * Génère un identifiant unique */ generateUniqueId() { return Date.now().toString(36) + Math.random().toString(36).substring(2); } /** * Ajoute une nouvelle pensée au graphe * * @param content Le contenu de la pensée * @param type Le type de pensée * @param connections Les connexions à d'autres pensées * @returns L'identifiant de la pensée ajoutée */ addThought(content, type = 'regular', connections = []) { const id = this.generateUniqueId(); const node = { id, content, type, timestamp: new Date(), connections: [...connections], // Ensure a copy is made metrics: { confidence: 0.5, // Valeur par défaut relevance: 0.5, // Valeur par défaut quality: 0.5 // Valeur par défaut }, metadata: { sessionId: this.sessionId } }; this.nodes.set(id, node); // Établir les connexions bidirectionnelles this.establishConnections(id, connections); // Émettre un événement pour notifier de l'ajout d'une pensée this.eventEmitter.emit('thought-added', id, node); // Vérifier automatiquement les calculs si nécessaire (async, but don't wait for it) this.checkForCalculationsAndVerify(id, content).catch(err => { console.error("Error during background calculation check:", err); }); // Calculate metrics asynchronously after adding the thought (don't block return) this.updateMetricsForThought(id).catch(err => { console.error(`Error during background metric calculation for ${id}:`, err); }); return id; } /** * Met à jour les métriques pour une pensée spécifique (maintenant asynchrone) * @param thoughtId L'ID de la pensée */ async updateMetricsForThought(thoughtId) { if (!this.qualityEvaluator) { console.warn("QualityEvaluator not available for metric update."); return; } const thought = this.getThought(thoughtId); if (!thought) return; try { const metrics = await this.qualityEvaluator.evaluate(thoughtId, this); this.updateThoughtMetrics(thoughtId, metrics); this.eventEmitter.emit('metrics-updated', thoughtId, metrics); } catch (error) { console.error(`Failed to update metrics for thought ${thoughtId}:`, error); } } /** * Établit des connexions bidirectionnelles entre les pensées * * @param sourceId L'identifiant de la pensée source * @param connections Les connexions à établir */ establishConnections(sourceId, connections) { for (const connection of connections) { const targetNode = this.nodes.get(connection.targetId); if (targetNode) { // Avoid adding duplicate reciprocal connections if one already exists const existingReciprocal = targetNode.connections.find(conn => conn.targetId === sourceId); if (!existingReciprocal) { targetNode.connections.push({ targetId: sourceId, type: this.getReciprocalConnectionType(connection.type), strength: connection.strength, description: connection.description, // Transférer les attributs si présents attributes: connection.attributes, inferred: connection.inferred, inferenceConfidence: connection.inferenceConfidence, bidirectional: connection.bidirectional }); } } } } /** * Détermine le type de connexion réciproque * * @param type Le type de connexion original * @returns Le type de connexion réciproque */ getReciprocalConnectionType(type) { // Mapping for reciprocal types const reciprocalMap = { supports: 'supports', contradicts: 'contradicts', refines: 'derives', // If A refines B, B derives from A derives: 'refines', // If A derives from B, B refines A branches: 'branches', associates: 'associates', exemplifies: 'generalizes', // If A exemplifies B, B generalizes A generalizes: 'exemplifies', // If A generalizes B, B exemplifies A compares: 'compares', contrasts: 'contrasts', questions: 'questions', // Questioning can be reciprocal extends: 'extended-by', analyzes: 'analyzed-by', synthesizes: 'component-of', // If A synthesizes B, B is a component of A applies: 'applied-by', evaluates: 'evaluated-by', cites: 'cited-by', 'extended-by': 'extends', 'analyzed-by': 'analyzes', 'component-of': 'synthesizes', 'applied-by': 'applies', 'evaluated-by': 'evaluates', 'cited-by': 'cites' }; return reciprocalMap[type] || 'associates'; // Default to 'associates' } /** * Récupère une pensée par son identifiant * * @param id L'identifiant de la pensée * @returns La pensée ou undefined si non trouvée */ getThought(id) { return this.nodes.get(id); } /** * Met à jour les métriques d'une pensée * * @param id L'identifiant de la pensée * @param metrics Les nouvelles métriques * @returns true si la mise à jour a réussi, false sinon */ updateThoughtMetrics(id, metrics) { const thought = this.nodes.get(id); if (!thought) return false; // Ensure metrics object exists if (!thought.metrics) { thought.metrics = { confidence: 0.5, relevance: 0.5, quality: 0.5 }; } thought.metrics = { ...thought.metrics, ...metrics }; return true; } /** * Récupère toutes les pensées * * @param sessionId L'identifiant de session facultatif pour filtrer les pensées * @returns Un tableau de toutes les pensées (filtrées par session si sessionId est fourni) */ getAllThoughts(sessionId) { const allNodes = Array.from(this.nodes.values()); if (!sessionId) { // Maybe return only thoughts from the graph's default session? Or all? // For now, let's return all if no session specified, but log a warning. // console.warn("getAllThoughts called without sessionId, returning all thoughts."); return allNodes; } return allNodes.filter(node => node.metadata?.sessionId === sessionId); } /** * Récupère les pensées les plus récentes * * @param limit Le nombre maximum de pensées à récupérer * @param sessionId L'identifiant de session facultatif pour filtrer les pensées * @returns Un tableau des pensées les plus récentes (filtrées par session) */ getRecentThoughts(limit = 5, sessionId) { const thoughts = this.getAllThoughts(sessionId); // Use the session-filtered list return thoughts .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) .slice(0, limit); } /** * Récupère les pensées connectées à une pensée spécifique * * @param thoughtId L'identifiant de la pensée * @returns Un tableau des pensées connectées */ getConnectedThoughts(thoughtId) { const thought = this.nodes.get(thoughtId); if (!thought) return []; return thought.connections .map(conn => this.nodes.get(conn.targetId)) .filter((node) => node !== undefined); } /** * Récupère les pensées les plus pertinentes pour un contexte donné * * @param context Le contexte pour lequel chercher des pensées pertinentes * @param limit Le nombre maximum de pensées à récupérer * @param sessionId L'identifiant de session facultatif pour filtrer les pensées * @returns Un tableau des pensées les plus pertinentes (filtrées par session) */ async getRelevantThoughts(context, limit = 5, sessionId) { const allThoughts = this.getAllThoughts(sessionId); // Use the session-filtered list // Si pas de pensées ou pas de service d'embeddings, utiliser l'algorithme de base if (allThoughts.length === 0 || !this.embeddingService) { console.warn("Embedding service not available or no thoughts in graph. Falling back to keyword relevance."); return this.getRelevantThoughtsWithKeywords(context, limit); } try { // Utiliser le service d'embeddings pour trouver les pensées similaires const thoughtTexts = allThoughts.map(thought => thought.content); const similarResults = await this.embeddingService.findSimilarTexts(context, thoughtTexts, limit); // Convertir les résultats en pensées return similarResults.map(result => { const matchingThought = allThoughts.find(thought => thought.content === result.text); if (matchingThought) { // Stocker le score de similarité dans les métadonnées pour référence future matchingThought.metadata.similarityScore = result.score; } return matchingThought; }).filter(thought => thought !== undefined); } catch (error) { console.error('Erreur lors de la recherche de pensées pertinentes avec embeddings:', error); // En cas d'erreur, revenir à l'algorithme basé sur les mots-clés return this.getRelevantThoughtsWithKeywords(context, limit); } } /** * Implémentation de secours basée sur les mots-clés * * @param context Le contexte pour lequel chercher des pensées pertinentes * @param limit Le nombre maximum de pensées à récupérer * @returns Un tableau des pensées les plus pertinentes */ getRelevantThoughtsWithKeywords(context, limit = 5) { // Une implémentation simple basée sur la correspondance de mots-clés const contextWords = context.toLowerCase().split(/\W+/).filter(word => word.length > 3); if (contextWords.length === 0) return []; // Avoid division by zero if context is empty return Array.from(this.nodes.values()) .map(thought => { const thoughtWords = thought.content.toLowerCase().split(/\W+/).filter(word => word.length > 3); // Calculer un score simple basé sur le nombre de mots partagés const matchingWords = contextWords.filter(word => thoughtWords.includes(word)); const score = matchingWords.length / contextWords.length; // Normalize by context length return { thought, score }; }) .filter(item => item.score > 0) // Only keep thoughts with some relevance .sort((a, b) => b.score - a.score) .slice(0, limit) .map(item => item.thought); } /** * Crée un hyperlien entre plusieurs pensées * * @param nodeIds Les identifiants des pensées à connecter * @param type Le type de connexion * @param label Une étiquette descriptive facultative * @param attributes Attributs sémantiques facultatifs * @param strength La force de la connexion (0 à 1) * @returns L'identifiant de l'hyperlien créé */ createHyperlink(nodeIds, type, label, attributes, strength = 0.5) { // Vérifier que les nœuds existent if (nodeIds.some(id => !this.nodes.has(id))) { console.error('Certains nœuds spécifiés n\'existent pas'); return ''; } // Générer un ID unique pour l'hyperlien const id = `hl-${this.generateUniqueId()}`; // Prefix for clarity // Créer l'hyperlien const hyperlink = { id, nodeIds, type, label, attributes, strength, inferred: false, confidence: 1.0, // Non inféré, donc confiance maximale metadata: { createdAt: new Date(), sessionId: this.sessionId } }; // Ajouter l'hyperlien à la collection this.hyperlinks.set(id, hyperlink); return id; } /** * Récupère un hyperlien par son identifiant * * @param id L'identifiant de l'hyperlien * @returns L'hyperlien ou undefined si non trouvé */ getHyperlink(id) { return this.hyperlinks.get(id); } /** * Récupère tous les hyperliens * * @param sessionId L'identifiant de session facultatif pour filtrer les hyperliens * @returns Un tableau de tous les hyperliens (filtrés par session) */ getAllHyperlinks(sessionId) { const allLinks = Array.from(this.hyperlinks.values()); if (!sessionId) { // console.warn("getAllHyperlinks called without sessionId, returning all hyperlinks."); return allLinks; } return allLinks.filter(link => link.metadata?.sessionId === sessionId); } /** * Récupère les hyperliens impliquant une pensée spécifique * * @param thoughtId L'identifiant de la pensée * @param sessionId L'identifiant de session facultatif pour filtrer les hyperliens * @returns Un tableau des hyperliens impliquant cette pensée (filtrés par session) */ getHyperlinksForThought(thoughtId, sessionId) { const thought = this.getThought(thoughtId); // Ensure the thought itself belongs to the requested session if provided if (!thought || (sessionId && thought.metadata?.sessionId !== sessionId)) { return []; } const allLinks = this.getAllHyperlinks(sessionId); // Get session-filtered links return allLinks.filter(hyperlink => hyperlink.nodeIds.includes(thoughtId)); } /** * Infère des relations entre pensées basées sur l'analyse de contenu et de contexte * * @param confidenceThreshold Seuil de confiance minimum pour les relations inférées (0 à 1) * @returns Le nombre de nouvelles relations inférées */ async inferRelations(confidenceThreshold = 0.7) { if (!this.embeddingService) { console.error('Service d\'embeddings non disponible pour l\'inférence de relations'); return 0; } const thoughts = this.getAllThoughts(); let newRelationsCount = 0; // Inférence basée sur la similarité sémantique const similarityCount = await this.inferRelationsBySimilarity(thoughts, confidenceThreshold); // Inférence basée sur la transitivité des relations const transitivityCount = this.inferRelationsByTransitivity(confidenceThreshold); // Inférence basée sur des patterns dans le graphe const patternsCount = this.inferRelationsByPatterns(confidenceThreshold); newRelationsCount = similarityCount + transitivityCount + patternsCount; return newRelationsCount; } /** * Infère des relations basées sur la similarité sémantique * * @param thoughts Les pensées à analyser * @param confidenceThreshold Seuil de confiance minimum * @returns Le nombre de nouvelles relations inférées */ async inferRelationsBySimilarity(thoughts, confidenceThreshold) { if (thoughts.length < 2 || !this.embeddingService) return 0; let newRelationsCount = 0; const thoughtTexts = thoughts.map(t => t.content); const thoughtIds = thoughts.map(t => t.id); // Get embeddings for all thoughts (potentially batch this if service supports it) let embeddings = []; try { embeddings = await this.embeddingService.getEmbeddings(thoughtTexts); if (embeddings.length !== thoughts.length) { console.error("Mismatch between number of thoughts and embeddings received."); return 0; } } catch (error) { console.error("Failed to get embeddings for similarity inference:", error); return 0; } // Calculer les similarités entre toutes les paires de pensées for (let i = 0; i < thoughts.length; i++) { const sourceThought = thoughts[i]; const sourceEmbedding = embeddings[i]; for (let j = i + 1; j < thoughts.length; j++) { const targetThought = thoughts[j]; const targetEmbedding = embeddings[j]; const similarityScore = this.embeddingService.calculateCosineSimilarity(sourceEmbedding, targetEmbedding); if (similarityScore >= confidenceThreshold) { // Éviter les doublons const hasConnection = sourceThought.connections.some(conn => conn.targetId === targetThought.id); const hasReciprocalConnection = targetThought.connections.some(conn => conn.targetId === sourceThought.id); if (!hasConnection && !hasReciprocalConnection) { // Déterminer le type de connexion en fonction du contexte const connectionType = this.inferConnectionType(sourceThought, targetThought); // Ajouter la nouvelle connexion this.addInferredConnection(sourceThought.id, targetThought.id, connectionType, similarityScore // Use the calculated similarity as confidence ); newRelationsCount++; } } } } return newRelationsCount; } /** * Infère des relations basées sur la transitivité * * @param confidenceThreshold Seuil de confiance minimum * @returns Le nombre de nouvelles relations inférées */ inferRelationsByTransitivity(confidenceThreshold) { let newRelationsCount = 0; const thoughts = this.getAllThoughts(); // Règles de transitivité pour certains types de connexions const transitivityRules = { // Si A supporte B et B supporte C, alors A supporte C (transitivité affaiblie) 'supports': [ { firstType: 'supports', secondType: 'supports', resultType: 'supports', confidenceMultiplier: 0.8 } ], // Si A contredit B et B supporte C, alors A contredit C (transitivité inversée) 'contradicts': [ { firstType: 'contradicts', secondType: 'supports', resultType: 'contradicts', confidenceMultiplier: 0.7 } ], // Si A dérive de B et B dérive de C, alors A dérive de C 'derives': [ { firstType: 'derives', secondType: 'derives', resultType: 'derives', confidenceMultiplier: 0.9 } ] // Autres règles... }; // Appliquer les règles de transitivité for (const thought of thoughts) { for (const conn1 of thought.connections) { const secondThought = this.getThought(conn1.targetId); if (!secondThought) continue; const rules = transitivityRules[conn1.type] || []; for (const rule of rules) { if (conn1.type !== rule.firstType) continue; for (const conn2 of secondThought.connections) { if (conn2.type !== rule.secondType) continue; const thirdThought = this.getThought(conn2.targetId); if (!thirdThought || thirdThought.id === thought.id) continue; // Éviter les cycles et les doublons const hasConnection = thought.connections.some(c => c.targetId === thirdThought.id); const hasReciprocalConnection = thirdThought.connections.some(c => c.targetId === thought.id); if (!hasConnection && !hasReciprocalConnection) { // Calculer la confiance basée sur les connexions existantes const confidence = conn1.strength * conn2.strength * rule.confidenceMultiplier; if (confidence >= confidenceThreshold) { this.addInferredConnection(thought.id, thirdThought.id, rule.resultType, confidence); newRelationsCount++; } } } } } } return newRelationsCount; } /** * Infère des relations basées sur des patterns dans le graphe * * @param confidenceThreshold Seuil de confiance minimum * @returns Le nombre de nouvelles relations inférées */ inferRelationsByPatterns(confidenceThreshold) { let newRelationsCount = 0; // Détecter des clusters de pensées fortement connectées const clusters = this.detectClusters(); // Pour chaque cluster, créer des hyperliens entre les membres for (const cluster of clusters) { if (cluster.nodeIds.length >= 3) { // Créer un hyperlien pour le groupe si la confiance est suffisante if (cluster.cohesion >= confidenceThreshold) { // Check if hyperlink already exists for this cluster const existingHyperlink = Array.from(this.hyperlinks.values()).find(hl => hl.nodeIds.length === cluster.nodeIds.length && hl.nodeIds.every(id => cluster.nodeIds.includes(id))); if (!existingHyperlink) { this.createHyperlink(cluster.nodeIds, 'associates', // Type par défaut, pourrait être affiné `Cluster: ${cluster.label || 'Sans nom'}`, { nature: 'associative', scope: 'broad' }, cluster.cohesion); newRelationsCount++; // Count hyperlink creation } } // Également, inférer des relations individuelles entre les membres du cluster for (let i = 0; i < cluster.nodeIds.length; i++) { for (let j = i + 1; j < cluster.nodeIds.length; j++) { const thought1 = this.getThought(cluster.nodeIds[i]); const thought2 = this.getThought(cluster.nodeIds[j]); if (!thought1 || !thought2) continue; // Vérifier si une connexion existe déjà dans les deux sens const hasConnection = thought1.connections.some(conn => conn.targetId === thought2.id); const hasReciprocalConnection = thought2.connections.some(conn => conn.targetId === thought1.id); if (!hasConnection && !hasReciprocalConnection) { // La confiance est basée sur la cohésion du cluster const confidence = cluster.cohesion * 0.9; // Slightly lower confidence than cluster itself if (confidence >= confidenceThreshold) { this.addInferredConnection(thought1.id, thought2.id, 'associates', // Assume association within cluster confidence); newRelationsCount++; } } } } } } return newRelationsCount; } /** * Détecte des clusters (groupes) de pensées fortement connectées * * @returns Un tableau de clusters détectés */ detectClusters() { const clusters = []; // Algorithme simple de clustering basé sur la connectivité const thoughts = this.getAllThoughts(); const visited = new Set(); for (const thought of thoughts) { if (visited.has(thought.id)) continue; // Parcourir le graphe à partir de cette pensée const cluster = this.exploreCommunity(thought.id, visited); // Seulement considérer les clusters avec au moins 2 nœuds if (cluster.nodeIds.length >= 2) { clusters.push(cluster); } } return clusters; } /** * Explore une communauté connectée à partir d'un nœud de départ * * @param startNodeId ID du nœud de départ * @param visited Ensemble des nœuds déjà visités * @returns Un cluster de nœuds connectés */ exploreCommunity(startNodeId, visited) { const communityNodes = []; const queue = [startNodeId]; const connectivityScores = []; const internalEdges = new Set(); // Track internal edges to calculate cohesion while (queue.length > 0) { const currentId = queue.shift(); if (visited.has(currentId)) continue; visited.add(currentId); communityNodes.push(currentId); const thought = this.getThought(currentId); if (!thought) continue; // Évaluer les connexions for (const conn of thought.connections) { // Check if the target is part of the potential community (already visited or in queue) const targetInCommunity = visited.has(conn.targetId) || queue.includes(conn.targetId); if (!visited.has(conn.targetId) && conn.strength > 0.5) { // Threshold for exploring queue.push(conn.targetId); } // If the connection is internal to the community being explored if (targetInCommunity) { const edgeId = [currentId, conn.targetId].sort().join('-'); // Unique ID for undirected edge if (!internalEdges.has(edgeId)) { connectivityScores.push(conn.strength); internalEdges.add(edgeId); } } } } // Calculer la cohésion du cluster basée sur la force moyenne des connexions internes const avgConnectivity = connectivityScores.length > 0 ? connectivityScores.reduce((sum, score) => sum + score, 0) / connectivityScores.length : 0; // Basic labeling (e.g., based on most frequent keywords in the cluster) - can be enhanced let label = undefined; if (communityNodes.length > 0) { const clusterContent = communityNodes.map(id => this.getThought(id)?.content || "").join(" "); // Simple keyword extraction for label (replace with more sophisticated method if needed) const words = clusterContent.toLowerCase().split(/\W+/).filter(w => w.length > 4); const wordCounts = {}; words.forEach(w => { wordCounts[w] = (wordCounts[w] || 0) + 1; }); const sortedWords = Object.entries(wordCounts).sort((a, b) => b[1] - a[1]); if (sortedWords.length > 0) { label = sortedWords[0][0]; } } return { nodeIds: communityNodes, label: label, cohesion: avgConnectivity }; } /** * Infère le type de connexion approprié entre deux pensées * * @param sourceThought La pensée source * @param targetThought La pensée cible * @returns Le type de connexion inféré */ inferConnectionType(sourceThought, targetThought) { // Analyse simple basée sur le contenu et le type des pensées const sourceContent = sourceThought.content.toLowerCase(); const targetContent = targetThought.content.toLowerCase(); const combinedContent = sourceContent + " " + targetContent; // Combine for easier marker checking // --- Enrichissement des marqueurs --- const contradictionMarkers = [ 'cependant', 'mais', 'toutefois', 'contrairement', 'oppose', 'inversement', 'contredit', 'différent', 'désaccord', 'conteste', 'au contraire', 'réfute', 'pourtant', 'malgré', 'bien que', 'alors que', 'pas d\'accord', 'faux', 'incorrect' ]; const supportMarkers = [ 'confirme', 'soutient', 'renforce', 'valide', 'appuie', 'corrobore', 'accord', 'similaire', 'également', 'aussi', 'de même', 'en effet', 'prouve', 'démontre', 'illustre', 'comme', 'ainsi', 'par exemple' // Certains peuvent aussi être 'exemplifies' ]; const derivationMarkers = [ 'donc', 'par conséquent', 'ainsi', 'résulte', 'implique', 'entraîne', 'si...alors', 'parce que', 'car', 'puisque', 'étant donné', 'conclusion', 'synthèse' ]; const exemplificationMarkers = [ 'par exemple', 'comme', 'illustre', 'notamment', 'tel que' ]; const questionMarkers = [ 'pourquoi', 'comment', 'quel', 'quelle', 'qui', 'où', 'quand', '?', 'remet en question' ]; const refinementMarkers = [ 'précise', 'détaille', 'améliore', 'corrige', 'en d\'autres termes', 'spécifiquement' ]; // --- Logique d'inférence améliorée --- // Priorité aux contradictions if (contradictionMarkers.some((marker) => combinedContent.includes(marker))) { // Explicitly typed param return 'contradicts'; } // Ensuite, les dérivations/applications (logique forte) if (derivationMarkers.some((marker) => combinedContent.includes(marker))) { // Explicitly typed param // Affiner si possible (ex: si source est conclusion/meta et cible est regular -> synthesizes) if (sourceThought.type === 'conclusion' && targetThought.type !== 'conclusion') return 'synthesizes'; if (sourceThought.type === 'meta') return 'analyzes'; // ou evaluates // Si contient "si...alors", pourrait être 'applies' ou 'derives' if (combinedContent.includes('si') && combinedContent.includes('alors')) return 'applies'; return 'derives'; // Ou 'supports' selon le contexte exact } // Ensuite, les exemples if (exemplificationMarkers.some((marker) => combinedContent.includes(marker))) { // Explicitly typed param // Vérifier la direction (source exemplifie cible ou l'inverse) if (exemplificationMarkers.some((marker) => sourceContent.includes(marker))) return 'exemplifies'; // Check marker in source if (exemplificationMarkers.some((marker) => targetContent.includes(marker))) return 'generalizes'; // Check marker in target return 'exemplifies'; // Par défaut } // Ensuite, les questions if (questionMarkers.some((marker) => combinedContent.includes(marker))) { // Explicitly typed param return 'questions'; } // Ensuite, les raffinements/révisions if (refinementMarkers.some((marker) => combinedContent.includes(marker)) || sourceThought.type === 'revision') { // Explicitly typed param return 'refines'; } // Ensuite, le support (logique plus faible ou association positive) if (supportMarkers.some((marker) => combinedContent.includes(marker))) { // Explicitly typed param return 'supports'; } // Inférence basée sur les types de pensées if (sourceThought.type === 'conclusion' && targetThought.type !== 'conclusion') { return 'synthesizes'; } if (sourceThought.type === 'hypothesis' && targetThought.type === 'regular') { return 'generalizes'; } if (sourceThought.type === 'regular' && targetThought.type === 'hypothesis') { return 'exemplifies'; } if (sourceThought.type === 'meta') { return 'analyzes'; } // Removed redundant check for 'revision' here as it's handled earlier // Par défaut, type d'association générique return 'associates'; } /** * Ajoute une connexion inférée entre deux pensées * * @param sourceId ID de la pensée source * @param targetId ID de la pensée cible * @param type Type de connexion * @param confidence Niveau de confiance dans l'inférence * @returns true si l'ajout a réussi, false sinon */ addInferredConnection(sourceId, targetId, type, confidence) { const sourceThought = this.getThought(sourceId); const targetThought = this.getThought(targetId); if (!sourceThought || !targetThought) { return false; } // Créer la connexion avec marquage d'inférence const connection = { targetId, type, strength: confidence, // Use confidence as strength for inferred connections inferred: true, inferenceConfidence: confidence, attributes: { certainty: this.mapConfidenceToCertainty(confidence) } }; // Ajouter la connexion à la pensée source sourceThought.connections.push(connection); // Créer une connexion réciproque si nécessaire if (this.shouldCreateReciprocalConnection(type)) { const reciprocalType = this.getReciprocalConnectionType(type); targetThought.connections.push({ targetId: sourceId, type: reciprocalType, strength: confidence, inferred: true, inferenceConfidence: confidence, attributes: { certainty: this.mapConfidenceToCertainty(confidence) } }); } return true; } /** * Détermine si une connexion réciproque doit être créée * * @param type Le type de connexion * @returns true si une connexion réciproque doit être créée */ shouldCreateReciprocalConnection(type) { // Certains types de connexion sont intrinsèquement bidirectionnels const bidirectionalTypes = [ 'associates', 'compares', 'contrasts', 'supports', 'contradicts' // Added supports/contradicts as often reciprocal ]; return bidirectionalTypes.includes(type); } /** * Convertit un niveau de confiance en niveau de certitude * * @param confidence Le niveau de confiance (0 à 1) * @returns Le niveau de certitude correspondant */ mapConfidenceToCertainty(confidence) { if (confidence >= 0.9) return 'definite'; if (confidence >= 0.75) return 'high'; if (confidence >= 0.5) return 'moderate'; if (confidence >= 0.3) return 'low'; return 'speculative'; } /** * Enrichit une pensée existante avec des attributs sémantiques pour ses connexions * * @param thoughtId L'ID de la pensée à enrichir * @returns Le nombre de connexions enrichies */ enrichThoughtConnections(thoughtId) { const thought = this.getThought(thoughtId); if (!thought) return 0; let enrichedCount = 0; for (const connection of thought.connections) { // Ignorer les connexions déjà enrichies ou inférées (qui ont déjà des attributs) if (connection.attributes || connection.inferred) continue; const targetThought = this.getThought(connection.targetId); if (!targetThought) continue; // Enrichir avec des attributs sémantiques inférés du contexte connection.attributes = this.inferConnectionAttributes(thought, targetThought, connection.type); enrichedCount++; } return enrichedCount; } /** * Infère des attributs sémantiques pour une connexion * * @param sourceThought La pensée source * @param targetThought La pensée cible * @param type Le type de connexion * @returns Les attributs sémantiques inférés */ inferConnectionAttributes(sourceThought, targetThought, type) { const attributes = {}; // Inférer la temporalité (inchangé) if (sourceThought.timestamp < targetThought.timestamp) { attributes.temporality = 'before'; } else if (sourceThought.timestamp > targetThought.timestamp) { attributes.temporality = 'after'; } else { attributes.temporality = 'concurrent'; } // --- Inférence de Certitude --- const certaintyMarkersLow = ['peut-être', 'semble', 'possible', 'pourrait', 'probablement', 'suggère']; const certaintyMarkersHigh = ['certainement', 'clairement', 'évidemment', 'prouvé', 'démontré', 'sans aucun doute']; const combinedContentForCertainty = sourceThought.content.toLowerCase() + " " + targetThought.content.toLowerCase(); if (certaintyMarkersHigh.some((marker) => combinedContentForCertainty.includes(marker))) { // Explicitly typed param attributes.certainty = 'high'; // Ou 'definite' si très fort } else if (certaintyMarkersLow.some((marker) => combinedContentForCertainty.includes(marker))) { // Explicitly typed param attributes.certainty = 'low'; // Ou 'speculative' si très faible } else { // Default basé sur la confiance de la connexion inférée (si disponible) // Ou 'moderate' par défaut attributes.certainty = 'moderate'; } // Note: La confiance de la connexion elle-même (strength) pourrait aussi influencer la certitude. // Inférer la nature (légèrement affiné) switch (type) { case 'supports': case 'contradicts': attributes.nature = 'associative'; // Could be causal/correlational depending on content break; case 'derives': attributes.nature = combinedContentForCertainty.includes('parce que') || combinedContentForCertainty.includes('car') ? 'causal' : 'sequential'; break; case 'refines': case 'generalizes': case 'exemplifies': case 'component-of': case 'synthesizes': attributes.nature = 'hierarchical'; break; case 'branches': attributes.nature = 'sequential'; break; case 'analyzes': case 'evaluates': case 'applies': attributes.nature = 'causal'; // Maintient l'hypothèse causale/applicative break; default: attributes.nature = 'associative'; } // Inférer la directionnalité (inchangé) if (this.shouldCreateReciprocalConnection(type)) { attributes.directionality = 'bidirectional'; } else { attributes.directionality = 'unidirectional'; } // Inférer la portée (inchangé - heuristique simple) const sourceLength = sourceThought.content.length; const targetLength = targetThought.content.length; if (Math.abs(sourceLength - targetLength) < 50) { attributes.scope = 'specific'; } else if (sourceLength > targetLength * 1.5 || targetLength > sourceLength * 1.5) { attributes.scope = 'broad'; // One thought is much broader/narrower } else { attributes.scope = 'partial'; } return attributes; } /** * Suggère les prochaines étapes de raisonnement, potentiellement en utilisant le LLM. * * @param limit Le nombre maximum de suggestions * @param sessionId L'identifiant de session facultatif pour filtrer le contexte * @returns Une promesse résolvant vers un tableau de suggestions pour les prochaines étapes */ async suggestNextSteps(limit = 3, sessionId) { const sessionThoughts = this.getAllThoughts(sessionId); // Si aucune pensée n'existe dans la session, retourner un tableau vide if (sessionThoughts.length === 0) { return []; } // Recueillir un contexte riche pour le LLM // 1. Obtenir la pensée initiale (probablement la première de la session) const initialThought = sessionThoughts.length > 0 ? sessionThoughts.reduce((earliest, current) => new Date(earliest.metadata?.timestamp || 0) <= new Date(current.metadata?.timestamp || 0) ? earliest : current, sessionThoughts[0]) : null; const initialThoughtSummary = initialThought ? `[${initialThought.type}] ${initialThought.content.substring(0, 150)}...` : 'Non identifié'; // 2. Identifier les nœuds clés // Hypothèses récentes const hypotheses = sessionThoughts.filter(t => t.type === 'hypothesis'); const recentHypotheses = hypotheses .sort((a, b) => new Date(b.metadata?.timestamp || 0).getTime() - new Date(a.metadata?.timestamp || 0).getTime()) .slice(0, 3); const hypothesesSummary = recentHypotheses.length > 0 ? recentHypotheses.map(h => h.content.substring(0, 100)).join('\n') : 'Aucune hypothèse formulée'; // Conclusions récentes const conclusions = sessionThoughts.filter(t => t.type === 'conclusion'); const recentConclusions = conclusions .sort((a, b) => new Date(b.metadata?.timestamp || 0).getTime() - new Date(a.metadata?.timestamp || 0).getTime()) .slice(0, 2); const conclusionsSummary = recentConclusions.length > 0 ? recentConclusions.map(c => c.content.substring(0, 100)).join('\n') : 'Aucune conclusion formulée'; // Questions ouvertes (pensées contenant des points d'interrogation ou des mots-clés de question) const questionKeywords = ['pourquoi', 'comment', 'quoi', 'qui', 'où', 'quand', 'quel', 'quelle']; const openQuestions = sessionThoughts.filter(t => t.content.includes('?') || questionKeywords.some(keyword => t.content.toLowerCase().includes(keyword))); const recentQuestions = openQuestions .sort((a, b) => new Date(b.metadata?.timestamp || 0).getTime() - new Date(a.metadata?.timestamp || 0).getTime()) .slice(0, 3); const questionsSummary = recentQuestions.length > 0 ? recentQuestions.map(q => q.content.substring(0, 100)).join('\n') : 'Aucune question explicite identifiée'; // 3. Identifier les contradictions et problèmes const contradictions = sessionThoughts.filter(thought => thought.connections.some(conn => { // Check if the connected thought also belongs to the session const targetThought = this.getThought(conn.targetId); return conn.type === 'contradicts' && targetThought?.metadata?.sessionId === sessionId; })); const contradictionsSummary = contradictions.length > 0 ? contradictions.map(c => { const contradictingConnections = c.connections.filter(conn => conn.type === 'contradicts'); const contradictedThoughts = contradictingConnections.map(conn => this.getThought(conn.targetId)); return `"${c.content.substring(0, 70)}..." contredit "${contradictedThoughts[0]?.content.substring(0, 70)}..."`; }).join('\n') : 'Aucune contradiction explicite détectée'; // 4. Résumer l'activité récente const recentThoughts = this.getRecentThoughts(5, sessionId); const thoughtTypes = recentThoughts.map(t => t.type); const uniqueTypes = [...new Set(thoughtTypes)]; const recentActivitySummary = `Activité récente: ${uniqueTypes.join(', ')}`; // 5. Analyser la structure du graphe let graphStructureSummary = 'Structure indéterminée'; if (sessionThoughts.length <= 3) { graphStructureSummary = 'Exploration initiale'; } else { // Vérifier si la structure est principalement linéaire const isLinear = sessionThoughts.every(t => t.connections.length <= 2); // Vérifier s'il y a des clusters const hasClusters = this.detectClusters().length > 1; // Vérifier s'il y a beaucoup de connexions (structure en réseau) const avgConnections = sessionThoughts.reduce((sum, t) => sum + t.connections.length, 0) / sessionThoughts.length; if (isLinear) { graphStructureSummary = 'Progression principalement linéaire'; } else if (hasClusters) { graphStructureSummary = 'Plusieurs clusters de pensées identifiés'; } else if (avgConnections > 3) { graphStructureSummary = 'Structure en réseau avec nombreuses connexions'; } else { graphStructureSummary = 'Structure mixte avec quelques branches'; } } // 6. Construire un prompt utilisateur enrichi const userPrompt = ` Analyse de la Session de Raisonnement (ID: ${sessionId || 'session principale'}): - Pensée Initiale/Objectif: ${