smart-thinking-mcp
Version:
Un serveur MCP avancé pour le raisonnement multi-dimensionnel, adaptatif et collaboratif
1,071 lines • 67.6 kB
JavaScript
"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: ${