dotandru commited on
Commit
a589ce7
·
1 Parent(s): 5005dc6

V9.0.5: Prevent SSE Disconnects - Non-Blocking SymPy Validator & Timeout Optimization

Browse files
Files changed (2) hide show
  1. domain/math_validator.py +67 -100
  2. orchestrator.py +1 -1
domain/math_validator.py CHANGED
@@ -1,10 +1,11 @@
1
  # domain/math_validator.py — V1.1 (STABLE REGEX POLYGRAPH)
2
  import re
3
  import logging
4
- import multiprocessing
 
5
  import time
6
  import asyncio
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
@@ -95,75 +96,6 @@ def _is_plaintext(expr_str: str) -> bool:
95
  class MathPolygraph:
96
  TIMEOUT_SECONDS = 3
97
 
98
- @staticmethod
99
- def _sympify_worker(expr_str: str, queue: multiprocessing.Queue):
100
- """
101
- V280.0: Security Hardened Worker.
102
- 1. Character Whitelist: Only allow safe mathematical characters.
103
- 2. parse_expr(evaluate=False): Prevent RCE and immediate evaluation.
104
- """
105
- try:
106
- # V317.8: Suppress SymPy Deprecation Warnings (e.g. non-Expr in Pow)
107
- import warnings
108
- from sympy.utilities.exceptions import SymPyDeprecationWarning
109
- warnings.filterwarnings("ignore", category=SymPyDeprecationWarning)
110
-
111
- # RCE Prevention: Extreme character whitelist before parsing
112
- # V280.0 FIX: Added ! for factorials and ensured strict match.
113
- safe_pattern = r'^[a-zA-Z0-9\s\+\-\*\/\^\(\)\.\,\!\%\=]+$'
114
- if not re.match(safe_pattern, expr_str):
115
- queue.put(False)
116
- return
117
-
118
- # Security: evaluate=False stops automatic eval() of passed strings.
119
- res = parse_expr(expr_str, evaluate=False)
120
-
121
- # V280.0 FIX: Catch arithmetic errors like 1/0.
122
- # In SymPy, 1/0 evaluates to 'zoo' (ComplexInfinity).
123
- if res is not None:
124
- # evaluate the expression
125
- evaluated = res.doit()
126
- # If the result is infinite (zoo, oo, -oo) or NaN, treat as error
127
- # We check is_finite directly.
128
- if hasattr(evaluated, 'is_finite') and evaluated.is_finite is False:
129
- raise ZeroDivisionError("Infinite or undefined result")
130
- if hasattr(evaluated, 'is_nan') and evaluated.is_nan:
131
- raise ValueError("NaN result")
132
-
133
- queue.put(True)
134
- except (ZeroDivisionError, TypeError, ValueError, Exception) as e:
135
- queue.put(False)
136
-
137
- @staticmethod
138
- def _sympify_with_timeout(expr_str: str) -> bool:
139
- """Helper to run parsing in a separate process to enforce timeout."""
140
- if not expr_str or not expr_str.strip():
141
- return True
142
-
143
- # Strip characters that might survive _latex_to_sympy_str but fail whitelist
144
- s = expr_str.replace('\\', '').replace('_', '').replace('{', '(').replace('}', ')')
145
-
146
- queue = multiprocessing.Queue()
147
- process = multiprocessing.Process(target=MathPolygraph._sympify_worker, args=(s, queue))
148
- try:
149
- process.start()
150
- # Windows needs a generous timeout for cold process start + SymPy import.
151
- # 10 seconds is safe for verification/testing.
152
- process.join(timeout=10)
153
- if process.is_alive():
154
- process.terminate()
155
- process.join()
156
- with open('debug_math.val', 'a', encoding='utf-8') as f:
157
- f.write(f"[{time.time()}] TIMEOUT on '{s}'\n")
158
- return None # TIMEOUT
159
- if not queue.empty():
160
- return queue.get()
161
- return False
162
- except Exception:
163
- if process.is_alive():
164
- process.terminate()
165
- return False
166
-
167
  @staticmethod
168
  async def _validate_single(text: str, step_id) -> Tuple[bool, str]:
169
  """
@@ -207,6 +139,20 @@ class MathPolygraph:
207
 
208
  return True, ""
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  @staticmethod
211
  async def _check_segment(raw_segment: str, step_id) -> Tuple[bool, str]:
212
  """Internal helper to validate a single extracted math segment."""
@@ -222,18 +168,34 @@ class MathPolygraph:
222
  continue
223
 
224
  try:
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
- # Provide clearer error context
229
- logger.warning(f"[V9.0.1] SymPy Parse Error on part '{part}' (from segment '{raw_segment}')")
230
- return False, f"SYMPY_PARSE_ERROR:step_{step_id}"
231
- elif status is None:
232
- # Timeout is treated as a soft warning for now
233
- logger.warning(f"[V280.0] SymPy timeout on segment: {part}")
234
  except Exception as e:
235
- logger.error(f"[V280.0] Unexpected validation crash: {e}")
236
- return False, f"SYMPY_CRASH:step_{step_id}"
237
 
238
  return True, ""
239
 
@@ -267,21 +229,20 @@ class MathPolygraph:
267
  return False, reason
268
  return True, ""
269
  @staticmethod
270
- def are_equivalent(latex1: str, latex2: str) -> bool:
271
  """
272
- V1.2: Checks if two LaTeX expressions are mathematically equivalent.
273
  Supports expressions and equations (by converting to 'expr = 0').
274
  """
275
  try:
276
- # V280.0: Handle Equations in Equivalence Check
277
- # If both contain '=', split and compare parts.
278
- # Only recurse once!
279
  if '=' in latex1 and '=' in latex2 and latex1.count('=') == 1 and latex2.count('=') == 1:
280
  parts1 = [p.strip() for p in latex1.split('=') if p.strip()]
281
  parts2 = [p.strip() for p in latex2.split('=') if p.strip()]
282
  if len(parts1) == 2 and len(parts2) == 2:
283
- return MathPolygraph.are_equivalent(parts1[0], parts2[0]) and \
284
- MathPolygraph.are_equivalent(parts1[1], parts2[1])
 
285
 
286
  s1_raw = _latex_to_sympy_str(latex1)
287
  s2_raw = _latex_to_sympy_str(latex2)
@@ -300,19 +261,25 @@ class MathPolygraph:
300
  if not (is_safe(s1_raw) and is_safe(s2_raw)):
301
  return latex1.strip() == latex2.strip()
302
 
303
- expr1 = parse_expr(s1_raw, evaluate=False)
304
- expr2 = parse_expr(s2_raw, evaluate=False)
305
-
306
- # "Variable Trap": Basic structural equivalence if variables are involved
307
- if len(expr1.free_symbols) > 0 or len(expr2.free_symbols) > 0:
308
- return sympy.simplify(expr1 - expr2) == 0
 
 
 
 
 
 
309
 
310
- # Numerical Identity check: simplify(LHS - RHS) == 0
311
- diff = sympy.simplify(expr1 - expr2)
312
- return diff == 0
313
  except Exception as e:
314
- logger.warning(f"[POLYGRAPH] Equivalence check failed: {e}")
315
- return False
316
 
317
  @staticmethod
318
  async def verify_algebraic_consistency(steps: List[dict], topic: str = "GENERAL") -> Tuple[bool, str]:
@@ -340,7 +307,7 @@ class MathPolygraph:
340
  s2 = math_steps[i+1]['math']
341
 
342
  # Simple heuristic: Only verify if they look like comparable equations/expressions
343
- if not MathPolygraph.are_equivalent(s1, s2):
344
  logger.info(f"[POLYGRAPH] Consistency warning between {s1} and {s2}")
345
  # We return False only if we are VERY sure.
346
  # For now, we'll return False to trigger self-correction as requested.
 
1
  # domain/math_validator.py — V1.1 (STABLE REGEX POLYGRAPH)
2
  import re
3
  import logging
4
+ import re
5
+ import logging
6
  import time
7
  import asyncio
8
+ from typing import Tuple, List, Optional, Any
9
  import sympy
10
  from sympy.parsing.sympy_parser import parse_expr
11
  from utils.math_utils import aggressive_sympy_sanitizer
 
96
  class MathPolygraph:
97
  TIMEOUT_SECONDS = 3
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  @staticmethod
100
  async def _validate_single(text: str, step_id) -> Tuple[bool, str]:
101
  """
 
139
 
140
  return True, ""
141
 
142
+ @staticmethod
143
+ async def _run_safe_math_op(func, *args, timeout_sec: float = 0.3) -> Any:
144
+ """
145
+ V9.0.5: Executes a CPU-bound SymPy operation in a separate thread with a strict timeout.
146
+ Returns the result of the function, or raises TimeoutError/Exception.
147
+ """
148
+ try:
149
+ return await asyncio.wait_for(
150
+ asyncio.to_thread(func, *args),
151
+ timeout=timeout_sec
152
+ )
153
+ except asyncio.TimeoutError:
154
+ raise asyncio.TimeoutError("SymPy operation timed out")
155
+
156
  @staticmethod
157
  async def _check_segment(raw_segment: str, step_id) -> Tuple[bool, str]:
158
  """Internal helper to validate a single extracted math segment."""
 
168
  continue
169
 
170
  try:
171
+ # V9.0.5: Wrap CPU-bound sympify/parsing in a non-blocking thread with 300ms timeout
172
+ # We bypass the slow multiprocessing approach for SSE safety.
173
+ def parse_and_eval(s):
174
+ # Character Whitelist (from legacy _sympify_worker)
175
+ safe_pattern = r'^[a-zA-Z0-9\s\+\-\*\/\^\(\)\.\,\!\%\=]+$'
176
+ if not re.match(safe_pattern, s):
177
+ return False
178
+ res = parse_expr(s, evaluate=False)
179
+ if res is not None:
180
+ evaluated = res.doit()
181
+ if hasattr(evaluated, 'is_finite') and evaluated.is_finite is False:
182
+ return False
183
+ if hasattr(evaluated, 'is_nan') and evaluated.is_nan:
184
+ return False
185
+ return True
186
+
187
+ status = await MathPolygraph._run_safe_math_op(parse_and_eval, sympy_str)
188
+
189
  if status is False:
190
+ logger.warning(f"🛡️ [SOFT FAIL] SymPy Parse Error on part '{part}'. Bypassing validator.")
191
+ return True, "" # Soft Fail: allow stream to continue
192
+
193
+ except asyncio.TimeoutError:
194
+ logger.warning(f"🛡️ [SOFT FAIL] SymPy TIMEOUT (300ms) on part '{part}'. Bypassing validator.")
195
+ return True, "" # Soft Fail: allow stream to continue
196
  except Exception as e:
197
+ logger.warning(f"🛡️ [SOFT FAIL] Unexpected validation crash: {e} for part '{part}'. Bypassing.")
198
+ return True, "" # Soft Fail: allow stream to continue
199
 
200
  return True, ""
201
 
 
229
  return False, reason
230
  return True, ""
231
  @staticmethod
232
+ async def are_equivalent(latex1: str, latex2: str) -> bool:
233
  """
234
+ V9.0.5: Checks if two LaTeX expressions are mathematically equivalent (Non-Blocking).
235
  Supports expressions and equations (by converting to 'expr = 0').
236
  """
237
  try:
238
+ # 1. Handle Equations in Equivalence Check
 
 
239
  if '=' in latex1 and '=' in latex2 and latex1.count('=') == 1 and latex2.count('=') == 1:
240
  parts1 = [p.strip() for p in latex1.split('=') if p.strip()]
241
  parts2 = [p.strip() for p in latex2.split('=') if p.strip()]
242
  if len(parts1) == 2 and len(parts2) == 2:
243
+ res1 = await MathPolygraph.are_equivalent(parts1[0], parts2[0])
244
+ res2 = await MathPolygraph.are_equivalent(parts1[1], parts2[1])
245
+ return res1 and res2
246
 
247
  s1_raw = _latex_to_sympy_str(latex1)
248
  s2_raw = _latex_to_sympy_str(latex2)
 
261
  if not (is_safe(s1_raw) and is_safe(s2_raw)):
262
  return latex1.strip() == latex2.strip()
263
 
264
+ # V9.0.5: Wrap CPU-bound SymPy simplify in a thread with timeout
265
+ def calc_equivalence(str1, str2):
266
+ expr1 = parse_expr(str1, evaluate=False)
267
+ expr2 = parse_expr(str2, evaluate=False)
268
+ # "Variable Trap": Basic structural equivalence if variables are involved
269
+ if len(expr1.free_symbols) > 0 or len(expr2.free_symbols) > 0:
270
+ return sympy.simplify(expr1 - expr2) == 0
271
+ # Numerical Identity check
272
+ diff = sympy.simplify(expr1 - expr2)
273
+ return diff == 0
274
+
275
+ return await MathPolygraph._run_safe_math_op(calc_equivalence, s1_raw, s2_raw)
276
 
277
+ except asyncio.TimeoutError:
278
+ logger.warning(f"🛡️ [SOFT FAIL] Equivalence check TIMEOUT (300ms) for {latex1} vs {latex2}")
279
+ return True # Soft Fail: Assume equivalent if we can't prove otherwise in time
280
  except Exception as e:
281
+ logger.warning(f"🛡️ [SOFT FAIL] Equivalence check failed: {e}")
282
+ return True # Soft Fail: Assume equivalent on error
283
 
284
  @staticmethod
285
  async def verify_algebraic_consistency(steps: List[dict], topic: str = "GENERAL") -> Tuple[bool, str]:
 
307
  s2 = math_steps[i+1]['math']
308
 
309
  # Simple heuristic: Only verify if they look like comparable equations/expressions
310
+ if not await MathPolygraph.are_equivalent(s1, s2):
311
  logger.info(f"[POLYGRAPH] Consistency warning between {s1} and {s2}")
312
  # We return False only if we are VERY sure.
313
  # For now, we'll return False to trigger self-correction as requested.
orchestrator.py CHANGED
@@ -2306,7 +2306,7 @@ ctx.finish("$$ 4 $$", "מעולה! הגענו לתוצאה.")
2306
  fast_result, _, _ = validate_and_fix_solution(fast_result)
2307
  # Quick Polygraph check
2308
  _poly_steps = collect_all_steps(fast_result)
2309
- _poly_ok, _ = MathPolygraph.validate_step_sequence(_poly_steps, topic=str(strategy.value))
2310
 
2311
  if _poly_ok:
2312
  yield BuddyEvent(
 
2306
  fast_result, _, _ = validate_and_fix_solution(fast_result)
2307
  # Quick Polygraph check
2308
  _poly_steps = collect_all_steps(fast_result)
2309
+ _poly_ok, _ = await MathPolygraph.validate_step_sequence(_poly_steps, topic=str(strategy.value))
2310
 
2311
  if _poly_ok:
2312
  yield BuddyEvent(