dotandru commited on
Commit
984ec8c
·
1 Parent(s): 73b031f

V9.0.1: SymPy Pipeline Hardening (Aggressive Sanitizer, Hebrew & Arrow Stripping)

Browse files
domain/math_validator.py CHANGED
@@ -7,6 +7,7 @@ import asyncio
7
  from typing import Tuple, List, Optional
8
  import sympy
9
  from sympy.parsing.sympy_parser import parse_expr
 
10
 
11
  logger = logging.getLogger(__name__)
12
 
@@ -205,18 +206,13 @@ class MathPolygraph:
205
  @staticmethod
206
  async def _check_segment(raw_segment: str, step_id) -> Tuple[bool, str]:
207
  """Internal helper to validate a single extracted math segment."""
208
- # 4. Multi-Equal Sign Handling & Unpacking Crash Prevention
209
- eq_count = raw_segment.count('=')
210
 
211
- parts_to_check = []
212
- if eq_count >= 1:
213
- # Split by all equalities and check each segment (e.g. x=y=5 -> check x, y, 5)
214
- # This bypasses SymPy's inability to parse "=" and prevents split() unpacking errors.
215
- parts_to_check = [p.strip() for p in raw_segment.split('=') if p.strip()]
216
- else:
217
- parts_to_check = [raw_segment]
218
 
219
- for part in parts_to_check:
220
  sympy_str = _latex_to_sympy_str(part)
221
  if not sympy_str or sympy_str in ('', '-', '()', '( )'):
222
  continue
@@ -225,6 +221,8 @@ class MathPolygraph:
225
  # Run with timeout to prevent ReDoS or complex simplification hangs
226
  status = await asyncio.to_thread(MathPolygraph._sympify_with_timeout, sympy_str)
227
  if status is False:
 
 
228
  return False, f"SYMPY_PARSE_ERROR:step_{step_id}"
229
  elif status is None:
230
  # Timeout is treated as a soft warning for now
 
7
  from typing import Tuple, List, Optional
8
  import sympy
9
  from sympy.parsing.sympy_parser import parse_expr
10
+ from utils.math_utils import aggressive_sympy_sanitizer
11
 
12
  logger = logging.getLogger(__name__)
13
 
 
206
  @staticmethod
207
  async def _check_segment(raw_segment: str, step_id) -> Tuple[bool, str]:
208
  """Internal helper to validate a single extracted math segment."""
209
+ # V9.0.1: Use Aggressive Sanitizer to handle Hebrew, Arrows, and Comma-splitting
210
+ sanitized_parts = aggressive_sympy_sanitizer(raw_segment)
211
 
212
+ if not sanitized_parts:
213
+ return True, ""
 
 
 
 
 
214
 
215
+ for part in sanitized_parts:
216
  sympy_str = _latex_to_sympy_str(part)
217
  if not sympy_str or sympy_str in ('', '-', '()', '( )'):
218
  continue
 
221
  # Run with timeout to prevent ReDoS or complex simplification hangs
222
  status = await asyncio.to_thread(MathPolygraph._sympify_with_timeout, sympy_str)
223
  if status is False:
224
+ # Provide clearer error context
225
+ logger.warning(f"[V9.0.1] SymPy Parse Error on part '{part}' (from segment '{raw_segment}')")
226
  return False, f"SYMPY_PARSE_ERROR:step_{step_id}"
227
  elif status is None:
228
  # Timeout is treated as a soft warning for now
orchestrator.py CHANGED
@@ -9,7 +9,7 @@ from domain.math_validator import MathPolygraph # V1.0: SymPy Polygraph
9
  from domain.processing_strategy import ProcessingStrategy
10
  from domain.ontology import get_allowed_concepts, get_pedagogical_tag
11
  from domain.math_normalizer import MathCanonicalizer
12
- from utils.math_utils import sanitize_latex_for_sympy
13
  from domain.curriculum_classifier import CurriculumClassifier
14
  from domain.proposal_engine import ProposalEngine
15
  from domain.risk_engine import CognitiveRiskEngine
@@ -58,12 +58,10 @@ def build_ast_metadata(math_input: str, category: str) -> dict:
58
  estimated_complexity = 0.5
59
 
60
  try:
61
- parts = str(math_input).split(',')
62
  free_syms = set()
63
- for part in parts:
64
  try:
65
- # V317.8: Clean LaTeX (colors/text) before validation
66
- clean_part = sanitize_latex_for_sympy(part)
67
  expr = sp.sympify(clean_part.replace('=', '-'), evaluate=False)
68
  free_syms.update(expr.free_symbols)
69
  except Exception as e:
@@ -80,7 +78,7 @@ def build_ast_metadata(math_input: str, category: str) -> dict:
80
  # Build node registry for Planner (IDs → expressions)
81
  ast_registry = {}
82
  try:
83
- parts = [p.strip() for p in str(math_input).split(',')]
84
  for i, part in enumerate(parts):
85
  ast_registry[f"ast_node_{i}"] = part
86
  except Exception as e:
 
9
  from domain.processing_strategy import ProcessingStrategy
10
  from domain.ontology import get_allowed_concepts, get_pedagogical_tag
11
  from domain.math_normalizer import MathCanonicalizer
12
+ from utils.math_utils import sanitize_latex_for_sympy, aggressive_sympy_sanitizer
13
  from domain.curriculum_classifier import CurriculumClassifier
14
  from domain.proposal_engine import ProposalEngine
15
  from domain.risk_engine import CognitiveRiskEngine
 
58
  estimated_complexity = 0.5
59
 
60
  try:
61
+ parts = aggressive_sympy_sanitizer(math_input)
62
  free_syms = set()
63
+ for clean_part in parts:
64
  try:
 
 
65
  expr = sp.sympify(clean_part.replace('=', '-'), evaluate=False)
66
  free_syms.update(expr.free_symbols)
67
  except Exception as e:
 
78
  # Build node registry for Planner (IDs → expressions)
79
  ast_registry = {}
80
  try:
81
+ parts = aggressive_sympy_sanitizer(math_input)
82
  for i, part in enumerate(parts):
83
  ast_registry[f"ast_node_{i}"] = part
84
  except Exception as e:
smart_solver.py CHANGED
@@ -11,6 +11,8 @@ from dataclasses import dataclass
11
  import copy
12
  from domain.step_types import StepType, SignedStep
13
 
 
 
14
  logger = logging.getLogger(__name__)
15
 
16
  # ==================== V7.3: TYPED ACTION CONTRACT ====================
@@ -449,8 +451,19 @@ class StepValidator:
449
  Deterministically verifies the algebraic equivalence between two steps.
450
  """
451
  try:
452
- expr_n = sympify(str(step_n).replace('=', '-'), evaluate=False)
453
- expr_n_plus_1 = sympify(str(step_n_plus_1).replace('=', '-'), evaluate=False)
 
 
 
 
 
 
 
 
 
 
 
454
 
455
  # Use simplify to check equivalence
456
  diff = simplify(expr_n - expr_n_plus_1)
@@ -481,12 +494,15 @@ class StepValidator:
481
  and no hallucinations occurred via injected fake constants/variables.
482
  """
483
  try:
484
- initial_exprs = [sympify(str(p).replace('=', '-'), evaluate=False) for p in str(initial_math).split(',')]
 
 
485
  initial_vars = set()
486
  for ex in initial_exprs:
487
  initial_vars.update(ex.free_symbols)
488
 
489
- final_exprs = [sympify(str(p).replace('=', '-'), evaluate=False) for p in str(final_step).split(',')]
 
490
  final_vars = set()
491
  for ex in final_exprs:
492
  final_vars.update(ex.free_symbols)
 
11
  import copy
12
  from domain.step_types import StepType, SignedStep
13
 
14
+ from utils.math_utils import aggressive_sympy_sanitizer
15
+
16
  logger = logging.getLogger(__name__)
17
 
18
  # ==================== V7.3: TYPED ACTION CONTRACT ====================
 
451
  Deterministically verifies the algebraic equivalence between two steps.
452
  """
453
  try:
454
+ # V9.0.1: Aggressive sanitization before comparison
455
+ # We take the first expression from the sanitizer if multiple are returned
456
+ parts_n = aggressive_sympy_sanitizer(str(step_n))
457
+ parts_n_plus_1 = aggressive_sympy_sanitizer(str(step_n_plus_1))
458
+
459
+ if not parts_n or not parts_n_plus_1:
460
+ return False
461
+
462
+ clean_n = parts_n[0].replace('=', '-')
463
+ clean_n_plus_1 = parts_n_plus_1[0].replace('=', '-')
464
+
465
+ expr_n = sympify(clean_n, evaluate=False)
466
+ expr_n_plus_1 = sympify(clean_n_plus_1, evaluate=False)
467
 
468
  # Use simplify to check equivalence
469
  diff = simplify(expr_n - expr_n_plus_1)
 
494
  and no hallucinations occurred via injected fake constants/variables.
495
  """
496
  try:
497
+ # V9.0.1: Aggressive sanitization
498
+ initial_parts = aggressive_sympy_sanitizer(str(initial_math))
499
+ initial_exprs = [sympify(str(p).replace('=', '-'), evaluate=False) for p in initial_parts]
500
  initial_vars = set()
501
  for ex in initial_exprs:
502
  initial_vars.update(ex.free_symbols)
503
 
504
+ final_parts = aggressive_sympy_sanitizer(str(final_step))
505
+ final_exprs = [sympify(str(p).replace('=', '-'), evaluate=False) for p in final_parts]
506
  final_vars = set()
507
  for ex in final_exprs:
508
  final_vars.update(ex.free_symbols)
utils/math_utils.py CHANGED
@@ -21,3 +21,38 @@ def sanitize_latex_for_sympy(latex_str: str) -> str:
21
  cleaned = cleaned.replace(r'\left', '').replace(r'\right', '')
22
 
23
  return cleaned
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  cleaned = cleaned.replace(r'\left', '').replace(r'\right', '')
22
 
23
  return cleaned
24
+
25
+ from typing import List
26
+
27
+ def aggressive_sympy_sanitizer(latex_str: str) -> List[str]:
28
+ """
29
+ V9.0.1: Cleans dirty LaTeX generated by the LLM before feeding it to SymPy.
30
+ Splits comma-separated equations into a list of pure math strings.
31
+ """
32
+ if not latex_str:
33
+ return []
34
+
35
+ s = latex_str
36
+
37
+ # 1. Strip Hebrew Content including surrounding parentheses (e.g. (לכן C))
38
+ # This prevents leaving behind empty shells like "( C)" which crash SymPy.
39
+ s = re.sub(r'\([^)]*[\u0590-\u05FF]+[^)]*\)', '', s)
40
+ s = re.sub(r'[\u0590-\u05FF]+', '', s)
41
+
42
+ # 2. Strip formatting tags (\text{} and \color{})
43
+ s = re.sub(r'\\text\{.*?\}', '', s)
44
+ s = re.sub(r'\\color\{.*?\}', '', s)
45
+
46
+ # 3. Replace Logical Arrows with Equals (to maintain equation structure)
47
+ arrows = [r'\Rightarrow', r'\implies', r'\iff', r'\rightarrow']
48
+ for arrow in arrows:
49
+ s = s.replace(arrow, '=')
50
+
51
+ # 4. Split multiple equations
52
+ # SymPy cannot parse "x=0, y=9" as a single AST node.
53
+ # We must split by comma and return a list of clean equations.
54
+ # We also run the existing sanitize_latex_for_sympy on each part.
55
+ expressions = [sanitize_latex_for_sympy(expr.strip()) for expr in s.split(',')]
56
+
57
+ # Filter out empty strings that might have resulted from the split/regex
58
+ return [e for e in expressions if e]
visuals.py CHANGED
@@ -13,12 +13,14 @@ def clean_latex_for_sympy(latex_str):
13
  cleaned = cleaned.replace(r'\ln', 'log')
14
  return cleaned
15
 
16
- def sanitize_math_for_sympy(expr_str: str) -> str:
17
- if not expr_str: return ""
18
- expr_str = clean_latex_for_sympy(expr_str)
19
-
20
- # 1. Strip Hebrew
21
  expr_str = re.sub(r'[\u0590-\u05FF]', '', expr_str)
 
 
 
 
 
22
 
23
  # 2. Cut multiple equations
24
  if r'\\' in expr_str:
 
13
  cleaned = cleaned.replace(r'\ln', 'log')
14
  return cleaned
15
 
16
+ # 1. Strip Hebrew Characters and parenthetical Hebrew explanations (V9.0.1)
17
+ expr_str = re.sub(r'\([^)]*[\u0590-\u05FF]+[^)]*\)', '', expr_str)
 
 
 
18
  expr_str = re.sub(r'[\u0590-\u05FF]', '', expr_str)
19
+
20
+ # 1.1 Replace Logical Arrows with Equals (V9.0.1)
21
+ arrows = [r'\Rightarrow', r'\implies', r'\iff', r'\rightarrow']
22
+ for arrow in arrows:
23
+ expr_str = expr_str.replace(arrow, '=')
24
 
25
  # 2. Cut multiple equations
26
  if r'\\' in expr_str: