dotandru commited on
Commit
aae908d
·
1 Parent(s): 5230898

feat(backend): V281.0 - Vision-First OCR, Visuals robustness (Pow fix), and Rigid Graph Identification

Browse files
Files changed (4) hide show
  1. orchestrator.py +11 -5
  2. prompts.py +30 -3
  3. topic_taxonomy.py +7 -0
  4. visuals.py +11 -6
orchestrator.py CHANGED
@@ -768,13 +768,19 @@ ctx.finish("$$ 4 $$", "מעולה! הגענו לתוצאה.")
768
  print(f"📸 🏆 [BIT-LOG] OCR Winner: score={winner[1]} (of {len(valid)} candidates)")
769
  return winner[0]
770
 
771
- async def _extract_key_data(self, problem_text: str) -> dict:
772
- """V231.12: Phase 1 - Extract specific values with validation and retry."""
773
  for attempt in range(1, 3): # 2 attempts
774
  try:
775
  prompt = prompts.get_data_extraction_prompt(problem_text)
 
 
 
 
 
 
776
  res = await asyncio.wait_for(
777
- self.model.generate_content_async(prompt),
778
  timeout=15.0 # 15s timeout per attempt
779
  )
780
  cost_tracker.log_api_usage(res.usage_metadata, "DATA_ANCHOR")
@@ -1182,7 +1188,7 @@ ctx.finish("$$ 4 $$", "מעולה! הגענו לתוצאה.")
1182
  # First, transcribe and extract the "Absolute Truth" of the problem
1183
  print("📝 [CHECK-ME] Step 1.5: Extracting Problem Data (Data Slicing)...")
1184
  problem_text = await self.transcribe_image(image_data)
1185
- data_anchor = await self._extract_key_data(problem_text)
1186
 
1187
  # Step 2: Build check-me prompt and send to Vision LLM
1188
  check_prompt = prompts.get_check_me_prompt(
@@ -2136,7 +2142,7 @@ ctx.finish("$$ 4 $$", "מעולה! הגענו לתוצאה.")
2136
  # ===================== FULL STREAMING PIPELINE =====================
2137
  print(f"🎯 [BIT-LOG] Using Streaming Pipeline Strategy: {strategy.value}")
2138
 
2139
- data_anchor = await self._extract_key_data(problem_text) or {}
2140
 
2141
  # Iterate through the streaming smart_solve
2142
  # V5.10.2: Remove keys already passed explicitly to avoid TypeError collision
 
768
  print(f"📸 🏆 [BIT-LOG] OCR Winner: score={winner[1]} (of {len(valid)} candidates)")
769
  return winner[0]
770
 
771
+ async def _extract_key_data(self, problem_text: str, image_data: bytes = None) -> dict:
772
+ """V231.14: Phase 1 - Extract specific values with validation and image support."""
773
  for attempt in range(1, 3): # 2 attempts
774
  try:
775
  prompt = prompts.get_data_extraction_prompt(problem_text)
776
+
777
+ # Build multimodal request if image is available
778
+ content = [prompt]
779
+ if image_data:
780
+ content.append({"mime_type": "image/png", "data": image_data})
781
+
782
  res = await asyncio.wait_for(
783
+ self.model.generate_content_async(content),
784
  timeout=15.0 # 15s timeout per attempt
785
  )
786
  cost_tracker.log_api_usage(res.usage_metadata, "DATA_ANCHOR")
 
1188
  # First, transcribe and extract the "Absolute Truth" of the problem
1189
  print("📝 [CHECK-ME] Step 1.5: Extracting Problem Data (Data Slicing)...")
1190
  problem_text = await self.transcribe_image(image_data)
1191
+ data_anchor = await self._extract_key_data(problem_text, image_data=image_data)
1192
 
1193
  # Step 2: Build check-me prompt and send to Vision LLM
1194
  check_prompt = prompts.get_check_me_prompt(
 
2142
  # ===================== FULL STREAMING PIPELINE =====================
2143
  print(f"🎯 [BIT-LOG] Using Streaming Pipeline Strategy: {strategy.value}")
2144
 
2145
+ data_anchor = await self._extract_key_data(problem_text, image_data=image_data) or {}
2146
 
2147
  # Iterate through the streaming smart_solve
2148
  # V5.10.2: Remove keys already passed explicitly to avoid TypeError collision
prompts.py CHANGED
@@ -230,11 +230,15 @@ def _detect_relevant_rules(text: str, category: str = "") -> list:
230
  return rules
231
 
232
  def get_data_extraction_prompt(problem_text: str) -> str:
233
- """V231.13: Extract ALL equations, not just functions."""
234
  return fr"""
235
- EXTRACT key mathematical data from this problem. Return ONLY valid JSON.
 
 
 
 
236
 
237
- Problem:
238
  {problem_text}
239
 
240
  Extract:
@@ -384,6 +388,28 @@ def get_specialist_prompt(category, problem_text, solver_hint, grade, student_na
384
  }
385
  """
386
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  return f"""
388
  🎓 תפקיד: אתה "המורה למתמטיקה" — מורה פרטית חמה ומעודדת בגישת 'הנסיך והנסיכה'.
389
  🌟 הנחיה עליונה: הפוך את הלמידה לחוויה מעצימה, אישית ונעימה עבור {student_name}.
@@ -421,6 +447,7 @@ def get_specialist_prompt(category, problem_text, solver_hint, grade, student_na
421
 
422
  {proof_block}
423
  {investigation_block}
 
424
  """
425
 
426
 
 
230
  return rules
231
 
232
  def get_data_extraction_prompt(problem_text: str) -> str:
233
+ """V231.14: Extract strictly from IMAGE. OCR is a secondary reference."""
234
  return fr"""
235
+ ### [DATA ANCHOR SUPREMACY]
236
+ You are provided with an IMAGE and its rough OCR transcription.
237
+ The OCR text is highly unreliable and may contain critical errors in mathematical symbols.
238
+ YOUR TASK: Extract the mathematical "Absolute Truth" strictly by visually inspecting the IMAGE.
239
+ Use the OCR text only as a secondary reference for context.
240
 
241
+ Problem Text (OCR):
242
  {problem_text}
243
 
244
  Extract:
 
388
  }
389
  """
390
 
391
+ # V281.0: Graph Identification Rigid JSON Rule
392
+ graph_identification_block = ""
393
+ if category == "GRAPH_IDENTIFICATION" or "גרף" in problem_text and "איזה" in problem_text:
394
+ graph_identification_block = """
395
+ ═══════════════════════════════════════════════════
396
+ 📊 חוק התאמת גרפים (GRAPH MATCHING PROTOCOL - V281.0):
397
+ ═══════════════════════════════════════════════════
398
+
399
+ בשאלה זו עליך להתאים בין פונקציה לגרף. כדי למנוע הזיות (hallucinations), חובה לעבוד בפורמט ניתוח קשיח.
400
+ חובה להוסיף בשורש ה-JSON את השדה "graph_analysis" המילוני:
401
+ "graph_analysis": {
402
+ "function_analysis": "תיאור מתמטי מפורט של הפונקציה (נקודות חיתוך עם הצירים, נקודות קיצון, אסימפטוטות, תחומי חיוביות ושליליות)",
403
+ "graph_options": [
404
+ {"id": "I", "description": "תיאור ויזואלי של גרף I מהתמונה - איפה הוא עובר, האם הוא עולה/יורד, נקודות חיתוך בולטות"},
405
+ {"id": "II", "description": "תיאור ויזואלי של גרף II מהתמונה..."},
406
+ {"id": "III", "description": "תיאור ויזואלי של גרף III... (אם קיים)"}
407
+ ],
408
+ "matching_logic": "הסבר לוגי-מתמטי המקשר בין תכונות הפונקציה לבין המאפיינים הויזואליים של הגרף שנבחר, והסבר למה הגרפים האחרים נפסלו",
409
+ "final_match": "התשובה הסופית (למשל: גרף II)"
410
+ }
411
+ """
412
+
413
  return f"""
414
  🎓 תפקיד: אתה "המורה למתמטיקה" — מורה פרטית חמה ומעודדת בגישת 'הנסיך והנסיכה'.
415
  🌟 הנחיה עליונה: הפוך את הלמידה לחוויה מעצימה, אישית ונעימה עבור {student_name}.
 
447
 
448
  {proof_block}
449
  {investigation_block}
450
+ {graph_identification_block}
451
  """
452
 
453
 
topic_taxonomy.py CHANGED
@@ -502,6 +502,13 @@ TOPIC_TAXONOMY = {
502
  "category": "ALGEBRA",
503
  "complexity": "medium"
504
  },
 
 
 
 
 
 
 
505
  }
506
 
507
  # ==================== DETECTION FUNCTIONS ====================
 
502
  "category": "ALGEBRA",
503
  "complexity": "medium"
504
  },
505
+
506
+ "GRAPH_IDENTIFICATION": {
507
+ "keywords": ["איזה מהגרפים", "מתאים לפונקציה", "התאם בין", "מתאמת לגרף"],
508
+ "grade_range": (9, 12),
509
+ "category": "GRAPH_IDENTIFICATION",
510
+ "complexity": "high"
511
+ },
512
  }
513
 
514
  # ==================== DETECTION FUNCTIONS ====================
visuals.py CHANGED
@@ -38,10 +38,12 @@ def sanitize_math_for_sympy(expr_str: str) -> str:
38
  text = text.replace(r'\ln', 'ln').replace(r'\log', 'log').replace(r'\exp', 'exp')
39
  text = text.replace(r'\sqrt', 'sqrt').replace(r'\pi', 'pi').replace(r'\theta', 'theta')
40
 
41
- # Implicit function arguments: sin x -> sin(x)
42
- text = re.sub(r'(sin|cos|tan|ln|log|exp|sqrt)\s*([a-zA-Z0-9]+)', r'\1(\2)', text)
 
 
43
 
44
- # V276.0 CRITICAL FIX: \cdot → * (BEFORE ^ conversion!)
45
  text = text.replace(r'\cdot', '*').replace('cdot', '*')
46
 
47
  # 5. ABSOLUTE VALUE FIX (V309.5: Clean Paired Pipes |x| -> Abs(x))
@@ -69,9 +71,12 @@ def sanitize_math_for_sympy(expr_str: str) -> str:
69
  blocklist = ["calculate", "find", "solve", "graph", ":"]
70
  for w in blocklist: text = re.sub(rf'\b{w}\b', '', text, flags=re.IGNORECASE)
71
 
72
- # Implicit multiplication
73
- text = re.sub(r'(\d)([a-zA-Z\(])', r'\1*\2', text)
74
- text = re.sub(r'(\))(\()', r'\1*\2', text)
 
 
 
75
  text = re.sub(r'(\))([a-zA-Z])', r'\1*\2', text)
76
 
77
  # Whitelist → replaced with targeted blocklist to protect all math-relevant chars
 
38
  text = text.replace(r'\ln', 'ln').replace(r'\log', 'log').replace(r'\exp', 'exp')
39
  text = text.replace(r'\sqrt', 'sqrt').replace(r'\pi', 'pi').replace(r'\theta', 'theta')
40
 
41
+ # V281.0: CRITICAL FIX - Implicit multiplication before parentheses and functions
42
+ # 1. Add parentheses to functions missing them: ln x -> ln(x), ln Abs(x) -> ln(Abs(x))
43
+ # Match function + (identifier or Abs(...) or nested function)
44
+ text = re.sub(r'(sin|cos|tan|ln|log|exp|sqrt)\s*([a-zA-Z\d]+(?:\([^)]*\))?)', r'\1(\2)', text)
45
 
46
+ # 2. V276.0 CRITICAL FIX: \cdot → * (BEFORE ^ conversion!)
47
  text = text.replace(r'\cdot', '*').replace('cdot', '*')
48
 
49
  # 5. ABSOLUTE VALUE FIX (V309.5: Clean Paired Pipes |x| -> Abs(x))
 
71
  blocklist = ["calculate", "find", "solve", "graph", ":"]
72
  for w in blocklist: text = re.sub(rf'\b{w}\b', '', text, flags=re.IGNORECASE)
73
 
74
+ # V281.0: REFINED IMPLICIT MULTIPLICATION (Fixes "Pow object is not callable")
75
+ # x(x+1) -> x*(x+1), 2(x) -> 2*(x), (x)(y) -> (x)*(y)
76
+ text = re.sub(r'(\d|[a-zA-Z]|\))(\()', r'\1*\2', text)
77
+ # 2x -> 2*x, )x -> )*x
78
+ text = re.sub(r'(\d|\))([a-zA-Z\(])', r'\1*\2', text)
79
+ # (x)y -> (x)*y
80
  text = re.sub(r'(\))([a-zA-Z])', r'\1*\2', text)
81
 
82
  # Whitelist → replaced with targeted blocklist to protect all math-relevant chars