metrics-calculator.ts•53.3 kB
import {
ThoughtNode,
ThoughtMetrics,
Connection,
ConnectionType,
VerificationStatus,
CalculationVerificationResult,
MetricBreakdown,
ThoughtMetricBreakdown,
MetricContribution
} from './types';
/**
* metrics-calculator.ts - VERSION OPTIMISÉE
*
* Système centralisé pour tous les calculs de métriques dans Smart-Thinking
* Implémente des algorithmes avancés optimisés pour calculer la confiance, la pertinence,
* la qualité et autres métriques utilisées par le système.
*/
/**
* Classe qui centralise tous les calculs de métriques dans Smart-Thinking.
* Cette classe implémente des algorithmes avancés pour le calcul de confiance,
* la pertinence, la qualité et d'autres métriques utilisées dans le système.
*/
export class MetricsCalculator {
private metricBreakdowns: Map<string, ThoughtMetricBreakdown> = new Map();
// Dictionnaires de mots indicateurs pour l'analyse linguistique
private positiveWords: string[] = [
'précis', 'clair', 'cohérent', 'logique', 'détaillé', 'rigoureux', 'méthodique',
'analytique', 'systématique', 'fondé', 'approfondi', 'équilibré', 'objectif',
'exact', 'raisonnable', 'valide', 'pertinent', 'significatif'
];
private negativeWords: string[] = [
'vague', 'confus', 'incohérent', 'illogique', 'superficiel', 'flou', 'ambigu',
'subjectif', 'inexact', 'imprécis', 'douteux', 'spéculatif', 'non pertinent',
'biaisé', 'contradictoire', 'simpliste', 'circulaire'
];
// Indicateurs de qualité par type de pensée
private qualityIndicators: Record<string, {positive: string[], negative: string[]}> = {
'regular': {
positive: ['clairement formulé', 'bien structuré', 'idée développée'],
negative: ['incomplet', 'hors sujet', 'mal structuré']
},
'meta': {
positive: ['auto-critique', 'réflexif', 'évaluatif', 'conscient'],
negative: ['superficiel', 'non réflexif', 'auto-complaisant']
},
'hypothesis': {
positive: ['testable', 'falsifiable', 'précis', 'fondé sur', 'prédictif'],
negative: ['vague', 'non testable', 'infalsifiable', 'sans fondement']
},
'conclusion': {
positive: ['synthétise', 'résume', 'découle de', 'cohérent avec'],
negative: ['déconnecté', 'sans rapport', 'non justifié', 'contradictoire']
},
'revision': {
positive: ['améliore', 'corrige', 'précise', 'clarifie', 'nuance'],
negative: ['répétitif', 'redondant', 'contredit sans justification']
}
};
// Modalisateurs de certitude/incertitude pour l'analyse linguistique
private uncertaintyModifiers: string[] = [
'peut-être', 'possible', 'probablement', 'semble',
'pourrait', 'hypothèse', 'suppose', 'doute',
'éventuellement', 'potentiellement', 'apparemment', 'suggère'
];
private certaintyModifiers: string[] = [
'certainement', 'clairement', 'évidemment', 'sans doute', 'indiscutablement',
'nécessairement', 'doit', 'est', 'démontré', 'prouvé', 'assurément',
'incontestablement', 'manifestement', 'définitivement', 'inévitablement'
];
// Mots-stop en français (stop words) - OPTIMISATION: Utilisation d'un Set pour recherche O(1)
private stopWords: Set<string> = new Set([
'le', 'la', 'les', 'un', 'une', 'des', 'ce', 'cette', 'ces',
'et', 'ou', 'mais', 'donc', 'car', 'ni', 'que', 'qui',
'dans', 'sur', 'sous', 'avec', 'sans', 'pour', 'par',
'je', 'tu', 'il', 'elle', 'nous', 'vous', 'ils', 'elles',
'est', 'sont', 'être', 'avoir', 'fait', 'faire',
'plus', 'moins', 'très', 'trop', 'peu', 'beaucoup'
]);
// OPTIMISATION: Pré-compilation des expressions régulières
private REGEX = {
REFERENCES: /\(([^)]+)\)|\[[^\]]+\]/g,
NUMBERS: /\d+([.,]\d+)?%?/g,
PUNCTUATION: /[.,\/#!$%\^&\*;:{}=\-_`~()]/g,
WHITESPACE: /\s+/,
SENTENCES: /[.!?]+/,
UNCERTAINTY_MODIFIERS: null as RegExp | null,
CERTAINTY_MODIFIERS: null as RegExp | null,
MATH_CALCULATION: /(?:\d+(?:\.\d+)?)\s*(?:[+\-*/^]|plus|moins|divisé|fois|multiplié)\s*(?:\d+(?:\.\d+)?)/i,
MATHEMATICAL_PROOF: /(?:prouvons|démontrons|supposons|soit|démonstration|preuve|CQFD|théorème|lemme|corollaire)/i,
LOGICAL_DEDUCTION: /(?:donc|par conséquent|ainsi|il s'ensuit que|cela implique|on en déduit|cela prouve)/i,
FACTUAL_CLAIM: /(?:est|sont|était|étaient|a été|ont été|sera|seront)\s+(?:un|une|des|le|la|les|du|de la)/i,
STRONG_EMOTION: /(?:!{2,}|incroyable|fantastique|horrible|déteste|adore|absolument|totalement|complètement|extrêmement)/i
};
// OPTIMISATION: Mappage pour les types de pensée
private typeScoresMap: Map<string, number> = new Map([
['conclusion', 0.8],
['hypothesis', 0.6],
['meta', 0.7],
['revision', 0.75],
['regular', 0.65]
]);
// OPTIMISATION: Mappage pour les types de connexion
private connectionWeightsMap: Map<ConnectionType, number> = new Map([
['supports', 0.9],
['contradicts', 0.7],
['refines', 0.8],
['branches', 0.6],
['derives', 0.75],
['associates', 0.5],
['exemplifies', 0.7],
['generalizes', 0.75],
['compares', 0.6],
['contrasts', 0.65],
['questions', 0.5],
['extends', 0.7],
['analyzes', 0.8],
['synthesizes', 0.85],
['applies', 0.7],
['evaluates', 0.8],
['cites', 0.9],
['extended-by', 0.7],
['analyzed-by', 0.8],
['component-of', 0.7],
['applied-by', 0.7],
['evaluated-by', 0.8],
['cited-by', 0.9]
]);
// OPTIMISATION: Mappage pour status de vérification
private verificationScoreMap: Record<VerificationStatus, number> = {
'verified': 0.95,
'partially_verified': 0.75,
'contradicted': 0.3,
'contradictory': 0.2,
'uncertain': 0.4,
'absence_of_information': 0.65,
'unverified': 0.45,
'inconclusive': 0.55
};
// Constantes centralisées pour les seuils utilisés dans les calculs
private THRESHOLDS = {
// Confiance
MIN_CONFIDENCE: 0.1,
MAX_CONFIDENCE: 0.95,
// Pertinence
MIN_RELEVANCE: 0.1,
MAX_RELEVANCE: 0.95,
// Qualité
MIN_QUALITY: 0.1,
MAX_QUALITY: 0.95,
// Fiabilité
MIN_RELIABILITY: 0.1,
MAX_RELIABILITY: 0.95,
// Confiance
HIGH_CONFIDENCE_THRESHOLD: 0.80,
MEDIUM_CONFIDENCE_THRESHOLD: 0.50,
LOW_CONFIDENCE_THRESHOLD: 0.30,
// Fiabilité
HIGH_RELIABILITY_THRESHOLD: 0.75,
MEDIUM_RELIABILITY_THRESHOLD: 0.45,
LOW_RELIABILITY_THRESHOLD: 0.25,
// Vérification
VERIFIED_THRESHOLD: 0.8,
PARTIALLY_VERIFIED_THRESHOLD: 0.45,
ABSENCE_THRESHOLD: 0.65,
UNCERTAIN_THRESHOLD: 0.5,
CONTRADICTION_THRESHOLD: 0.3,
// Similarité
HIGH_SIMILARITY: 0.85,
// Erreur numérique
WEIGHT_TOLERANCE: 0.001,
// Nouveaux seuils pour les calculs mathématiques
MATH_HIGH_CONFIDENCE: 0.8,
MATH_MEDIUM_CONFIDENCE: 0.65
};
// Paramètres configurables pour ajuster les calculs
private config = {
// Poids des différentes composantes dans le calcul de la confiance (HEURISTIQUE UNIQUEMENT)
confidenceWeights: {
modifierAnalysis: 0.4, // Analyse des modalisateurs de certitude/incertitude
thoughtType: 0.2, // Type de pensée (hypothesis, conclusion, etc.)
structuralIndicators: 0.2, // Indicateurs structurels (références, citations, etc.)
sentimentBalance: 0.2 // Équilibre des sentiments positifs/négatifs
},
// Poids des différentes composantes dans le calcul de la pertinence (HEURISTIQUE UNIQUEMENT)
relevanceWeights: {
keywordOverlap: 0.5, // Chevauchement de mots-clés
connectionStrength: 0.5 // Force des connexions dans le graphe
},
// Poids des différentes composantes dans le calcul de la qualité (HEURISTIQUE UNIQUEMENT)
qualityWeights: {
wordIndicators: 0.25, // Indicateurs de mots positifs/négatifs
typeSpecificIndicators: 0.25, // Indicateurs spécifiques au type
structuralBalance: 0.2, // Équilibre structurel
coherence: 0.3 // Cohérence globale
},
// Ajustements par type de pensée (HEURISTIQUE UNIQUEMENT)
typeAdjustments: {
'hypothesis': { confidence: 0.9, quality: 1.0, coherence: 1.1 },
'conclusion': { confidence: 1.1, quality: 1.2, coherence: 1.2 },
'meta': { confidence: 1.0, quality: 1.1, coherence: 0.9 },
'revision': { confidence: 1.05, quality: 1.1, coherence: 1.05 },
'regular': { confidence: 1.0, quality: 1.0, coherence: 1.0 }
},
// Paramètres pour l'algorithme TF-IDF (HEURISTIQUE UNIQUEMENT)
tfIdf: {
minFrequency: 2, // Fréquence minimale pour qu'un terme soit considéré
maxDocumentPercentage: 0.7 // Pourcentage maximum de documents contenant un terme
}
};
/**
* Constructeur
* @param customConfig Configuration personnalisée (optionnelle)
*/
constructor(customConfig?: Partial<typeof MetricsCalculator.prototype.config>) {
if (customConfig) {
this.config = { ...this.config, ...customConfig };
}
// Vérifier que les poids s'additionnent à 1.0
this.validateWeights();
// OPTIMISATION: Précompiler les expressions régulières pour les modalisateurs
this.REGEX.UNCERTAINTY_MODIFIERS = new RegExp('\\b(' + this.uncertaintyModifiers.join('|') + ')\\b', 'gi');
this.REGEX.CERTAINTY_MODIFIERS = new RegExp('\\b(' + this.certaintyModifiers.join('|') + ')\\b', 'gi');
}
/**
* Valide et normalise tous les poids dans les configurations pour s'assurer qu'ils s'additionnent à 1.0
* Affiche un avertissement si ce n'est pas le cas et corrige automatiquement
*/
private validateWeights(): void {
const categories = [
{ name: 'confiance', weights: this.config.confidenceWeights as Record<string, number> },
{ name: 'pertinence', weights: this.config.relevanceWeights as Record<string, number> },
{ name: 'qualité', weights: this.config.qualityWeights as Record<string, number> }
];
for (const category of categories) {
const sum = Object.values(category.weights).reduce((total, w) => total + w, 0);
// Vérifier si la somme des poids est proche de 1.0
if (Math.abs(sum - 1.0) > this.THRESHOLDS.WEIGHT_TOLERANCE) {
console.error(`AVERTISSEMENT: Les poids de ${category.name} s'additionnent à ${sum}, pas à 1.0`);
// Normaliser automatiquement les poids
const factor = 1.0 / sum;
for (const key in category.weights) {
category.weights[key] *= factor;
}
console.error(`Correction automatique appliquée aux poids de ${category.name}`);
}
}
}
/**
* OPTIMISATION: Compter les modalisateurs avec RegExp (plus rapide)
*
* @param content Contenu à analyser
* @param regex Expression régulière à utiliser
* @returns Nombre d'occurrences
*/
private countModifiers(content: string, regex: RegExp): number {
const matches = content.match(regex);
return matches ? matches.length : 0;
}
/**
* Calcule la métrique de confiance pour une pensée.
* Priorise l'appel LLM avec contexte, utilise l'heuristique en fallback.
*
* @param thought La pensée à évaluer
* @param connectedThoughts Les pensées connectées (pour le contexte)
* @returns Niveau de confiance entre 0 et 1
*/
async calculateConfidence(thought: ThoughtNode, connectedThoughts: ThoughtNode[] = []): Promise<number> {
const breakdown = this.computeConfidenceBreakdown(thought, connectedThoughts);
this.storeBreakdown(thought.id, 'confidence', breakdown);
return breakdown.score;
}
private computeConfidenceBreakdown(thought: ThoughtNode, connectedThoughts: ThoughtNode[]): MetricBreakdown {
const content = thought.content.toLowerCase();
const weights = this.config.confidenceWeights;
const typeAdjustment = this.config.typeAdjustments[thought.type]?.confidence ?? 1;
const uncertaintyCount = this.countModifiers(content, this.REGEX.UNCERTAINTY_MODIFIERS!);
const certaintyCount = this.countModifiers(content, this.REGEX.CERTAINTY_MODIFIERS!);
const totalModifiers = uncertaintyCount + certaintyCount;
const modifierScore = totalModifiers === 0 ? 0.5 : this.clamp(certaintyCount / totalModifiers, 0, 1);
const referenceMatches = content.match(this.REGEX.REFERENCES) || [];
const numberMatches = content.match(this.REGEX.NUMBERS) || [];
const referenceScore = this.clamp(0.5 + Math.min(referenceMatches.length * 0.05, 0.2), 0, 1);
const numberScore = Math.min(numberMatches.length * 0.03, 0.15);
const structuralScore = this.clamp(referenceScore + numberScore, 0.3, 0.9);
const typeScore = this.typeScoresMap.get(thought.type) || 0.65;
const positiveWordsRegex = new RegExp('\\b(' + this.positiveWords.join('|') + ')\\b', 'gi');
const negativeWordsRegex = new RegExp('\\b(' + this.negativeWords.join('|') + ')\\b', 'gi');
const positiveMatches = content.match(positiveWordsRegex) || [];
const negativeMatches = content.match(negativeWordsRegex) || [];
const totalSentiment = positiveMatches.length + negativeMatches.length;
const sentimentBalance = totalSentiment === 0 ? 0.5 : this.clamp(0.5 + (positiveMatches.length - negativeMatches.length) / (totalSentiment * 2), 0, 1);
let connectionContextBoost = 0;
if (connectedThoughts.length > 0 && thought.connections.length > 0) {
const firstConnection = thought.connections[0];
const prevThought = connectedThoughts.find(t => t.id === firstConnection.targetId);
if (prevThought) {
const overlap = this.computeTokenOverlap(thought.content, prevThought.content);
connectionContextBoost = overlap * 0.1;
if (['contradicts', 'questions'].includes(firstConnection.type)) {
connectionContextBoost -= 0.05;
}
}
}
const contributions: MetricContribution[] = [
{
label: 'Modalisateurs de certitude',
weight: weights.modifierAnalysis,
value: modifierScore,
impact: modifierScore * weights.modifierAnalysis,
rationale: totalModifiers === 0
? 'Peu de modalisateurs explicites détectés.'
: `${certaintyCount} marqueurs de certitude contre ${uncertaintyCount} d\'incertitude.`
},
{
label: 'Structure factuelle',
weight: weights.structuralIndicators,
value: structuralScore,
impact: structuralScore * weights.structuralIndicators,
rationale: `${referenceMatches.length} références et ${numberMatches.length} éléments chiffrés détectés.`
},
{
label: 'Type de pensée',
weight: weights.thoughtType,
value: typeScore,
impact: typeScore * weights.thoughtType,
rationale: `Le type ${thought.type} est associé à un niveau de certitude ${typeScore >= 0.7 ? 'élevé' : 'modéré'}.`
},
{
label: 'Équilibre lexical',
weight: weights.sentimentBalance,
value: sentimentBalance,
impact: sentimentBalance * weights.sentimentBalance,
rationale: totalSentiment === 0
? 'Aucun lexique évaluatif particulier.'
: `${positiveMatches.length} termes positifs contre ${negativeMatches.length} négatifs.`
}
];
if (connectionContextBoost !== 0) {
contributions.push({
label: 'Contexte des connexions',
weight: 0.1,
value: this.clamp(connectionContextBoost + 0.5, 0, 1),
impact: connectionContextBoost,
rationale: connectionContextBoost > 0
? 'Overlap lexical avec les pensées reliées.'
: 'Connexions contradictoires réduisant la certitude.'
});
}
const baseScore = contributions.reduce((sum, item) => sum + item.impact, 0);
const adjustedScore = this.clamp(baseScore * typeAdjustment, this.THRESHOLDS.MIN_CONFIDENCE, this.THRESHOLDS.MAX_CONFIDENCE);
contributions.push({
label: 'Ajustement type',
weight: typeAdjustment,
value: this.clamp(typeAdjustment, 0, 2),
impact: adjustedScore - baseScore,
rationale: `Le type ${thought.type} applique un facteur ${typeAdjustment.toFixed(2)}.`
});
const summary = this.buildMetricSummary('confiance', adjustedScore, contributions);
return {
score: adjustedScore,
contributions,
summary
};
}
/**
* Calcule la métrique de pertinence pour une pensée.
* Priorise l'appel LLM avec contexte, utilise l'heuristique en fallback.
*
* @param thought La pensée à évaluer
* @param connectedThoughts Les pensées connectées (contexte)
* @returns Niveau de pertinence entre 0 et 1
*/
async calculateRelevance(thought: ThoughtNode, connectedThoughts: ThoughtNode[]): Promise<number> {
const breakdown = this.computeRelevanceBreakdown(thought, connectedThoughts);
this.storeBreakdown(thought.id, 'relevance', breakdown);
return breakdown.score;
}
private computeRelevanceBreakdown(thought: ThoughtNode, connectedThoughts: ThoughtNode[]): MetricBreakdown {
const contextContent = connectedThoughts.map(t => t.content).join(' ');
const keywordWeights = this.extractAndWeightContextKeywords(contextContent);
const thoughtKeywords = this.extractKeywords(thought.content);
let keywordScore = 0;
let totalWeight = 0;
for (const [keyword, weight] of Object.entries(keywordWeights)) {
totalWeight += weight;
if (thoughtKeywords.includes(keyword)) {
keywordScore += weight;
}
}
const keywordOverlapScore = totalWeight > 0 ? keywordScore / totalWeight : 0.4;
let outgoingScore = 0.5;
if (thought.connections.length > 0) {
const total = thought.connections.reduce((sum, conn) => {
const typeWeight = this.connectionWeightsMap.get(conn.type) || 0.5;
return sum + conn.strength * typeWeight;
}, 0);
outgoingScore = total / thought.connections.length;
}
let incomingScore = 0.5;
const incomingConnections: Connection[] = [];
for (const node of connectedThoughts) {
for (const conn of node.connections) {
if (conn.targetId === thought.id) {
incomingConnections.push(conn);
}
}
}
if (incomingConnections.length > 0) {
const total = incomingConnections.reduce((sum, conn) => sum + conn.strength, 0);
incomingScore = total / incomingConnections.length;
}
const connectionScore = (outgoingScore + incomingScore) / 2;
const weights = this.config.relevanceWeights;
const baseScore = keywordOverlapScore * weights.keywordOverlap + connectionScore * weights.connectionStrength;
let typeAdjustment = 1;
if (thought.type === 'revision') typeAdjustment = 1.1;
if (thought.type === 'meta') typeAdjustment = 0.9;
const adjustedScore = this.clamp(baseScore * typeAdjustment, this.THRESHOLDS.MIN_RELEVANCE, this.THRESHOLDS.MAX_RELEVANCE);
const contributions: MetricContribution[] = [
{
label: 'Recoupement lexical',
weight: weights.keywordOverlap,
value: this.clamp(keywordOverlapScore, 0, 1),
impact: keywordOverlapScore * weights.keywordOverlap,
rationale: totalWeight === 0
? 'Peu de mots-clés partagés avec le contexte.'
: `${Math.round(keywordScore * 100) / 100} poids cumulés sur ${Math.round(totalWeight * 100) / 100}.`
},
{
label: 'Connexions',
weight: weights.connectionStrength,
value: this.clamp(connectionScore, 0, 1),
impact: connectionScore * weights.connectionStrength,
rationale: `Connexions sortantes ${outgoingScore.toFixed(2)} | entrantes ${incomingScore.toFixed(2)}.`
},
{
label: 'Ajustement type',
weight: typeAdjustment,
value: this.clamp(typeAdjustment, 0, 2),
impact: adjustedScore - baseScore,
rationale: typeAdjustment === 1
? 'Type ne modifiant pas la pertinence.'
: `Le type ${thought.type} applique un facteur ${typeAdjustment.toFixed(2)}.`
}
];
const summary = this.buildMetricSummary('pertinence', adjustedScore, contributions);
return {
score: adjustedScore,
contributions,
summary
};
}
/**
* Calcule la métrique de qualité globale pour une pensée.
* Priorise l'appel LLM avec contexte, utilise l'heuristique en fallback.
*
* @param thought La pensée à évaluer
* @param connectedThoughts Les pensées connectées (contexte)
* @returns Niveau de qualité entre 0 et 1
*/
async calculateQuality(thought: ThoughtNode, connectedThoughts: ThoughtNode[] = []): Promise<number> {
const breakdown = this.computeQualityBreakdown(thought, connectedThoughts);
this.storeBreakdown(thought.id, 'quality', breakdown);
return breakdown.score;
}
private computeQualityBreakdown(thought: ThoughtNode, connectedThoughts: ThoughtNode[]): MetricBreakdown {
const content = thought.content.toLowerCase();
const weights = this.config.qualityWeights;
const typeAdjustment = this.config.typeAdjustments[thought.type]?.quality ?? 1;
const positiveWordsRegex = new RegExp('\\b(' + this.positiveWords.join('|') + ')\\b', 'gi');
const negativeWordsRegex = new RegExp('\\b(' + this.negativeWords.join('|') + ')\\b', 'gi');
const positiveMatches = content.match(positiveWordsRegex) || [];
const negativeMatches = content.match(negativeWordsRegex) || [];
const totalMatches = positiveMatches.length + negativeMatches.length;
const lexicalScore = totalMatches === 0 ? 0.5 : this.clamp(0.5 + (positiveMatches.length - negativeMatches.length) / (totalMatches * 2), 0.25, 0.95);
const typeIndicators = this.qualityIndicators[thought.type] || this.qualityIndicators['regular'];
const positiveTypeRegex = new RegExp(typeIndicators.positive.join('|'), 'gi');
const negativeTypeRegex = new RegExp(typeIndicators.negative.join('|'), 'gi');
const positiveTypeMatches = content.match(positiveTypeRegex) || [];
const negativeTypeMatches = content.match(negativeTypeRegex) || [];
const typeIndicatorScoreRaw = positiveTypeMatches.length - negativeTypeMatches.length;
const typeIndicatorScore = this.clamp(0.5 + typeIndicatorScoreRaw * 0.1, 0.2, 0.95);
const wordsArray = content.split(this.REGEX.WHITESPACE).filter(Boolean);
const wordCount = wordsArray.length;
const sentencesArray = content.split(this.REGEX.SENTENCES).filter(s => s.trim().length > 0);
const sentenceCount = sentencesArray.length;
const avgSentenceLength = sentenceCount === 0 ? wordCount : wordCount / sentenceCount;
let structuralScore: number;
if (wordCount < 5) structuralScore = 0.3;
else if (wordCount > 300) structuralScore = 0.45;
else if (wordCount >= 150) structuralScore = 0.65;
else if (wordCount >= 40) structuralScore = 0.8;
else structuralScore = 0.55;
if (avgSentenceLength > 28 || (avgSentenceLength < 6 && sentenceCount > 1)) {
structuralScore *= 0.85;
}
const hasOrderedMarkers = /(premièrement|deuxièmement|en conclusion|pour commencer)/i.test(content);
if (hasOrderedMarkers) {
structuralScore = this.clamp(structuralScore + 0.1, 0, 1);
}
let coherenceScore = 0.5;
if (thought.connections.length > 0) {
const avgStrength = thought.connections.reduce((sum, conn) => sum + conn.strength, 0) / thought.connections.length;
coherenceScore = this.clamp(0.5 + avgStrength * 0.3, 0.4, 0.9);
}
if (connectedThoughts.length > 0) {
const overlap = connectedThoughts.reduce((acc, node) => acc + this.computeTokenOverlap(thought.content, node.content), 0);
coherenceScore = this.clamp(coherenceScore + overlap * 0.1, 0.4, 0.95);
}
const baseScore =
lexicalScore * weights.wordIndicators +
typeIndicatorScore * weights.typeSpecificIndicators +
structuralScore * weights.structuralBalance +
coherenceScore * weights.coherence;
const adjustedScore = this.clamp(baseScore * typeAdjustment, this.THRESHOLDS.MIN_QUALITY, this.THRESHOLDS.MAX_QUALITY);
const contributions: MetricContribution[] = [
{
label: 'Lexique qualitatif',
weight: weights.wordIndicators,
value: lexicalScore,
impact: lexicalScore * weights.wordIndicators,
rationale: totalMatches === 0
? 'Aucun terme qualitatif spécifique détecté.'
: `${positiveMatches.length} indices positifs vs ${negativeMatches.length} négatifs.`
},
{
label: 'Indicateurs spécifiques au type',
weight: weights.typeSpecificIndicators,
value: typeIndicatorScore,
impact: typeIndicatorScore * weights.typeSpecificIndicators,
rationale: `${positiveTypeMatches.length} indices attendus, ${negativeTypeMatches.length} signaux défavorables.`
},
{
label: 'Structure',
weight: weights.structuralBalance,
value: structuralScore,
impact: structuralScore * weights.structuralBalance,
rationale: `${wordCount} mots, ${sentenceCount} phrases (longueur moyenne ${avgSentenceLength.toFixed(1)}).`
},
{
label: 'Cohérence',
weight: weights.coherence,
value: coherenceScore,
impact: coherenceScore * weights.coherence,
rationale: thought.connections.length === 0
? 'Peu de liens explicites avec d\'autres pensées.'
: `${thought.connections.length} connexions utilisées pour valider la cohérence.`
},
{
label: 'Ajustement type',
weight: typeAdjustment,
value: this.clamp(typeAdjustment, 0, 2),
impact: adjustedScore - baseScore,
rationale: typeAdjustment === 1
? 'Type sans ajustement spécifique.'
: `Le type ${thought.type} applique un facteur ${typeAdjustment.toFixed(2)}.`
}
];
const summary = this.buildMetricSummary('qualité', adjustedScore, contributions);
return {
score: adjustedScore,
contributions,
summary
};
}
private computeTokenOverlap(a: string, b: string): number {
const aTokens = new Set(this.extractKeywords(a));
const bTokens = new Set(this.extractKeywords(b));
if (aTokens.size === 0 || bTokens.size === 0) {
return 0;
}
let overlap = 0;
for (const token of aTokens) {
if (bTokens.has(token)) {
overlap += 1;
}
}
return overlap / Math.min(aTokens.size, bTokens.size);
}
private clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
private buildMetricSummary(metricLabel: string, score: number, contributions: MetricContribution[]): string {
const formattedScore = (Math.round(score * 100) / 100).toFixed(2);
const descriptors = contributions
.filter(item => item.label !== 'Ajustement type')
.sort((a, b) => b.impact - a.impact)
.slice(0, 3)
.map(item => `${item.label.toLowerCase()} (${(item.value * 100).toFixed(0)}%)`);
return `Score de ${metricLabel} ${formattedScore}. Principaux facteurs : ${descriptors.join(', ')}.`;
}
private storeBreakdown(thoughtId: string, metric: keyof ThoughtMetricBreakdown, breakdown: MetricBreakdown): void {
const existing = this.metricBreakdowns.get(thoughtId) ?? {};
const updated: ThoughtMetricBreakdown = { ...existing, [metric]: breakdown };
this.metricBreakdowns.set(thoughtId, updated);
}
public getMetricBreakdown(thoughtId: string): ThoughtMetricBreakdown | undefined {
const breakdown = this.metricBreakdowns.get(thoughtId);
if (!breakdown) {
return undefined;
}
return {
confidence: breakdown.confidence ? { ...breakdown.confidence, contributions: [...breakdown.confidence.contributions] } : undefined,
relevance: breakdown.relevance ? { ...breakdown.relevance, contributions: [...breakdown.relevance.contributions] } : undefined,
quality: breakdown.quality ? { ...breakdown.quality, contributions: [...breakdown.quality.contributions] } : undefined
};
}
public clearMetricBreakdown(thoughtId?: string): void {
if (thoughtId) {
this.metricBreakdowns.delete(thoughtId);
} else {
this.metricBreakdowns.clear();
}
}
private hasAny(text: string, terms: string[]): boolean {
return terms.some(term => text.includes(term));
}
/**
* Calcule un score global de fiabilité basé sur différentes métriques et vérifications
* avec une normalisation automatique des poids - VERSION OPTIMISÉE
*
* @param metrics Les métriques de base
* @param verificationStatus Statut de vérification actuel
* @param calculationResults Résultats de vérification des calculs (optionnel)
* @param previousScore Score précédent (optionnel)
* @returns Score de fiabilité entre 0 et 1
*/
calculateReliabilityScore(
metrics: ThoughtMetrics,
verificationStatus: VerificationStatus,
calculationResults?: CalculationVerificationResult[],
previousScore?: number
): number {
// OPTIMISATION: Utiliser Map pour accéder au score de vérification
const verificationScore = this.verificationScoreMap[verificationStatus] || 0.45;
// OPTIMISATION: Déterminer les poids avec moins de conditions
// Poids pour chaque métrique
let weights = calculationResults && calculationResults.length > 0
? { confidence: 0.25, relevance: 0.10, quality: 0.10, verification: 0.55 }
: { confidence: 0.35, relevance: 0.15, quality: 0.15, verification: 0.35 };
// Calcul du score brut pondéré
let rawScore =
weights.confidence * metrics.confidence +
weights.relevance * metrics.relevance +
weights.quality * metrics.quality +
weights.verification * verificationScore;
// OPTIMISATION: Bonus pour les calculs corrects en une seule passe
if (calculationResults && calculationResults.length > 0) {
let correctCalculations = 0;
for (const result of calculationResults) {
if (result.isCorrect) correctCalculations++;
}
const correctRatio = correctCalculations / calculationResults.length;
rawScore += correctRatio * 0.1;
}
// OPTIMISATION: Ajustement selon le statut spécifique avec moins de conditions
if (verificationStatus === 'verified' && metrics.confidence > this.THRESHOLDS.HIGH_CONFIDENCE_THRESHOLD) {
rawScore *= 1.1;
} else if (verificationStatus === 'absence_of_information') {
rawScore = rawScore > 0.75 ? 0.75 : rawScore;
}
// Lissage temporel si score précédent disponible
if (previousScore !== undefined) {
rawScore = 0.7 * rawScore + 0.3 * previousScore;
}
// OPTIMISATION: Normalisation finale directe
if (rawScore < this.THRESHOLDS.MIN_RELIABILITY) {
return this.THRESHOLDS.MIN_RELIABILITY;
} else if (rawScore > this.THRESHOLDS.MAX_RELIABILITY) {
return this.THRESHOLDS.MAX_RELIABILITY;
}
return rawScore;
}
/**
* Calcule un score de pertinence basé sur la correspondance avec le contexte - VERSION OPTIMISÉE
*
* @param thought La pensée à évaluer
* @param context Le contexte actuel
* @returns Score de pertinence (0-1)
*/
calculateRelevanceScore(thought: ThoughtNode, context: string): number {
// OPTIMISATION: Extraction des mots-clés
const thoughtKeywords = this.extractKeywords(thought.content);
const contextKeywordWeights = this.extractAndWeightContextKeywords(context);
let relevanceScore = 0;
let totalWeight = 0;
// OPTIMISATION: Boucle unique pour calculer le score
for (const keyword of thoughtKeywords) {
const weight = contextKeywordWeights[keyword];
if (weight) {
relevanceScore += weight;
totalWeight += weight;
}
}
// Normaliser le score
if (totalWeight > 0) {
relevanceScore = relevanceScore / totalWeight;
} else {
// Aucune correspondance, score minimal
relevanceScore = this.THRESHOLDS.MIN_RELEVANCE;
}
// OPTIMISATION: Pondérer en fonction des connexions et du type de pensée
let connectionFactor = 1.0;
// Bonus pour les connexions fortes avec d'autres pensées pertinentes
if (thought.connections && thought.connections.length > 0) {
let connectionScore = 0;
for (const connection of thought.connections) {
// OPTIMISATION: Utiliser Map
const typeWeight = this.connectionWeightsMap.get(connection.type) || 0.5;
connectionScore += typeWeight * connection.strength;
}
connectionFactor += (connectionScore / thought.connections.length) * 0.5;
}
relevanceScore *= connectionFactor;
// OPTIMISATION: Normaliser entre MIN et MAX directement
if (relevanceScore < this.THRESHOLDS.MIN_RELEVANCE) {
return this.THRESHOLDS.MIN_RELEVANCE;
} else if (relevanceScore > this.THRESHOLDS.MAX_RELEVANCE) {
return this.THRESHOLDS.MAX_RELEVANCE;
}
return relevanceScore;
}
/**
* Extrait les mots-clés d'un texte donné - VERSION OPTIMISÉE
*
* @param text Le texte à analyser
* @returns Un tableau de mots-clés
*/
extractKeywords(text: string): string[] {
// OPTIMISATION: Convertir en minuscules et supprimer la ponctuation
const processedText = text.toLowerCase().replace(this.REGEX.PUNCTUATION, '');
// OPTIMISATION: Utiliser Map pour le comptage (plus efficace)
const wordCountsMap = new Map<string, number>();
// OPTIMISATION: Diviser et traiter en une seule passe
const words = processedText.split(this.REGEX.WHITESPACE);
for (const word of words) {
if (word.length > 2 && !this.stopWords.has(word)) {
wordCountsMap.set(word, (wordCountsMap.get(word) || 0) + 1);
}
}
// OPTIMISATION: Convertir en tableau et trier
const sortedEntries = Array.from(wordCountsMap.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 15);
// Extraire les mots uniquement
return sortedEntries.map(entry => entry[0]);
}
/**
* Extrait et pondère les mots-clés du contexte - VERSION OPTIMISÉE
*
* @param text Le texte du contexte
* @returns Un objet avec les mots-clés et leurs poids
*/
extractAndWeightContextKeywords(text: string): Record<string, number> {
const keywords = this.extractKeywords(text);
const weightedKeywords: Record<string, number> = {};
// OPTIMISATION: Calculer la longueur une seule fois
const keywordsLength = keywords.length;
const factor = 1 / (keywordsLength * 2);
// OPTIMISATION: Assigner des poids en une seule passe
for (let i = 0; i < keywordsLength; i++) {
const keyword = keywords[i];
weightedKeywords[keyword] = 1 - (i * factor);
}
return weightedKeywords;
}
/**
* Obtient le poids associé à un type de connexion - VERSION OPTIMISÉE
*
* @param type Le type de connexion
* @returns Le poids de ce type de connexion
*/
getConnectionTypeWeight(type: ConnectionType): number {
// OPTIMISATION: Utiliser Map pour un accès plus rapide
return this.connectionWeightsMap.get(type) || 0.5;
}
/**
* Détermine le statut de vérification en fonction du score de confiance
* et d'autres facteurs - VERSION OPTIMISÉE
*
* @param confidenceOrResults Niveau de confiance dans la vérification (0-1) ou résultats de vérification
* @param hasContradictions Indique s'il y a des contradictions
* @param hasInformation Indique s'il y a des informations disponibles
* @returns Le statut de vérification
*/
determineVerificationStatus(
confidenceOrResults: number | any[],
hasContradictions: boolean = false,
hasInformation: boolean = true
): VerificationStatus {
// Si le premier paramètre est un tableau, on est dans le cas d'un appel depuis verification-service
if (Array.isArray(confidenceOrResults)) {
return this.determineVerificationStatusFromResults(confidenceOrResults);
}
// OPTIMISATION: Logic simplifiée avec retours immédiats
const confidence = confidenceOrResults as number;
// Absence d'information
if (!hasInformation) {
return 'absence_of_information';
}
// Contradictions
if (hasContradictions) {
return confidence < this.THRESHOLDS.CONTRADICTION_THRESHOLD
? 'contradictory'
: 'uncertain';
}
// Vérification normale basée sur le niveau de confiance
if (confidence >= this.THRESHOLDS.VERIFIED_THRESHOLD) {
return 'verified';
} else if (confidence >= this.THRESHOLDS.PARTIALLY_VERIFIED_THRESHOLD) {
return 'partially_verified';
}
return 'unverified';
}
/**
* Détermine le statut de vérification à partir des résultats de multiples sources - VERSION OPTIMISÉE
* Algorithme amélioré pour gérer les cas ambigus
*
* @param results Résultats de vérification provenant de différentes sources
* @returns Statut de vérification (VerificationStatus)
*/
private determineVerificationStatusFromResults(results: any[]): VerificationStatus {
if (!results || results.length === 0) {
return 'unverified';
}
// OPTIMISATION: Comptage direct sans créer des objets intermédiaires
let verifiedCount = 0;
let contradictedCount = 0;
let uncertainCount = 0;
let absenceCount = 0;
let totalConfidence = 0;
// OPTIMISATION: Une seule passe pour tous les comptages
for (const result of results) {
const confidence = result.confidence || 0.5;
totalConfidence += confidence;
const isValid = result.result?.isValid;
if (isValid === true) {
verifiedCount += confidence;
} else if (isValid === false) {
contradictedCount += confidence;
} else if (isValid === 'uncertain') {
uncertainCount += confidence;
} else if (isValid === 'absence_of_information') {
absenceCount += confidence;
}
}
// Calcul des ratios
const verifiedRatio = verifiedCount / totalConfidence;
const contradictedRatio = contradictedCount / totalConfidence;
const uncertainRatio = uncertainCount / totalConfidence;
const absenceRatio = absenceCount / totalConfidence;
// OPTIMISATION: Logique de décision simplifiée avec retours immédiats
// 1. Contradiction forte prend priorité
if (contradictedRatio > 0.5) {
return 'contradictory';
}
// 2. Désaccord franc entre vérifié et contredit
if (verifiedRatio > 0.3 && contradictedRatio > 0.3) {
return 'uncertain';
}
// 3. Vérification forte si le ratio est suffisant
if (verifiedRatio > this.THRESHOLDS.VERIFIED_THRESHOLD) {
return 'verified';
}
// 4. Vérification partielle pour les cas intermédiaires
if (verifiedRatio > this.THRESHOLDS.PARTIALLY_VERIFIED_THRESHOLD) {
return 'partially_verified';
}
// 5. Cas d'incertitude forte
if (uncertainRatio > this.THRESHOLDS.UNCERTAIN_THRESHOLD) {
return 'uncertain';
}
// 6. Absence d'information comme cas particulier
if (absenceRatio > this.THRESHOLDS.ABSENCE_THRESHOLD) {
return 'absence_of_information';
}
// 7. Par défaut, vérification partielle si au moins quelques éléments positifs
if (verifiedRatio > 0) {
return 'partially_verified';
}
// 8. Sinon, considérer comme non vérifié
return 'unverified';
}
/**
* Génère un résumé explicatif du niveau de certitude - VERSION OPTIMISÉE
*
* @param status Statut de vérification
* @param confidence Niveau de confiance (0-1)
* @returns Résumé textuel du niveau de certitude
*/
generateCertaintySummary(status: VerificationStatus, confidence: number = 0.5): string {
// Formater le pourcentage pour l'affichage
const percentage = Math.round(confidence * 100);
// OPTIMISATION: Utiliser un modèle de chaîne de base et personnaliser selon le statut
const statusDescriptions: Record<VerificationStatus, string> = {
'verified': `Information vérifiée avec un niveau de confiance de ${percentage}%. Plusieurs sources fiables confirment cette information.`,
'partially_verified': `Information partiellement vérifiée avec un niveau de confiance de ${percentage}%. Certains éléments sont confirmés par des sources fiables.`,
'unverified': `Information non vérifiée. Niveau de confiance: ${percentage}%. Aucune source ne confirme ou n'infirme cette information.`,
'contradicted': `Information contredite. Niveau de confiance: ${percentage}%. Des sources fiables contredisent cette information.`,
'contradictory': `Information contradictoire. Niveau de confiance: ${percentage}%. Des sources crédibles se contredisent sur ce sujet.`,
'absence_of_information': `Aucune information trouvée sur ce sujet. Niveau de confiance: ${percentage}%. Cette absence d'information est elle-même une information pertinente.`,
'uncertain': `Information incertaine. Niveau de confiance: ${percentage}%. Les sources disponibles ne permettent pas de conclure avec certitude.`,
'inconclusive': `Résultat non concluant. Niveau de confiance: ${percentage}%. Les données sont insuffisantes ou ambiguës pour tirer une conclusion définitive.`
};
let summary = statusDescriptions[status] || `Niveau de confiance: ${percentage}%.`;
// Ajouter des conseils supplémentaires selon le niveau de confiance
if (confidence < 0.3) {
summary += ' Cette information doit être considérée comme hautement spéculative.';
} else if (confidence > 0.85) {
summary += ' Cette information peut être considérée comme fiable.';
}
return summary;
}
evaluateVerificationHeuristics(thought: ThoughtNode): { status: VerificationStatus; confidence: number; notes: string; keyFactors: string[] } {
const lower = thought.content.toLowerCase();
const keyFactors: string[] = [];
let status: VerificationStatus = 'unverified';
let confidence = 0.45;
const hasNumbers = /\d/.test(lower);
const hasFactualClaim = this.REGEX.FACTUAL_CLAIM.test(lower);
if (hasNumbers || hasFactualClaim) {
keyFactors.push('Présence d\'éléments factuels nécessitant une confirmation.');
confidence = Math.max(confidence, 0.5);
}
const hasSpeculation = this.hasAny(lower, this.uncertaintyModifiers);
const hasAbsolute = /(toujours|jamais|impossible|certainement|indiscutablement|absolument)/i.test(lower);
const hasStrongEmotion = this.REGEX.STRONG_EMOTION.test(lower);
if (hasAbsolute && hasSpeculation) {
status = 'contradicted';
confidence = Math.max(confidence, 0.55);
keyFactors.push('Combinaison de certitudes absolues et de formulations spéculatives.');
} else if (hasAbsolute && !hasSpeculation && !hasStrongEmotion) {
status = 'verified';
confidence = Math.max(confidence, 0.6);
keyFactors.push('Affirmations catégoriques sans marqueurs d\'incertitude.');
} else if (!hasNumbers && hasSpeculation) {
status = 'uncertain';
confidence = Math.min(confidence, 0.48);
keyFactors.push('Énoncé spéculatif sans soutien factuel explicite.');
}
if (hasStrongEmotion) {
keyFactors.push('Langage émotionnel détecté, à vérifier avec prudence.');
confidence = Math.min(confidence, 0.5);
}
if (keyFactors.length === 0) {
keyFactors.push('Aucun indice heuristique fort détecté.');
}
const notes = keyFactors.join(' ');
return {
status,
confidence: this.clamp(confidence, 0.2, 0.85),
notes,
keyFactors
};
}
/**
* Détecte les biais potentiels dans une pensée - VERSION OPTIMISÉE
*
* @param thought La pensée à analyser
* @returns Un tableau des biais détectés avec leur score (0-1) (basé sur LLM ou heuristique)
*/
async detectBiases(thought: ThoughtNode): Promise<Array<{type: string, score: number, description: string}>> {
const content = thought.content.toLowerCase();
const biases = [];
// OPTIMISATION: Liste des patterns de biais cognitifs courants
const biasPatterns = [
{
type: 'confirmation_bias',
regex: /je savais déjà|comme prévu|confirme que|toujours été|évidemment/gi,
patterns: ['je savais déjà', 'comme prévu', 'confirme que', 'toujours été', 'évidemment'],
description: 'Tendance à favoriser les informations qui confirment des croyances préexistantes'
},
{
type: 'recency_bias',
regex: /récemment|dernièrement|ces jours-ci|tendance actuelle|de nos jours/gi,
patterns: ['récemment', 'dernièrement', 'ces jours-ci', 'tendance actuelle', 'de nos jours'],
description: 'Tendance à donner plus d\'importance aux événements récents'
},
{
type: 'availability_heuristic',
regex: /souvent|fréquemment|généralement|habituellement|couramment/gi,
patterns: ['souvent', 'fréquemment', 'généralement', 'habituellement', 'couramment'],
description: 'Jugement basé sur des exemples qui viennent facilement à l\'esprit'
},
{
type: 'black_white_thinking',
regex: /toujours|jamais|impossible|absolument|parfaitement|totalement/gi,
patterns: ['toujours', 'jamais', 'impossible', 'absolument', 'parfaitement', 'totalement'],
description: 'Tendance à voir les choses en termes absolus sans nuances'
},
{
type: 'authority_bias',
regex: /expert dit|selon les experts|études montrent|scientifiquement prouvé/gi,
patterns: ['expert dit', 'selon les experts', 'études montrent', 'scientifiquement prouvé'],
description: 'Tendance à attribuer plus de poids aux opinions des figures d\'autorité'
}
];
// OPTIMISATION: Détecter les biais avec RegExp
for (const bias of biasPatterns) {
const matches = content.match(bias.regex);
if (matches && matches.length > 0) {
// Calculer un score basé sur le nombre de correspondances
const score = Math.min(matches.length / bias.patterns.length * 1.5, 1);
if (score > 0.2) { // Seuil minimum pour considérer un biais
biases.push({
type: bias.type,
score,
description: bias.description
});
}
}
}
// Analyse de sentiment pour détecter le biais émotionnel
if (this.REGEX.STRONG_EMOTION.test(content)) {
biases.push({
type: 'emotional_bias',
score: 0.7,
description: 'Jugement influencé par une forte charge émotionnelle'
});
}
return biases;
}
/**
* Détermine les besoins de vérification pour un contenu donné - VERSION OPTIMISÉE
*
* @param content Le contenu textuel à analyser
* @returns Configuration recommandée pour la vérification (basée sur LLM ou heuristique)
*/
async determineVerificationRequirements(content: string): Promise<{
needsFactCheck: boolean;
needsMathCheck: boolean;
needsSourceCheck: boolean;
priority: 'low' | 'medium' | 'high';
suggestedTools: string[];
requiresMultipleVerifications: boolean;
reasons: string[];
recommendedVerificationsCount: number;
}> {
const lowercaseContent = content.toLowerCase();
// Initialiser les résultats
const result = {
needsFactCheck: false,
needsMathCheck: false,
needsSourceCheck: false,
priority: 'low' as 'low' | 'medium' | 'high',
suggestedTools: [] as string[],
requiresMultipleVerifications: false,
reasons: [] as string[],
recommendedVerificationsCount: 1
};
let score = 0;
if (/\d/.test(lowercaseContent)) {
result.needsFactCheck = true;
if (!result.reasons.includes('Présence de données chiffrées.')) {
result.reasons.push('Présence de données chiffrées.');
}
if (!result.suggestedTools.includes('web_search')) {
result.suggestedTools.push('web_search');
}
score += 2;
}
// OPTIMISATION: Détection par RegExp
// Détection des affirmations factuelles
if (this.REGEX.FACTUAL_CLAIM.test(content)) {
result.needsFactCheck = true;
result.suggestedTools.push('web_search');
result.reasons.push('Contient des affirmations factuelles');
score += 1;
}
// Détection des calculs mathématiques
if (this.REGEX.MATH_CALCULATION.test(content) ||
this.REGEX.MATHEMATICAL_PROOF.test(content)) {
result.needsMathCheck = true;
result.suggestedTools.push('math_evaluator');
result.reasons.push('Contient des calculs ou preuves mathématiques');
}
// OPTIMISATION: Utiliser RegExp pour détecter les références à des sources
const sourceRegex = /selon|d'après|source|cité|référence|étude|recherche|publication/i;
if (sourceRegex.test(content)) {
result.needsSourceCheck = true;
result.suggestedTools.push('citation_checker');
result.reasons.push('Contient des références à des sources');
}
// OPTIMISATION: Détermination de la priorité avec RegExp
// Augmenter le score si contient des affirmations fortes
if (/certainement|absolument|sans aucun doute|clairement|évidemment|forcément/i.test(lowercaseContent)) {
score += 2;
result.reasons.push('Contient des affirmations fortes');
}
// Augmenter le score si contient des statistiques ou chiffres précis
if (/\d+\s?%|\d+\.\d+|millions?|milliards?/i.test(lowercaseContent)) {
score += 2;
result.reasons.push('Contient des statistiques ou chiffres précis');
result.needsFactCheck = true;
if (!result.suggestedTools.includes('web_search')) {
result.suggestedTools.push('web_search');
}
}
// Augmenter le score si contient des références temporelles récentes
if (/récemment|cette année|ce mois|cette semaine|aujourd'hui|actuellement/i.test(lowercaseContent)) {
score += 1;
result.reasons.push('Contient des références temporelles récentes');
}
if (score >= 4) {
result.priority = 'high';
} else if (score >= 2) {
result.priority = 'medium';
}
// OPTIMISATION: Déterminer si plusieurs vérifications sont nécessaires
// Basé sur la complexité et l'importance du contenu
result.requiresMultipleVerifications =
(result.needsFactCheck && (result.needsMathCheck || result.needsSourceCheck)) ||
(result.priority === 'high');
// OPTIMISATION: Calculer le nombre de vérifications recommandées directement
if (result.requiresMultipleVerifications) {
result.recommendedVerificationsCount =
(result.needsFactCheck && result.needsMathCheck && result.needsSourceCheck) ? 3 : 2;
}
return result;
}
/**
* Calcule un niveau de confiance pour un ensemble de résultats de vérification - VERSION OPTIMISÉE
* en utilisant une approche bayésienne
*
* @param results Résultats de vérification
* @returns Score de confiance entre 0 et 1
*/
calculateVerificationConfidence(results: any[]): number {
if (!results || results.length === 0) {
return 0.5; // Confiance neutre par défaut
}
// OPTIMISATION: Facteurs de pondération pour différentes sources
const sourceTypeWeights = new Map([
['search', 0.8],
['database', 0.9],
['calculation', 0.95],
['external_api', 0.85]
]);
const defaultWeight = 0.7;
// OPTIMISATION: Compter directement sans objets intermédiaires
let positiveWeight = 0;
let negativeWeight = 0;
// OPTIMISATION: Analyser chaque résultat en une seule passe
for (const result of results) {
// Déterminer le poids de cette source
const sourceWeight = sourceTypeWeights.get(result.toolType) || defaultWeight;
const confidence = result.confidence || 0.5;
// Ajuster la confiance en fonction de la validité
if (result.result?.isValid === true) {
positiveWeight += sourceWeight * confidence;
} else if (result.result?.isValid === false) {
negativeWeight += sourceWeight * confidence;
}
}
// Si aucun résultat positif ou négatif clair, retourner une confiance neutre
if (positiveWeight === 0 && negativeWeight === 0) {
return 0.5;
}
// OPTIMISATION: Calculer le ratio de vraisemblance bayésien directement
const likelihoodRatio = (positiveWeight + 0.5) / (negativeWeight + 0.5);
// Convertir les odds en probabilité
let confidence = likelihoodRatio / (1 + likelihoodRatio);
// Bonus pour le nombre de sources consultées
const sourceCountBonus = Math.min(results.length * 0.05, 0.2);
confidence = Math.min(confidence + sourceCountBonus, 0.95);
// Limite inférieure pour éviter une confiance trop basse
return Math.max(confidence, 0.2);
}
}