math-api / src /math /graph_utils.py
False45's picture
v2
c75184e
Raw History Blame Contribute Delete
4.08 kB
"""
Graph Utilities — Safe expression evaluation and OCR text extraction.
Contains:
- _safe_evaluate_expression: SymPy-based safe math evaluator (Phase 0 security fix)
- ocr_extract_text: Tesseract OCR for math image extraction
Extracted from main.py L815-L904 (Phase 0 security-fixed version).
"""
import re
import os
import logging
from typing import Optional
logger = logging.getLogger("math_assistant.graph")
def safe_evaluate_expression(expr_str: str, x_array):
"""Safely evaluate a mathematical expression string over a numpy array.
Uses SymPy's sympify() to parse the expression (rejecting arbitrary Python)
and lambdify() to convert it to a numpy-callable function. This is safe
because sympify() only understands mathematical syntax, not arbitrary code.
Args:
expr_str: Mathematical expression string (e.g., "sin(x)", "x**2 + 1").
x_array: numpy array of x values to evaluate the expression over.
Returns:
numpy array of y values.
Raises:
ValueError: If the expression contains disallowed constructs.
sympy.SympifyError: If the expression cannot be parsed as math.
"""
import sympy as sp
import numpy as np
# Reject expressions containing dangerous patterns before parsing
_BLOCKED_PATTERNS = [
"__", "import", "exec", "eval", "compile", "open", "getattr",
"setattr", "delattr", "globals", "locals", "vars", "dir",
"breakpoint", "exit", "quit", "input", "print", "os.",
"sys.", "subprocess", "shutil", "pathlib",
]
expr_lower = expr_str.lower().replace(" ", "")
for pattern in _BLOCKED_PATTERNS:
if pattern in expr_lower:
raise ValueError(f"Blocked: expression contains disallowed pattern '{pattern}'")
# Replace caret with power operator
sanitized = re.sub(r'\^', '**', expr_str.strip())
# Define allowed SymPy symbols and functions for parsing
x = sp.Symbol("x")
_local_dict = {
"x": x, "e": sp.E, "pi": sp.pi,
"sin": sp.sin, "cos": sp.cos, "tan": sp.tan,
"exp": sp.exp, "log": sp.log, "ln": sp.log,
"sqrt": sp.sqrt, "abs": sp.Abs,
"arcsin": sp.asin, "arccos": sp.acos, "arctan": sp.atan,
"asin": sp.asin, "acos": sp.acos, "atan": sp.atan,
"sinh": sp.sinh, "cosh": sp.cosh, "tanh": sp.tanh,
"sec": sp.sec, "csc": sp.csc, "cot": sp.cot,
"ceiling": sp.ceiling, "floor": sp.floor,
}
# Parse through SymPy — rejects arbitrary Python code
sym_expr = sp.sympify(sanitized, locals=_local_dict)
# Convert to numpy-callable function — safe, no code execution
f = sp.lambdify(x, sym_expr, modules=["numpy"])
y = f(x_array)
# Ensure result is array (handles constant expressions like "5" or "pi")
if not hasattr(y, "__len__"):
y = np.full_like(x_array, float(y))
return y
def ocr_extract_text(image) -> str:
"""Extract text from an image using Tesseract OCR.
Args:
image: PIL Image object.
Returns:
Extracted text string, or error message if OCR fails.
"""
try:
import pytesseract
from PIL import ImageEnhance as enhance
# Set tesseract binary path for common install locations
for _tess_path in [
"/opt/homebrew/bin/tesseract", # macOS Apple Silicon (brew)
"/usr/local/bin/tesseract", # macOS Intel (brew)
"/usr/bin/tesseract", # Linux
]:
if os.path.exists(_tess_path):
pytesseract.pytesseract.tesseract_cmd = _tess_path
break
gray = image.convert("L")
contrast = enhance.Contrast(gray).enhance(2.0)
sharpened = enhance.Sharpness(contrast).enhance(2.0)
text = pytesseract.image_to_string(sharpened, config='--psm 6').strip()
text = ' '.join(text.replace('\n', ' ').split())
return text
except ImportError:
return "ERROR_NO_PYTESSERACT"
except Exception as e:
logger.error(f"OCR error: {e}")
return ""