import re import logging import sympy as sp import numpy as np from typing import Dict, Any, List, Tuple, Optional logger = logging.getLogger(__name__) class MathCalculator: """ Deterministic Mathematical Calculation Engine (v5.3). Executes geometric formulas, algebraic expressions, and symbolic calculus using SymPy to eliminate LLM arithmetic hallucinations and ensure 100% calculation accuracy. """ def __init__(self): self.safe_globals = { "sp": sp, "sqrt": sp.sqrt, "pi": sp.pi, "sin": sp.sin, "cos": sp.cos, "tan": sp.tan, "asin": sp.asin, "acos": sp.acos, "atan": sp.atan, "Rational": sp.Rational, "Abs": sp.Abs, "exp": sp.exp, "log": sp.log, "deg": lambda rad: rad * 180 / sp.pi, "rad": lambda deg: deg * sp.pi / 180, } def evaluate_expression(self, expr_str: str, context: Optional[Dict[str, Any]] = None) -> Tuple[Optional[sp.Expr], Optional[float], str]: """ Evaluates a mathematical expression string using SymPy with intelligent variable aliasing. Returns: (exact_symbolic_value, float_value, latex_string) """ if not expr_str: return None, None, "0" s = str(expr_str).replace('$', '').strip().rstrip('.,;') s = s.replace("\x0crac", "frac").replace("\x0c", "") s = s.replace("^", "**").replace("×", "*").replace("÷", "/").replace("\\times", "*").replace("\\cdot", "*") s = s.replace("\\pi", "pi") # 1. LaTeX subscripts: S_{ABCD} -> S_ABCD s = re.sub(r'([A-Za-z]+)_\{([^}]+)\}', r'\1_\2', s) # 2. LaTeX sqrt: \sqrt{x} -> sqrt(x) s = re.sub(r'\\*sqrt\{([^}]+)\}', r'sqrt(\1)', s) s = re.sub(r'\\*sqrt([0-9]+)', r'sqrt(\1)', s) # 3. LaTeX fractions: \frac{a}{b} -> ((a)/(b)) s = re.sub(r'\\*frac\{([^}]+)\}\{([^}]+)\}', r'((\1)/(\2))', s) s = re.sub(r'frac\{([^}]+)\}\{([^}]+)\}', r'((\1)/(\2))', s) # 4. Insert implicit multiplications: # e.g. 9sqrt(3) -> 9*sqrt(3), 36sqrt(3) -> 36*sqrt(3), 2(64+...) -> 2*(64+...), ((6)/(3))(...) -> ((6)/(3))*(...) s = re.sub(r'(\d+|\))\s*(sqrt|pi|sin|cos|tan|[A-Za-z_])', r'\1*\2', s) s = re.sub(r'(\d+|\))\s*\(', r'\1*(', s) # 5. Standalone fractions like 1/3, 1/2 to Rational s = re.sub(r'(? Dict[str, Any]: """ Processes a structured solution plan from LLM, evaluating all calculations deterministically. """ context: Dict[str, Any] = {} formatted_steps: List[str] = [] final_answer_sym = None symbolic_expr_parts = [] for idx, step in enumerate(steps_data, start=1): if isinstance(step, str): verified_step, val = self._verify_text_step(step, context) formatted_steps.append(verified_step) if val is not None: final_answer_sym = val continue explanation = step.get("explanation", "").strip() formula = step.get("formula", "").strip() calc_expr = step.get("calculation", "").strip() var_name = step.get("variable", "").strip() unit = step.get("unit", "").strip() if calc_expr: sym_val, flt_val, latex_val = self.evaluate_expression(calc_expr, context) if sym_val is not None: if var_name: self._save_to_context(context, var_name, sym_val) context['prev'] = sym_val context['ans'] = sym_val final_answer_sym = sym_val if sym_val.is_integer: val_display = str(int(flt_val)) elif sym_val.has(sp.sqrt, sp.pi) or sym_val.is_rational: val_display = f"{sym_val} (≈ {flt_val:.2f})" else: val_display = f"{flt_val:.2f}" if abs(flt_val - round(flt_val)) > 1e-4 else str(int(round(flt_val))) unit_str = f" {unit}" if unit else "" step_text = f"Bước {idx}: {explanation}." if formula and calc_expr: step_text += f" Ta có: {formula} = {calc_expr} = {val_display}{unit_str}." elif formula: step_text += f" Ta có: {formula} = {val_display}{unit_str}." elif calc_expr: step_text += f" Tính toán: {calc_expr} = {val_display}{unit_str}." formatted_steps.append(step_text) if formula: symbolic_expr_parts.append(f"{formula} = {latex_val}") else: formatted_steps.append(f"Bước {idx}: {explanation}.") else: formatted_steps.append(f"Bước {idx}: {explanation}.") final_answer_str = self._format_final_answer(final_answer_sym) final_symbolic_expression = "; ".join(symbolic_expr_parts) if symbolic_expr_parts else None return { "answer": final_answer_str, "steps": formatted_steps, "symbolic_expression": final_symbolic_expression, "evaluated_context": {k: str(v) for k, v in context.items() if k not in ('prev', 'ans')}, } def process_text_steps(self, text_steps: List[str]) -> Dict[str, Any]: """ Parses raw text steps, intercepts mathematical formulas with '=' signs, evaluates all expressions via SymPy, and replaces arithmetic with verified results. """ context: Dict[str, Any] = {} formatted_steps: List[str] = [] final_answer_sym = None symbolic_expr_parts = [] for idx, step_str in enumerate(text_steps, start=1): clean_step = str(step_str).strip() verified_step, val = self._verify_text_step(clean_step, context) if not re.match(r'^(Bước|\d+[\.:\)])', verified_step, re.IGNORECASE): verified_step = f"Bước {idx}: {verified_step}" formatted_steps.append(verified_step) if val is not None: final_answer_sym = val symbolic_expr_parts.append(sp.latex(val)) final_answer_str = self._format_final_answer(final_answer_sym) final_symbolic_expression = "; ".join(symbolic_expr_parts) if symbolic_expr_parts else None return { "answer": final_answer_str, "steps": formatted_steps, "symbolic_expression": final_symbolic_expression, "evaluated_context": {k: str(v) for k, v in context.items() if k not in ('prev', 'ans')}, } def _verify_text_step(self, step_str: str, context: Dict[str, Any]) -> Tuple[str, Optional[sp.Expr]]: """ Sentence-level equation parser and re-evaluator. Splits by sentences so multiple equations in one step are independently processed. """ sentences = re.split(r'([.;]\s+)', step_str) rebuilt = [] last_val = None for sentence in sentences: if '=' in sentence: parts = [p.strip() for p in sentence.split('=') if p.strip()] if len(parts) >= 2: lhs = parts[0] best_val = None best_expr = None # Check each remaining part to find the computable mathematical expression for expr_cand in parts[1:]: clean = expr_cand.replace('$', '').rstrip('.,;').strip() if any(c in clean for c in '+-*/()0123456789') or clean in context: sym_val, flt_val, latex_val = self.evaluate_expression(clean, context) if sym_val is not None: best_val = sym_val best_expr = clean if best_val is not None: var_cand = re.findall(r'[A-Za-z_][A-Za-z0-9_]*', lhs) if var_cand: var_name = var_cand[-1] self._save_to_context(context, var_name, best_val) context['prev'] = best_val context['ans'] = best_val last_val = best_val flt = float(best_val.evalf()) disp = str(int(flt)) if best_val.is_integer else ( f"{best_val} (≈ {flt:.2f})" if best_val.has(sp.sqrt, sp.pi) or best_val.is_rational else f"{flt:.2f}" ) if len(parts) > 2: sentence = f"{lhs} = {parts[1]} = {disp}" else: sentence = f"{lhs} = {best_expr} = {disp}" rebuilt.append(sentence) return "".join(rebuilt), last_val or context.get('ans') def _save_to_context(self, context: Dict[str, Any], var_name: str, sym_val: sp.Expr): context[var_name] = sym_val clean_name = var_name.lower().replace('_', '').replace('{', '').replace('}', '') if 's' in clean_name or 'b' in clean_name or 'area' in clean_name: context['S_day'] = sym_val context['S'] = sym_val context['B'] = sym_val context['S_ABCD'] = sym_val context['S_ABC'] = sym_val if '1' in clean_name: context['S1'] = sym_val if '2' in clean_name: context['S2'] = sym_val elif 'h' in clean_name or 'so' in clean_name or 'height' in clean_name: context['h'] = sym_val context['SO'] = sym_val elif 'v' in clean_name: context['V'] = sym_val def _format_final_answer(self, sym_val: Optional[sp.Expr]) -> str: if sym_val is None: return "" flt_val = float(sym_val.evalf()) if sym_val.is_integer: return str(int(flt_val)) elif sym_val.has(sp.sqrt, sp.pi) or sym_val.is_rational: return f"{sym_val} (≈ {flt_val:.2f})" else: return f"{flt_val:.2f}" if abs(flt_val - round(flt_val)) > 1e-4 else str(int(round(flt_val)))