UNPKG

smart-thinking-mcp

Version:

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

615 lines 26 kB
"use strict"; /** * math-evaluator.ts * * Module optimisé pour la détection et l'évaluation performante d'expressions mathématiques dans un texte. * Utilise l'algorithme Shunting-Yard, une mise en cache des résultats, et des tables de lookup pour les opérateurs. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.MathEvaluator = void 0; /** * Classe utilitaire optimisée pour la détection et l'évaluation d'expressions mathématiques */ class MathEvaluator { /** * Cache pour les expressions évaluées * @private */ static expressionCache = new Map(); /** * Types d'expressions mathématiques supportées (précompilées) * @private */ static EXPRESSION_TYPES = { // Expressions arithmétiques standard (ex: 2 + 3 = 5) STANDARD: /(\d+(?:\.\d+)?)(?:\s*[\+\-\*\/\^]\s*\d+(?:\.\d+)?)+(?:\s*[\+\-\*\/\^]\s*\d+(?:\.\d+)?)*\s*(?:=|égale?|est égal à|vaut|font|donne)\s*(\d+(?:\.\d+)?)/i, // Expressions avec parenthèses (ex: (2 + 3) * 4 = 20) PARENTHESES: /\([\d\s\+\-\*\/\^\.]+\)(?:\s*[\+\-\*\/\^]\s*(?:\d+(?:\.\d+)?|\([\d\s\+\-\*\/\^\.]+\)))*\s*(?:=|égale?|est égal à|vaut|font|donne)\s*(\d+(?:\.\d+)?)/i, // Expressions textuelles (ex: 2 plus 3 égale 5) TEXTUAL: /(\d+(?:\.\d+)?)(?:\s*(?:plus|moins|fois|divisé par|multiplié par)\s*(?:\d+(?:\.\d+)?))(?:\s*(?:plus|moins|fois|divisé par|multiplié par)\s*(?:\d+(?:\.\d+)?))*\s*(?:=|égale?|est égal à|vaut|font|donne)\s*(\d+(?:\.\d+)?)/i, // Fonctions mathématiques (ex: racine carrée de 9 = 3, ou 2 au carré = 4) FUNCTIONS: /(?:(?:racine\s+carrée\s+(?:de)?\s*(\d+(?:\.\d+)?))|(?:(?:\d+(?:\.\d+)?)\s+au\s+(?:carré|cube)))\s*(?:=|égale?|est égal à|vaut|font|donne)\s*(\d+(?:\.\d+)?)/i, // Expressions séquentielles avec plusieurs étapes (ex: 2³ - 2×2 - 5 = 8 - 4 - 5 = -1) SEQUENTIAL: /(?:\d+(?:\.\d+)?(?:[\³\²\¹])?(?:\s*[\+\-\*×\/\^]\s*\d+(?:\.\d+)?(?:[\³\²\¹])?)+)\s*=\s*(?:\d+(?:\.\d+)?(?:\s*[\+\-\*×\/\^]\s*\d+(?:\.\d+)?)*)\s*(?:=\s*(?:\d+(?:\.\d+)?))+/i }; /** * Expression régulière pour détecter les notations de fonctions (précompilée) * @private */ static FUNCTION_NOTATION_REGEX = /[a-zA-Z]['\(\)\d₀₁₂₃₄₅₆₇₈₉]*\s*=\s*[a-zA-Z]['\(\)\d\.]+\s*=\s*[^=]+\s*=\s*[\d\.\-+]+/i; /** * Expression régulière pour extraire les revendications de résultat (précompilée) * @private */ static CLAIMED_RESULT_REGEX = /(?:=|égale?|est égal à|vaut|font|donne)\s*([\-\+]?\d+(?:\.\d+)?)/i; /** * Seuil de tolérance relatif pour comparer les nombres * @private */ static RELATIVE_EPSILON = 1e-10; /** * Seuil de tolérance absolu minimal pour les petits nombres * @private */ static ABSOLUTE_EPSILON = 1e-12; /** * Table de lookup pour la précédence des opérateurs dans l'algorithme Shunting-Yard * @private */ static OPERATOR_PRECEDENCE = { '+': 1, '-': 1, '*': 2, '/': 2, '^': 3 }; /** * Table de lookup pour l'associativité des opérateurs dans l'algorithme Shunting-Yard * @private */ static OPERATOR_ASSOCIATIVITY = { '+': 'left', '-': 'left', '*': 'left', '/': 'left', '^': 'right' }; /** * Table de lookup pour les fonctions d'opération dans l'algorithme Shunting-Yard * @private */ static OPERATOR_FUNCTIONS = { '+': (a, b) => a + b, '-': (a, b) => a - b, '*': (a, b) => a * b, '/': (a, b) => { if (Math.abs(b) < MathEvaluator.ABSOLUTE_EPSILON) { throw new Error("Division par zéro"); } return a / b; }, '^': (a, b) => Math.pow(a, b) }; /** * Détecte et évalue toutes les expressions mathématiques dans un texte * * @param text Texte à analyser * @returns Tableau des résultats d'évaluation pour chaque expression trouvée */ static detectAndEvaluate(text) { const results = []; // Filtrer les notations de fonctions mathématiques const functionNotations = this.detectFunctionNotations(text); for (const notation of functionNotations) { results.push({ original: notation, expressionText: "Notation de fonction", result: NaN, isCorrect: true, // On considère que c'est correct car ce n'est pas un calcul à vérifier claimedResult: NaN, confidence: 0.95, context: "notation_fonction" }); } // Exclure les parties détectées comme notations de fonctions let analysisText = text; for (const notation of functionNotations) { analysisText = analysisText.replace(notation, ' '.repeat(notation.length)); } // Rechercher par type d'expression dans le texte filtré this.detectExpressionsOfType(analysisText, 'STANDARD', results); this.detectExpressionsOfType(analysisText, 'PARENTHESES', results); this.detectExpressionsOfType(analysisText, 'TEXTUAL', results); this.detectExpressionsOfType(analysisText, 'FUNCTIONS', results); // Détecter et évaluer les expressions séquentielles this.detectSequentialExpressions(analysisText, results); return results; } /** * Détecte les notations de fonctions mathématiques * qui ne doivent pas être évaluées comme des calculs * * @param text Texte à analyser * @returns Tableau des notations de fonctions trouvées * @private */ static detectFunctionNotations(text) { const notations = []; // Rechercher les motifs comme "f(x₀) = f(2) = ..." const functionPattern = /(?:[a-zA-Z]['\(\)\d₀₁₂₃₄₅₆₇₈₉]*\s*=\s*[a-zA-Z][\'\(\)\d\.]+)|(?:[a-zA-Z]\'?\([\w₀₁₂₃₄₅₆₇₈₉\.]+\)\s*=\s*[a-zA-Z]\'?\([^)]+\))/g; let match; while ((match = functionPattern.exec(text)) !== null) { notations.push(match[0]); } return notations; } /** * Détecte et évalue les expressions séquentielles * (expressions avec plusieurs égalités en chaîne) * * @param text Texte à analyser * @param results Tableau des résultats à compléter * @private */ static detectSequentialExpressions(text, results) { // Regex pour trouver des expressions comme "2³ - 2×2 - 5 = 8 - 4 - 5 = -1" const sequentialPattern = /(\d+(?:[\³\²\¹])?\s*(?:[\+\-\*×\/]\s*\d+(?:[\³\²\¹])?)+)\s*=\s*([^=]+)\s*=\s*([^=]+)(?:\s*=\s*([^=]+))?/g; let match; while ((match = sequentialPattern.exec(text)) !== null) { try { const fullMatch = match[0]; const parts = fullMatch.split('=').map(part => part.trim()); // Vérifier que nous avons au moins 2 parties if (parts.length < 2) continue; // Le résultat final est la dernière partie const finalResult = parseFloat(parts[parts.length - 1]); // Si le résultat final n'est pas un nombre, ignorer if (isNaN(finalResult)) continue; // Évaluer la première expression pour vérifier la chaîne const firstExpression = this.convertSequentialExpression(parts[0]); const firstResult = this.safeEvaluate(firstExpression); let isCorrect = true; let failedStep = ""; // Vérifier chaque étape intermédiaire for (let i = 1; i < parts.length - 1; i++) { const intermediateResult = parseFloat(parts[i]); // Si l'étape intermédiaire n'est pas un nombre, ignorer if (isNaN(intermediateResult)) { // Essayer d'évaluer l'expression si ce n'est pas un simple nombre const intermediateExpr = this.convertSequentialExpression(parts[i]); const evalResult = this.safeEvaluate(intermediateExpr); // Vérifier avec la partie précédente if (i === 1 && !this.areNumbersEqual(firstResult, evalResult)) { isCorrect = false; failedStep = `Étape ${i}: ${firstResult}${evalResult}`; break; } } else if (i === 1 && !this.areNumbersEqual(firstResult, intermediateResult)) { isCorrect = false; failedStep = `Étape ${i}: ${firstResult}${intermediateResult}`; break; } } results.push({ original: fullMatch, expressionText: firstExpression, result: firstResult, isCorrect: isCorrect && this.areNumbersEqual(firstResult, finalResult), claimedResult: finalResult, confidence: 0.9, context: isCorrect ? undefined : failedStep }); } catch (error) { console.error(`Erreur lors de l'évaluation d'une expression séquentielle: ${error}`); } } } /** * Convertit une expression séquentielle en expression évaluable * * @param expr Expression à convertir * @returns Expression prête pour l'évaluation * @private */ static convertSequentialExpression(expr) { // Remplacer les puissances Unicode let result = expr .replace(/(\d+)[\³]/g, '$1**3') .replace(/(\d+)[\²]/g, '$1**2') .replace(/(\d+)[\¹]/g, '$1**1') .replace(/[×]/g, '*'); return this.sanitizeExpression(result); } /** * Détecte et évalue les expressions d'un type spécifique dans un texte * * @param text Texte à analyser * @param type Type d'expression à rechercher * @param results Tableau des résultats à compléter * @private */ static detectExpressionsOfType(text, type, results) { const regex = this.EXPRESSION_TYPES[type]; // Utiliser exec de manière répétée pour trouver toutes les occurrences const matches = []; const textCopy = text.slice(); const regexWithGlobal = new RegExp(regex.source, regex.flags + (regex.flags.includes('g') ? '' : 'g')); let match; while ((match = regexWithGlobal.exec(textCopy)) !== null) { matches.push(match); if (regexWithGlobal.lastIndex === match.index) { regexWithGlobal.lastIndex++; } } for (const match of matches) { try { const fullMatch = match[0]; // Vérifier si cette expression est une notation de fonction if (this.isFunctionNotation(fullMatch)) { continue; // Ignorer les notations de fonctions } // Extraire le résultat revendiqué const claimedResultMatch = this.CLAIMED_RESULT_REGEX.exec(fullMatch); if (!claimedResultMatch) continue; const claimedResultStr = claimedResultMatch[1]; const claimedResult = parseFloat(claimedResultStr); // Déterminer l'expression à évaluer selon le type let expressionToEvaluate = ''; let confidence = 0.99; switch (type) { case 'STANDARD': expressionToEvaluate = this.extractStandardExpression(fullMatch); break; case 'PARENTHESES': expressionToEvaluate = this.extractParenthesesExpression(fullMatch); break; case 'TEXTUAL': expressionToEvaluate = this.convertTextToMathExpression(fullMatch); confidence = 0.95; break; case 'FUNCTIONS': expressionToEvaluate = this.extractFunctionExpression(fullMatch); confidence = 0.97; break; } if (!expressionToEvaluate) continue; // Évaluer l'expression de manière sécurisée avec l'algorithme Shunting-Yard optimisé const actualResult = this.safeEvaluate(expressionToEvaluate); // Vérifier si le résultat correspond à celui revendiqué const isCorrect = this.areNumbersEqual(actualResult, claimedResult); results.push({ original: fullMatch, expressionText: expressionToEvaluate, result: actualResult, isCorrect, claimedResult, confidence: isCorrect ? confidence : (confidence * 0.8) }); } catch (error) { console.error(`Erreur lors de l'évaluation mathématique: ${error}`); const claimedResultMatch = this.CLAIMED_RESULT_REGEX.exec(match[0]); const claimedResult = claimedResultMatch ? parseFloat(claimedResultMatch[1]) : 0; results.push({ original: match[0], expressionText: '', result: NaN, isCorrect: false, claimedResult, confidence: 0.3, context: "erreur_evaluation" }); } } } /** * Vérifie si une expression est une notation de fonction mathématique * * @param expr Expression à vérifier * @returns Vrai si c'est une notation de fonction * @private */ static isFunctionNotation(expr) { // Vérifier les motifs comme "f(x)" ou "f'(x)" return /^[a-zA-Z]'?\([^)]+\)\s*=/.test(expr) || /\s[a-zA-Z]'?\([^)]+\)\s*=/.test(expr); } /** * Compare deux nombres en tenant compte des erreurs d'arrondi * * @param a Premier nombre * @param b Deuxième nombre * @returns Vrai si les nombres sont considérés égaux * @private */ static areNumbersEqual(a, b) { const diff = Math.abs(a - b); if (Math.abs(a) < this.ABSOLUTE_EPSILON || Math.abs(b) < this.ABSOLUTE_EPSILON) { return diff < this.ABSOLUTE_EPSILON; } const relativeEpsilon = this.RELATIVE_EPSILON * Math.max(Math.abs(a), Math.abs(b)); return diff < Math.max(this.ABSOLUTE_EPSILON, relativeEpsilon); } /** * Convertit les résultats d'évaluation au format CalculationVerificationResult * utilisé par le système de vérification * * @param evaluationResults Résultats d'évaluation à convertir * @returns Liste des résultats de vérification de calculs */ static convertToVerificationResults(evaluationResults) { return evaluationResults.map(result => { // Traitement spécial pour les notations de fonctions if (result.context === "notation_fonction") { return { original: result.original, verified: "Notation de fonction mathématique (non évaluée)", isCorrect: true, // On ne vérifie pas les notations confidence: 0.95 }; } // Message amélioré pour les erreurs d'étapes dans les expressions séquentielles if (result.context && result.context.startsWith("Étape")) { return { original: result.original, verified: `Calcul incorrect. Erreur à ${result.context}`, isCorrect: false, confidence: 0.85 }; } return { original: result.original, verified: result.isCorrect ? `${result.expressionText} = ${result.result}` : `Calcul incorrect. ${result.expressionText} = ${result.result}, pas ${result.claimedResult}`, isCorrect: result.isCorrect, confidence: result.confidence }; }); } /** * Extrait l'expression standard à partir d'une expression complète * * @param expr Expression complète * @returns Expression standard * @private */ static extractStandardExpression(expr) { const parts = expr.split(/(?:=|égale?|est égal à|vaut|font|donne)/i); if (parts.length < 1) return ''; return this.sanitizeExpression(parts[0]); } /** * Extrait l'expression avec parenthèses à partir d'une expression complète * * @param expr Expression complète * @returns Expression avec parenthèses * @private */ static extractParenthesesExpression(expr) { const parts = expr.split(/(?:=|égale?|est égal à|vaut|font|donne)/i); if (parts.length < 1) return ''; return this.sanitizeExpression(parts[0]); } /** * Extrait l'expression de fonction à partir d'une expression complète * * @param expr Expression complète * @returns Expression de fonction * @private */ static extractFunctionExpression(expr) { const sqrtMatch = expr.match(/racine\s+carrée\s+(?:de)?\s*(\d+(?:\.\d+)?)/i); if (sqrtMatch) { return `Math.sqrt(${sqrtMatch[1]})`; } const squareMatch = expr.match(/(\d+(?:\.\d+)?)\s+au\s+carré/i); if (squareMatch) { return `Math.pow(${squareMatch[1]}, 2)`; } const cubeMatch = expr.match(/(\d+(?:\.\d+)?)\s+au\s+cube/i); if (cubeMatch) { return `Math.pow(${cubeMatch[1]}, 3)`; } return ''; } /** * Nettoie une expression mathématique pour l'évaluation * * @param expr Expression à nettoyer * @returns Expression nettoyée * @private */ static sanitizeExpression(expr) { return expr.replace(/[^\d\s().+\-*/^]/g, '') .trim() .replace(/\^/g, '**'); } /** * Convertit une expression textuelle en expression mathématique * * @param textExpr Expression textuelle * @returns Expression mathématique * @private */ static convertTextToMathExpression(textExpr) { const parts = textExpr.split(/(?:=|égale?|est égal à|vaut|font|donne)/i); if (parts.length < 1) return ''; return parts[0] .replace(/\s+plus\s+/gi, ' + ') .replace(/\s+moins\s+/gi, ' - ') .replace(/\s+fois\s+/gi, ' * ') .replace(/\s+divisé\s+par\s+/gi, ' / ') .replace(/\s+multiplié\s+par\s+/gi, ' * ') .replace(/\s+au\s+carré/gi, ' ** 2 ') .replace(/\s+au\s+cube/gi, ' ** 3 '); } /** * Évalue une expression de manière sécurisée * Utilise la mise en cache et l'algorithme Shunting-Yard pour les expressions normales * * @param expr Expression à évaluer * @returns Résultat de l'évaluation * @private */ static safeEvaluate(expr) { if (expr.includes('Math.')) { return this.evaluateMathExpression(expr); } // Normaliser l'expression pour le cache const cleanExpr = expr.replace(/\s+/g, '').replace(/\*\*/g, '^'); // Vérifier si l'expression est dans le cache if (this.expressionCache.has(cleanExpr)) { return this.expressionCache.get(cleanExpr); } try { // Évaluer avec l'algorithme Shunting-Yard const result = this.evaluateWithShuntingYard(cleanExpr); // Mettre en cache le résultat this.expressionCache.set(cleanExpr, result); return result; } catch (error) { throw new Error(`Impossible d'évaluer l'expression: ${expr}`); } } /** * Évalue une expression Math.* de manière sécurisée * * @param expr Expression Math.* à évaluer * @returns Résultat de l'évaluation * @private */ static evaluateMathExpression(expr) { if (!/^Math\.(sqrt|pow|abs|round|floor|ceil|max|min|sin|cos|tan)\([\d\s,\.]+\)$/.test(expr)) { throw new Error("Expression Math non autorisée"); } const evalFunc = new Function('Math', `return ${expr};`); const result = evalFunc(Math); if (typeof result !== 'number' || isNaN(result) || !isFinite(result)) { throw new Error("Résultat n'est pas un nombre valide"); } return result; } /** * Implémentation de l'algorithme Shunting-Yard pour l'évaluation d'expressions * * @param expr Expression à évaluer * @returns Résultat de l'évaluation * @private */ static evaluateWithShuntingYard(expr) { // Vérifier que l'expression ne contient que des caractères autorisés if (!/^[\d\s().+\-*/^]+$/.test(expr)) { throw new Error("Expression contient des caractères non autorisés"); } // Tokenizer pour l'expression const tokens = []; let i = 0; while (i < expr.length) { if (/[0-9.]/.test(expr[i])) { // Extraire un nombre let number = ''; while (i < expr.length && /[0-9.]/.test(expr[i])) { number += expr[i++]; } tokens.push({ type: 'number', value: parseFloat(number) }); } else if (/[+\-*/^]/.test(expr[i])) { // Opérateur tokens.push({ type: 'operator', value: expr[i++] }); } else if (expr[i] === '(') { tokens.push({ type: 'left_paren', value: expr[i++] }); } else if (expr[i] === ')') { tokens.push({ type: 'right_paren', value: expr[i++] }); } else { // Ignorer les espaces et autres caractères non reconnus i++; } } // Piles pour l'algorithme Shunting-Yard const outputQueue = []; const operatorStack = []; // Algorithme Shunting-Yard pour convertir en notation polonaise inversée (RPN) for (const token of tokens) { if (token.type === 'number') { outputQueue.push(token.value); } else if (token.type === 'operator') { const o1 = token.value; while (operatorStack.length > 0) { const o2 = operatorStack[operatorStack.length - 1]; if (o2 === '(' || o2 === ')') break; if ((this.OPERATOR_ASSOCIATIVITY[o1] === 'left' && this.OPERATOR_PRECEDENCE[o1] <= this.OPERATOR_PRECEDENCE[o2]) || (this.OPERATOR_ASSOCIATIVITY[o1] === 'right' && this.OPERATOR_PRECEDENCE[o1] < this.OPERATOR_PRECEDENCE[o2])) { outputQueue.push(this.OPERATOR_FUNCTIONS[operatorStack.pop()]); } else { break; } } operatorStack.push(o1); } else if (token.type === 'left_paren') { operatorStack.push(token.value); } else if (token.type === 'right_paren') { while (operatorStack.length > 0 && operatorStack[operatorStack.length - 1] !== '(') { outputQueue.push(this.OPERATOR_FUNCTIONS[operatorStack.pop()]); } if (operatorStack.length > 0 && operatorStack[operatorStack.length - 1] === '(') { operatorStack.pop(); // Retirer la parenthèse gauche } else { throw new Error("Parenthèses déséquilibrées"); } } } while (operatorStack.length > 0) { const op = operatorStack.pop(); if (op === '(' || op === ')') { throw new Error("Parenthèses déséquilibrées"); } outputQueue.push(this.OPERATOR_FUNCTIONS[op]); } // Évaluer l'expression en notation polonaise inversée (RPN) const evaluationStack = []; for (const token of outputQueue) { if (typeof token === 'number') { evaluationStack.push(token); } else if (typeof token === 'function') { if (evaluationStack.length < 2) { throw new Error("Expression invalide: pas assez d'opérandes"); } const b = evaluationStack.pop(); const a = evaluationStack.pop(); try { evaluationStack.push(token(a, b)); } catch (error) { throw new Error(`Erreur lors de l'évaluation: ${error}`); } } } if (evaluationStack.length !== 1) { throw new Error("Expression invalide: trop d'opérandes"); } return evaluationStack[0]; } } exports.MathEvaluator = MathEvaluator; //# sourceMappingURL=math-evaluator.js.map