feat(backend): V281.0 - Vision-First OCR, Visuals robustness (Pow fix), and Rigid Graph Identification
Browse files- orchestrator.py +11 -5
- prompts.py +30 -3
- topic_taxonomy.py +7 -0
- 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.
|
| 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(
|
| 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.
|
| 234 |
return fr"""
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 42 |
-
|
|
|
|
|
|
|
| 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 |
-
#
|
| 73 |
-
|
| 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
|