merithalle-ai / graphs /tools /calculator.py
Cyril Dupland
Add batch_calculator tool for multiple arithmetic operations and update A3 prompts to reflect new usage guidelines. Replace calculator tool with batch_calculator in workflows for improved efficiency in data analysis.
397ec1c
"""Calculator tool for mathematical operations."""
from langchain_core.tools import tool
@tool
def calculator(expression: str) -> str:
"""
Effectue des calculs arithmétiques (ratios, agrégations, comparaisons).
⚠️ NE PAS UTILISER pour :
- Les variations N vs N-1 (déjà fournies dans les données via "Var. : X%")
- Les ratios déjà calculés (champs pré-calculés dans les indicateurs)
✅ UTILISER uniquement pour :
- Calculs manuels demandés explicitement (ex: heures phytos / heures totales)
- Calculs inter-enjeux après récupération via get_site_indicators
- Agrégations spécifiques non disponibles dans les données
Exemples d'expressions valides :
- "850 / 4108" → ratio heures phytos / heures totales
- "150 + 200 + 75" → somme de valeurs
- "2317 / 2157" → ratio inter-indicateurs
Args:
expression: Expression mathématique à évaluer (ex: "850 / 4108")
Returns:
Résultat du calcul sous forme de chaîne, arrondi à 2 décimales
"""
try:
# Use numexpr for safe evaluation (no arbitrary code execution).
# If numexpr is installed but fails at runtime (env incompat), fallback to AST eval.
try:
import numexpr # type: ignore
result = numexpr.evaluate(expression).item()
# Round to 2 decimal places for readability
if isinstance(result, float):
result = round(result, 2)
return str(result)
except ImportError:
return _safe_eval(expression)
except Exception:
return _safe_eval(expression)
except ImportError:
# Fallback to ast-based safe evaluation if numexpr not available
return _safe_eval(expression)
except Exception as e:
return f"Erreur de calcul: {str(e)}"
def _safe_eval(expression: str) -> str:
"""
Safe evaluation fallback using ast module.
Only allows basic arithmetic operations.
"""
import ast
import operator
# Allowed operators
operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}
def _eval(node):
if isinstance(node, ast.Constant):
return node.value
elif isinstance(node, ast.BinOp):
left = _eval(node.left)
right = _eval(node.right)
op = operators.get(type(node.op))
if op is None:
raise ValueError(f"Opérateur non supporté: {type(node.op).__name__}")
return op(left, right)
elif isinstance(node, ast.UnaryOp):
operand = _eval(node.operand)
op = operators.get(type(node.op))
if op is None:
raise ValueError(f"Opérateur non supporté: {type(node.op).__name__}")
return op(operand)
elif isinstance(node, ast.Expression):
return _eval(node.body)
else:
raise ValueError(f"Expression non supportée: {type(node).__name__}")
try:
tree = ast.parse(expression, mode='eval')
result = _eval(tree)
if isinstance(result, float):
result = round(result, 2)
return str(result)
except Exception as e:
return f"Erreur de calcul: {str(e)}"