BuddyMath / problem_understanding.py
dotandru's picture
QA Cycle 2.1: Data Slicing, Hotfix refinement, Firestore fix, and UI Safety updates
5e09541
Raw
History Blame Contribute Delete
5.15 kB
# problem_understanding.py - V231.14
# Analyzes problem structure BEFORE solving
"""
Problem Understanding Module
Ensures we understand WHAT is being asked before attempting to solve.
"""
import json
import re
from utils.safe_json import safe_extract_json # V1.0: Canonical extractor
def get_problem_understanding_prompt(ocr_text: str, data_anchor: dict) -> str:
"""
Prompt LLM to analyze problem structure.
Returns JSON with:
- problem_type
- sub_questions (all parts א, ב, ג, etc.)
- solving_order
- dependencies
"""
return f"""
ANALYZE this math problem structure. DO NOT SOLVE - only understand what is being asked.
Problem Text:
{ocr_text}
Extracted Data:
{json.dumps(data_anchor, ensure_ascii=False, indent=2)}
Your task: Identify ALL parts of this problem and create a solving plan.
Return JSON:
{{
"problem_type": "CIRCLE_EQUATION | LINE_EQUATION | GEOMETRIC_LOCUS | DERIVATIVE_QUOTIENT | etc.",
"main_question": "Brief description of main question",
"sub_questions": [
{{
"id": "א",
"question": "Full text of sub-question א",
"requires": ["center", "radius"],
"specific_values": ["m=2"],
"expected_output": "equation | number | point | etc.",
"topic": "CIRCLE_EQUATION"
}},
{{
"id": "ב",
"question": "Full text of sub-question ב",
"requires": ["equation_from_א", "point"],
"specific_values": ["a=1"],
"expected_output": "line_equation",
"topic": "LINE_TANGENT"
}}
],
"solving_order": ["א", "ב", "ג"],
"dependencies": {{
"ב": ["א"],
"ג": ["א"]
}}
}}
CRITICAL RULES:
1. Include ALL sub-questions (א, ב, ג, ד, etc.)
2. **CHRONOLOGICAL DATA ISOLATION (V310.0 - CRITICAL):** If a specific value (e.g., a=1, m=2) is mentioned ONLY in a specific sub-question, you MUST include it in the `specific_values` array for THAT sub-question only. NEVER put it in the top-level anchor if it's not global. This prevents data leakage (e.g., using a=1 from section ב' to solve section א').
3. **EXCEPTION:** If the problem asks for a **Geometric Locus (מקום גיאומטרי)**:
- This is a SINGLE QUESTION (even if it looks long).
- Set `problem_type` = "GEOMETRIC_LOCUS".
- Create ONLY ONE sub-question (id="א") containing the entire text.
4. Identify dependencies (ב needs א's result)
5. Determine topic for EACH sub-question
6. DO NOT solve - only analyze structure
Return ONLY valid JSON.
"""
def parse_understanding(response_text: str) -> dict:
"""Parse LLM understanding response using canonical safe_extract_json."""
result = safe_extract_json(response_text, caller="PROBLEM_UNDERSTANDING")
# If parsing failed, return a minimal fallback so the pipeline continues
if isinstance(result, dict) and result.get("logic_error"):
return {
"problem_type": "UNKNOWN",
"sub_questions": [],
"solving_order": [],
"dependencies": {}
}
return result
def validate_understanding(understanding: dict) -> bool:
"""Validate understanding structure."""
required_keys = ["problem_type", "sub_questions", "solving_order"]
if not all(key in understanding for key in required_keys):
return False
if not isinstance(understanding["sub_questions"], list):
return False
if len(understanding["sub_questions"]) == 0:
return False
# Validate each sub-question
for sq in understanding["sub_questions"]:
if not all(key in sq for key in ["id", "question", "topic"]):
return False
return True
def enforce_locus_rule(understanding: dict, ocr_text: str) -> dict:
"""
V260.2: Hard Rule - If 'מקום גיאומטרי' exists, FORCE Locus type.
"""
if any(k in ocr_text for k in ["מקום גיאומטרי", "Locus", "מצא את המקום", "המקום הגיאומטרי"]):
print("🛡️ [BIT-LOG] Hard Logic: Detected 'Geometric Locus' - Forcing Single Question structure.")
return {
"problem_type": "GEOMETRIC_LOCUS",
"main_question": understanding.get("main_question", "Find the Locus"),
"sub_questions": [{
"id": "א",
"question": ocr_text, # Give the WHOLE text to the single sub-question
"requires": [],
"expected_output": "equation",
"topic": "GEOMETRIC_LOCUS"
}],
"solving_order": ["א"],
"dependencies": {}
}
return understanding
# ==================== USAGE EXAMPLE ====================
if __name__ == "__main__":
# Test understanding prompt
ocr = """
מעגל עם משוואה x² + y² = 12
א. מצא את משוואת המעגל
ב. מצא משיק למעגל בנקודה A
ג. מצא את שטח המעגל
"""
data = {
"function_equations": ["x^2 + y^2 = 12"],
"points": ["A"],
"specific_values": [],
"constraints": []
}
prompt = get_problem_understanding_prompt(ocr, data)
print(prompt)