File size: 4,080 Bytes
c75184e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
"""
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 ""