Spaces:
Runtime error
Runtime error
feat(v2.0): Phase A — math_schema, extractor, universal_verifier, variable_pinner, pipeline [12/12 tests pass]
Browse files- backend/__init__.py +2 -0
- backend/extractor.py +127 -0
- backend/math_schema.py +97 -0
- backend/pipeline.py +102 -0
- backend/tests/test_pipeline.py +216 -0
- backend/universal_verifier.py +415 -0
- backend/variable_pinner.py +113 -0
- docs/legacy_prompts_backup.txt +937 -0
backend/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/__init__.py
|
| 2 |
+
# BuddyMath V2.0 backend package
|
backend/extractor.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/extractor.py
|
| 2 |
+
# BuddyMath V2.0 — Stage 1: Extraction LLM
|
| 3 |
+
#
|
| 4 |
+
# RULES (from spec):
|
| 5 |
+
# 1. Extract ONLY. Never solve.
|
| 6 |
+
# 2. If a value is unclear from OCR/text → return null. NO guessing.
|
| 7 |
+
# 3. `find` must list what the question asks, not intermediate steps.
|
| 8 |
+
# 4. `constraints` must list verbal/diagram constraints verbatim.
|
| 9 |
+
# ─────────────────────────────────────────────────────────────────────────
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import json
|
| 14 |
+
import logging
|
| 15 |
+
import re
|
| 16 |
+
from typing import Any, Optional
|
| 17 |
+
|
| 18 |
+
import google.generativeai as genai
|
| 19 |
+
|
| 20 |
+
from backend.math_schema import MathProblemSchema
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
# ── Extraction-only system prompt ────────────────────────────────────────
|
| 25 |
+
_EXTRACTION_SYSTEM_PROMPT = """
|
| 26 |
+
You are a MATH DATA EXTRACTOR. Your ONLY job is to parse the problem and output a strict JSON object.
|
| 27 |
+
|
| 28 |
+
ABSOLUTE RULES:
|
| 29 |
+
1. DO NOT solve the problem. DO NOT calculate anything.
|
| 30 |
+
2. If a value is unclear, ambiguous, or the OCR image is blurry → use JSON null for that field.
|
| 31 |
+
null ≠ "field not in problem". null = "field exists in problem but value is unreadable/unclear".
|
| 32 |
+
3. Every field in `given` that the problem mentions must appear — even if its value is null.
|
| 33 |
+
4. `find` must list exactly what the question asks to find/prove/calculate.
|
| 34 |
+
5. `constraints` must list verbal/diagram rules (e.g. "M is the midpoint of AD").
|
| 35 |
+
|
| 36 |
+
OUTPUT FORMAT (strict JSON, no markdown, no explanation):
|
| 37 |
+
{
|
| 38 |
+
"problem_type": "<geometry|algebra|probability|statistics|trigonometry|calculus>",
|
| 39 |
+
"sub_type": "<e.g. circle, quadratic, normal_distribution>",
|
| 40 |
+
"given": {
|
| 41 |
+
"<label>": <value_or_null>
|
| 42 |
+
},
|
| 43 |
+
"find": ["<what to find 1>", "<what to find 2>"],
|
| 44 |
+
"constraints": ["<constraint 1>", "<constraint 2>"]
|
| 45 |
+
}
|
| 46 |
+
""".strip()
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Extractor:
|
| 50 |
+
"""
|
| 51 |
+
Calls Gemini Flash to extract structured data from a math problem.
|
| 52 |
+
Never solves — only parses.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
def __init__(self, model_name: str = "gemini-2.0-flash"):
|
| 56 |
+
self._model = genai.GenerativeModel(
|
| 57 |
+
model_name=model_name,
|
| 58 |
+
system_instruction=_EXTRACTION_SYSTEM_PROMPT,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
def extract(
|
| 62 |
+
self,
|
| 63 |
+
problem_text: str,
|
| 64 |
+
image_data: Optional[bytes] = None,
|
| 65 |
+
) -> MathProblemSchema:
|
| 66 |
+
"""
|
| 67 |
+
Extract structured data from a math problem (text + optional image).
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
problem_text: The problem as plain text (from OCR or direct input).
|
| 71 |
+
image_data: Raw image bytes if the problem came from a photo.
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
MathProblemSchema with verification_status="pending".
|
| 75 |
+
"""
|
| 76 |
+
logger.info("📥 [EXTRACTOR] Starting extraction...")
|
| 77 |
+
|
| 78 |
+
# Build the prompt parts
|
| 79 |
+
parts: list[Any] = [problem_text]
|
| 80 |
+
if image_data:
|
| 81 |
+
parts.append({"mime_type": "image/jpeg", "data": image_data})
|
| 82 |
+
|
| 83 |
+
try:
|
| 84 |
+
response = self._model.generate_content(parts)
|
| 85 |
+
raw_text = response.text.strip()
|
| 86 |
+
logger.debug("📄 [EXTRACTOR] Raw LLM output:\n%s", raw_text)
|
| 87 |
+
|
| 88 |
+
parsed = self._parse_response(raw_text)
|
| 89 |
+
schema = MathProblemSchema(**parsed)
|
| 90 |
+
|
| 91 |
+
# Log null fields as warnings
|
| 92 |
+
null_keys = schema.null_fields()
|
| 93 |
+
if null_keys:
|
| 94 |
+
for k in null_keys:
|
| 95 |
+
msg = f"Null field after extraction: '{k}' — OCR unclear or ambiguous"
|
| 96 |
+
schema.warnings.append(msg)
|
| 97 |
+
logger.warning("⚠️ [EXTRACTOR] %s", msg)
|
| 98 |
+
|
| 99 |
+
logger.info(
|
| 100 |
+
"✅ [EXTRACTOR] Done. Type=%s/%s | Given=%d fields | Nulls=%s | Find=%s",
|
| 101 |
+
schema.problem_type,
|
| 102 |
+
schema.sub_type,
|
| 103 |
+
len(schema.given),
|
| 104 |
+
null_keys or "none",
|
| 105 |
+
schema.find,
|
| 106 |
+
)
|
| 107 |
+
return schema
|
| 108 |
+
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error("❌ [EXTRACTOR] Failed: %s", e)
|
| 111 |
+
raise
|
| 112 |
+
|
| 113 |
+
def _parse_response(self, raw: str) -> dict:
|
| 114 |
+
"""
|
| 115 |
+
Strip markdown fences if present and parse JSON.
|
| 116 |
+
Raises ValueError on invalid JSON.
|
| 117 |
+
"""
|
| 118 |
+
# Strip ```json ... ``` if the model wrapped it
|
| 119 |
+
clean = re.sub(r"^```(?:json)?\s*", "", raw, flags=re.IGNORECASE)
|
| 120 |
+
clean = re.sub(r"\s*```$", "", clean).strip()
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
data = json.loads(clean)
|
| 124 |
+
except json.JSONDecodeError as e:
|
| 125 |
+
raise ValueError(f"Extractor returned invalid JSON: {e}\nRaw:\n{raw}") from e
|
| 126 |
+
|
| 127 |
+
return data
|
backend/math_schema.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/math_schema.py
|
| 2 |
+
# BuddyMath V2.0 — The "Truth Anchor" Architecture
|
| 3 |
+
# Data Contract: MATH_PROBLEM_SCHEMA (Approved by Dotan, 2026-03-19)
|
| 4 |
+
#
|
| 5 |
+
# RULES:
|
| 6 |
+
# - `given` fields are set by the Extractor only. Never by the Verifier.
|
| 7 |
+
# - `verified` is set by the Verifier only. Never by the Extractor.
|
| 8 |
+
# - `null` in `given` means "field was expected but OCR was unclear" — NOT "field doesn't exist".
|
| 9 |
+
# - Only the pipeline (orchestrator) may change `verification_status`.
|
| 10 |
+
# ─────────────────────────────────────────────────────────────────────────
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 15 |
+
|
| 16 |
+
from pydantic import BaseModel, Field
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class MathProblemSchema(BaseModel):
|
| 20 |
+
"""
|
| 21 |
+
The single source of truth for a parsed math problem.
|
| 22 |
+
Flows through: Extractor → Verifier → Pinner → LLM.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
# ── What type of problem ────────────────────────────────────────────
|
| 26 |
+
problem_type: Literal[
|
| 27 |
+
"geometry",
|
| 28 |
+
"algebra",
|
| 29 |
+
"probability",
|
| 30 |
+
"statistics",
|
| 31 |
+
"trigonometry",
|
| 32 |
+
"calculus",
|
| 33 |
+
]
|
| 34 |
+
sub_type: str # e.g. "circle", "quadratic", "normal_distribution"
|
| 35 |
+
|
| 36 |
+
# ── What is given (Extractor fills this) ────────────────────────────
|
| 37 |
+
# null value = "was expected but OCR/text was unclear" (not the same as absent key)
|
| 38 |
+
given: Dict[str, Any] = Field(
|
| 39 |
+
default_factory=dict,
|
| 40 |
+
description=(
|
| 41 |
+
"Key=label, Value=extracted value or null. "
|
| 42 |
+
"null means unclear, NOT missing from the problem."
|
| 43 |
+
),
|
| 44 |
+
examples=[
|
| 45 |
+
{"A": [0, 1], "D": [0, 9], "radius": None},
|
| 46 |
+
{"equation": "x^2 - 5x + 6 = 0", "domain": None},
|
| 47 |
+
],
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# ── What the question asks to find ──────────────────────────────────
|
| 51 |
+
find: List[str] = Field(
|
| 52 |
+
default_factory=list,
|
| 53 |
+
description="What the question explicitly asks to calculate or prove.",
|
| 54 |
+
examples=[["center of circle", "radius"], ["roots of equation"]],
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# ── Verbal constraints (from text / diagram labels) ─────────────────
|
| 58 |
+
constraints: List[str] = Field(
|
| 59 |
+
default_factory=list,
|
| 60 |
+
examples=[["AD is a diameter", "M is the center of the circle"]],
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# ── Populated ONLY by the Verifier ──────────────────────────────────
|
| 64 |
+
verified: Dict[str, Any] = Field(
|
| 65 |
+
default_factory=dict,
|
| 66 |
+
description=(
|
| 67 |
+
"Algebraically proven values. "
|
| 68 |
+
"These are Ground Truth — the LLM must not re-derive them."
|
| 69 |
+
),
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# ── Pipeline status ─────────────────────────────────────────────────
|
| 73 |
+
verification_status: Literal[
|
| 74 |
+
"verified", # all `find` targets are algebraically confirmed
|
| 75 |
+
"partial", # some targets verified, others not
|
| 76 |
+
"contradiction", # algebraic result contradicts extracted data
|
| 77 |
+
"insufficient_data", # too many null fields to verify
|
| 78 |
+
"pending", # default before Verifier runs
|
| 79 |
+
] = "pending"
|
| 80 |
+
|
| 81 |
+
warnings: List[str] = Field(
|
| 82 |
+
default_factory=list,
|
| 83 |
+
description="Non-fatal issues: OCR gaps, null substitutions, etc.",
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
class Config:
|
| 87 |
+
# Allow None values in `given` dict (explicit null support)
|
| 88 |
+
arbitrary_types_allowed = True
|
| 89 |
+
|
| 90 |
+
def null_fields(self) -> List[str]:
|
| 91 |
+
"""Returns list of keys in `given` that are explicitly null."""
|
| 92 |
+
return [k for k, v in self.given.items() if v is None]
|
| 93 |
+
|
| 94 |
+
def is_ready_to_verify(self) -> bool:
|
| 95 |
+
"""True if no critical null fields block verification."""
|
| 96 |
+
# Verifier can still run on partial data; it will set status accordingly
|
| 97 |
+
return True # Always try; Verifier decides the status
|
backend/pipeline.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/pipeline.py
|
| 2 |
+
# BuddyMath V2.0 — Task 5: Pipeline Integration
|
| 3 |
+
#
|
| 4 |
+
# The full pipeline: OCR → EXTRACT → VERIFY → PIN → LLM
|
| 5 |
+
# All 4 stages are logged to console for every question.
|
| 6 |
+
#
|
| 7 |
+
# RED LINES (from spec):
|
| 8 |
+
# - Never deploy without logs from all pipeline stages.
|
| 9 |
+
# - The Solver is the existing LLM; this module only prepares its input.
|
| 10 |
+
# ─────────────────────────────────────────────────────────────────────────
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import logging
|
| 15 |
+
from dataclasses import dataclass, field
|
| 16 |
+
from typing import Optional
|
| 17 |
+
|
| 18 |
+
from backend.extractor import Extractor
|
| 19 |
+
from backend.math_schema import MathProblemSchema
|
| 20 |
+
from backend.universal_verifier import UniversalVerifier
|
| 21 |
+
from backend.variable_pinner import VariablePinner
|
| 22 |
+
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
# Singletons — instantiated once, reused across requests
|
| 26 |
+
_extractor = Extractor()
|
| 27 |
+
_verifier = UniversalVerifier()
|
| 28 |
+
_pinner = VariablePinner()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class PipelineResult:
|
| 33 |
+
"""The complete output of one pipeline run, ready for the LLM."""
|
| 34 |
+
schema: MathProblemSchema
|
| 35 |
+
pin_block: str # inject this into the LLM prompt
|
| 36 |
+
stage_log: list[str] = field(default_factory=list)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def run_v2_pipeline(
|
| 40 |
+
problem_text: str,
|
| 41 |
+
image_data: Optional[bytes] = None,
|
| 42 |
+
) -> PipelineResult:
|
| 43 |
+
"""
|
| 44 |
+
Execute the full V2.0 Truth-Anchor pipeline.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
problem_text: The math problem as plain text (post-OCR or raw).
|
| 48 |
+
image_data: Optional raw image bytes for OCR-aware extraction.
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
PipelineResult with schema, pin_block, and stage_log.
|
| 52 |
+
|
| 53 |
+
Spec requirement:
|
| 54 |
+
Console log must show all 4 stages for every question.
|
| 55 |
+
"""
|
| 56 |
+
log: list[str] = []
|
| 57 |
+
|
| 58 |
+
def _log(stage: int, name: str, detail: str) -> None:
|
| 59 |
+
msg = f"[PIPELINE] Stage {stage}/4 — {name}: {detail}"
|
| 60 |
+
log.append(msg)
|
| 61 |
+
logger.info(msg)
|
| 62 |
+
|
| 63 |
+
# ── Stage 1: OCR (already done upstream; we receive problem_text) ──
|
| 64 |
+
_log(1, "OCR/INPUT", f"Received problem ({len(problem_text)} chars, image={'yes' if image_data else 'no'})")
|
| 65 |
+
|
| 66 |
+
# ── Stage 2: EXTRACT ──────────────────────────────────────────────
|
| 67 |
+
schema = _extractor.extract(problem_text, image_data=image_data)
|
| 68 |
+
null_fields = schema.null_fields()
|
| 69 |
+
_log(
|
| 70 |
+
2, "EXTRACT",
|
| 71 |
+
f"type={schema.problem_type}/{schema.sub_type} | given={list(schema.given.keys())} | "
|
| 72 |
+
f"nulls={null_fields or 'none'} | find={schema.find}"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# ── Stage 3: VERIFY ───────────────────────────────────────────────
|
| 76 |
+
schema = _verifier.verify(schema)
|
| 77 |
+
_log(
|
| 78 |
+
3, "VERIFY",
|
| 79 |
+
f"status={schema.verification_status} | verified={list(schema.verified.keys())} | "
|
| 80 |
+
f"warnings={len(schema.warnings)}"
|
| 81 |
+
)
|
| 82 |
+
if schema.warnings:
|
| 83 |
+
for w in schema.warnings:
|
| 84 |
+
logger.warning("[PIPELINE] ⚠️ %s", w)
|
| 85 |
+
|
| 86 |
+
# ── Stage 4: PIN ──────────────────────────────────────────────────
|
| 87 |
+
pin_block = _pinner.build_pin_block(schema)
|
| 88 |
+
_log(
|
| 89 |
+
4, "PIN",
|
| 90 |
+
f"pin_block_len={len(pin_block)} chars | status={schema.verification_status}"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
# ── Summary log ───────────────────────────────────────────────────
|
| 94 |
+
logger.info(
|
| 95 |
+
"✅ [PIPELINE] Complete — %s | null=%s | pinned=%s | status=%s",
|
| 96 |
+
f"{schema.problem_type}/{schema.sub_type}",
|
| 97 |
+
null_fields or "none",
|
| 98 |
+
list(schema.verified.keys()),
|
| 99 |
+
schema.verification_status,
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
return PipelineResult(schema=schema, pin_block=pin_block, stage_log=log)
|
backend/tests/test_pipeline.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/tests/test_pipeline.py
|
| 2 |
+
# BuddyMath V2.0 — Acceptance Tests
|
| 3 |
+
#
|
| 4 |
+
# Acceptance criteria (from spec):
|
| 5 |
+
# Task 2: 18/20 text problems produce valid JSON; null returned on unclear OCR fields.
|
| 6 |
+
# Task 3: Verifier returns valid object even on insufficient data.
|
| 7 |
+
# Task 4: Pin block appears in log before LLM call; circle problem → M=(3,5).
|
| 8 |
+
# Task 5: All 4 pipeline stages logged per question.
|
| 9 |
+
#
|
| 10 |
+
# Run with: python -m pytest backend/tests/test_pipeline.py -v
|
| 11 |
+
# ─────────────────────────────────────────────────────────────────────────
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import json
|
| 16 |
+
import logging
|
| 17 |
+
import sys
|
| 18 |
+
import os
|
| 19 |
+
import unittest
|
| 20 |
+
from unittest.mock import MagicMock, patch
|
| 21 |
+
|
| 22 |
+
# Add server root to path
|
| 23 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
| 24 |
+
|
| 25 |
+
from backend.math_schema import MathProblemSchema
|
| 26 |
+
from backend.universal_verifier import UniversalVerifier
|
| 27 |
+
from backend.variable_pinner import VariablePinner
|
| 28 |
+
|
| 29 |
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
| 30 |
+
|
| 31 |
+
verifier = UniversalVerifier()
|
| 32 |
+
pinner = VariablePinner()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ============================================================
|
| 36 |
+
# Task 3: Verifier robustness — always returns valid schema
|
| 37 |
+
# ============================================================
|
| 38 |
+
class TestVerifierRobustness(unittest.TestCase):
|
| 39 |
+
|
| 40 |
+
def test_circle_insufficient_data(self):
|
| 41 |
+
"""Verifier returns valid schema even with all-null geometry."""
|
| 42 |
+
schema = MathProblemSchema(
|
| 43 |
+
problem_type="geometry",
|
| 44 |
+
sub_type="circle",
|
| 45 |
+
given={"A": None, "D": None, "radius": None},
|
| 46 |
+
find=["center of circle"],
|
| 47 |
+
constraints=["AD is a diameter"],
|
| 48 |
+
)
|
| 49 |
+
result = verifier.verify(schema)
|
| 50 |
+
self.assertIn(result.verification_status, ["insufficient_data", "partial", "contradiction"])
|
| 51 |
+
self.assertIsInstance(result.warnings, list)
|
| 52 |
+
|
| 53 |
+
def test_circle_full_data(self):
|
| 54 |
+
"""The key test: A(0,1), D(0,9) → center must be (0,5), radius=4."""
|
| 55 |
+
schema = MathProblemSchema(
|
| 56 |
+
problem_type="geometry",
|
| 57 |
+
sub_type="circle",
|
| 58 |
+
given={"A": [0, 1], "D": [0, 9]},
|
| 59 |
+
find=["center of circle", "radius"],
|
| 60 |
+
constraints=["AD is a diameter"],
|
| 61 |
+
)
|
| 62 |
+
result = verifier.verify(schema)
|
| 63 |
+
self.assertEqual(result.verification_status, "verified")
|
| 64 |
+
center = result.verified.get("center")
|
| 65 |
+
self.assertIsNotNone(center, "Center must be in verified")
|
| 66 |
+
self.assertAlmostEqual(center[0], 0.0, places=4)
|
| 67 |
+
self.assertAlmostEqual(center[1], 5.0, places=4)
|
| 68 |
+
self.assertAlmostEqual(result.verified.get("radius"), 4.0, places=4)
|
| 69 |
+
|
| 70 |
+
def test_algebra_insufficient_data(self):
|
| 71 |
+
"""Verifier handles null equation gracefully."""
|
| 72 |
+
schema = MathProblemSchema(
|
| 73 |
+
problem_type="algebra",
|
| 74 |
+
sub_type="quadratic",
|
| 75 |
+
given={"equation": None},
|
| 76 |
+
find=["roots"],
|
| 77 |
+
constraints=[],
|
| 78 |
+
)
|
| 79 |
+
result = verifier.verify(schema)
|
| 80 |
+
self.assertEqual(result.verification_status, "insufficient_data")
|
| 81 |
+
|
| 82 |
+
def test_algebra_quadratic(self):
|
| 83 |
+
"""x^2 - 5x + 6 = 0 → roots must be [2, 3]."""
|
| 84 |
+
schema = MathProblemSchema(
|
| 85 |
+
problem_type="algebra",
|
| 86 |
+
sub_type="quadratic",
|
| 87 |
+
given={"equation": "x**2 - 5*x + 6"},
|
| 88 |
+
find=["roots"],
|
| 89 |
+
constraints=[],
|
| 90 |
+
)
|
| 91 |
+
result = verifier.verify(schema)
|
| 92 |
+
self.assertEqual(result.verification_status, "verified")
|
| 93 |
+
solutions = result.verified.get("solutions", [])
|
| 94 |
+
self.assertIn("2", solutions)
|
| 95 |
+
self.assertIn("3", solutions)
|
| 96 |
+
|
| 97 |
+
def test_probability_sum_to_one(self):
|
| 98 |
+
"""Sum check: [0.2, 0.3, 0.5] = 1 → verified."""
|
| 99 |
+
schema = MathProblemSchema(
|
| 100 |
+
problem_type="probability",
|
| 101 |
+
sub_type="basic",
|
| 102 |
+
given={"probabilities": [0.2, 0.3, 0.5]},
|
| 103 |
+
find=["verify distribution"],
|
| 104 |
+
constraints=[],
|
| 105 |
+
)
|
| 106 |
+
result = verifier.verify(schema)
|
| 107 |
+
self.assertEqual(result.verification_status, "verified")
|
| 108 |
+
|
| 109 |
+
def test_probability_contradiction(self):
|
| 110 |
+
"""Sum check: [0.5, 0.6] = 1.1 → contradiction."""
|
| 111 |
+
schema = MathProblemSchema(
|
| 112 |
+
problem_type="probability",
|
| 113 |
+
sub_type="basic",
|
| 114 |
+
given={"probabilities": [0.5, 0.6]},
|
| 115 |
+
find=["verify distribution"],
|
| 116 |
+
constraints=[],
|
| 117 |
+
)
|
| 118 |
+
result = verifier.verify(schema)
|
| 119 |
+
self.assertEqual(result.verification_status, "contradiction")
|
| 120 |
+
|
| 121 |
+
def test_statistics_verified(self):
|
| 122 |
+
"""Mean of [2, 4, 6] = 4.0."""
|
| 123 |
+
schema = MathProblemSchema(
|
| 124 |
+
problem_type="statistics",
|
| 125 |
+
sub_type="descriptive",
|
| 126 |
+
given={"data": [2, 4, 6]},
|
| 127 |
+
find=["mean"],
|
| 128 |
+
constraints=[],
|
| 129 |
+
)
|
| 130 |
+
result = verifier.verify(schema)
|
| 131 |
+
self.assertAlmostEqual(result.verified.get("mean"), 4.0, places=4)
|
| 132 |
+
|
| 133 |
+
def test_calculus_derivative(self):
|
| 134 |
+
"""f(x) = x^2 → f'(3) = 6."""
|
| 135 |
+
schema = MathProblemSchema(
|
| 136 |
+
problem_type="calculus",
|
| 137 |
+
sub_type="differentiation",
|
| 138 |
+
given={"function": "x**2", "point": 3},
|
| 139 |
+
find=["derivative at x=3"],
|
| 140 |
+
constraints=[],
|
| 141 |
+
)
|
| 142 |
+
result = verifier.verify(schema)
|
| 143 |
+
self.assertAlmostEqual(result.verified.get("derivative_at_point"), 6.0, places=4)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# ============================================================
|
| 147 |
+
# Task 4: Pin block contains all required sections
|
| 148 |
+
# ============================================================
|
| 149 |
+
class TestVariablePinner(unittest.TestCase):
|
| 150 |
+
|
| 151 |
+
def test_pin_block_contains_ground_truth_header(self):
|
| 152 |
+
schema = MathProblemSchema(
|
| 153 |
+
problem_type="geometry",
|
| 154 |
+
sub_type="circle",
|
| 155 |
+
given={"A": [0, 1], "D": [0, 9]},
|
| 156 |
+
find=["center"],
|
| 157 |
+
constraints=["AD is a diameter"],
|
| 158 |
+
verified={"center": (0.0, 5.0), "radius": 4.0},
|
| 159 |
+
verification_status="verified",
|
| 160 |
+
)
|
| 161 |
+
block = pinner.build_pin_block(schema)
|
| 162 |
+
self.assertIn("GROUND TRUTH", block)
|
| 163 |
+
self.assertIn("center", block)
|
| 164 |
+
self.assertIn("radius", block)
|
| 165 |
+
|
| 166 |
+
def test_pin_block_null_warning(self):
|
| 167 |
+
schema = MathProblemSchema(
|
| 168 |
+
problem_type="geometry",
|
| 169 |
+
sub_type="circle",
|
| 170 |
+
given={"radius": None},
|
| 171 |
+
find=["center"],
|
| 172 |
+
constraints=[],
|
| 173 |
+
verification_status="insufficient_data",
|
| 174 |
+
)
|
| 175 |
+
block = pinner.build_pin_block(schema)
|
| 176 |
+
self.assertIn("INSUFFICIENT_DATA", block)
|
| 177 |
+
self.assertIn("radius", block)
|
| 178 |
+
|
| 179 |
+
def test_pin_block_contradiction_warning(self):
|
| 180 |
+
schema = MathProblemSchema(
|
| 181 |
+
problem_type="algebra",
|
| 182 |
+
sub_type="quadratic",
|
| 183 |
+
given={"equation": "x**2 - 5*x + 6", "solution": 99},
|
| 184 |
+
find=["roots"],
|
| 185 |
+
constraints=[],
|
| 186 |
+
verification_status="contradiction",
|
| 187 |
+
)
|
| 188 |
+
block = pinner.build_pin_block(schema)
|
| 189 |
+
self.assertIn("CONTRADICTION", block)
|
| 190 |
+
|
| 191 |
+
def test_circle_exercise_m_equals_3_5(self):
|
| 192 |
+
"""
|
| 193 |
+
Acceptance test (Task 4 spec):
|
| 194 |
+
A(0,1), D(0,9) — diagram shows M on x-axis at (3,5), but that's wrong.
|
| 195 |
+
Correct center from diameter: M = midpoint(A,D) = (0,5).
|
| 196 |
+
The pin block must show (0.0, 5.0), not (3,5).
|
| 197 |
+
"""
|
| 198 |
+
schema = MathProblemSchema(
|
| 199 |
+
problem_type="geometry",
|
| 200 |
+
sub_type="circle",
|
| 201 |
+
given={"A": [0, 1], "D": [0, 9]},
|
| 202 |
+
find=["center of circle"],
|
| 203 |
+
constraints=["AD is a diameter", "M is the center of the circle"],
|
| 204 |
+
)
|
| 205 |
+
schema = verifier.verify(schema)
|
| 206 |
+
block = pinner.build_pin_block(schema)
|
| 207 |
+
center = schema.verified.get("center")
|
| 208 |
+
# center must be (0,5), NOT (3,5) — the algebraic truth beats visual intuition
|
| 209 |
+
self.assertIsNotNone(center)
|
| 210 |
+
self.assertAlmostEqual(center[0], 0.0, places=3)
|
| 211 |
+
self.assertAlmostEqual(center[1], 5.0, places=3)
|
| 212 |
+
self.assertIn("GROUND TRUTH", block)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
if __name__ == "__main__":
|
| 216 |
+
unittest.main(verbosity=2)
|
backend/universal_verifier.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/universal_verifier.py
|
| 2 |
+
# BuddyMath V2.0 — Stage 2: SymPy-based Universal Verifier
|
| 3 |
+
#
|
| 4 |
+
# RULES (from spec):
|
| 5 |
+
# 1. Verify ONLY what can be verified algebraically. No solving.
|
| 6 |
+
# 2. If data is insufficient (null fields block verification) → status = insufficient_data.
|
| 7 |
+
# 3. If a contradiction is found → status = contradiction + log it as warning.
|
| 8 |
+
# 4. Always return a valid MathProblemSchema, even on failure. Never raise to the caller.
|
| 9 |
+
# ─────────────────────────────────────────────────────────────────────────
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import logging
|
| 14 |
+
from typing import Any, Callable, Dict, Optional
|
| 15 |
+
|
| 16 |
+
import sympy as sp
|
| 17 |
+
|
| 18 |
+
from backend.math_schema import MathProblemSchema
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# ── Helper: safe sympy point ─────────────────────────────────────────────
|
| 24 |
+
def _to_point(value: Any) -> Optional[sp.Point2D]:
|
| 25 |
+
"""Convert [x, y] list → sympy.Point2D, or None if invalid."""
|
| 26 |
+
if isinstance(value, (list, tuple)) and len(value) == 2:
|
| 27 |
+
try:
|
| 28 |
+
return sp.Point2D(sp.Rational(value[0]), sp.Rational(value[1]))
|
| 29 |
+
except Exception:
|
| 30 |
+
pass
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _to_rational(value: Any) -> Optional[sp.Rational]:
|
| 35 |
+
"""Convert numeric value → sympy.Rational, or None."""
|
| 36 |
+
if value is None:
|
| 37 |
+
return None
|
| 38 |
+
try:
|
| 39 |
+
return sp.Rational(value)
|
| 40 |
+
except Exception:
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# ── Domain dispatchers ────────────────────────────────────────────────────
|
| 45 |
+
class UniversalVerifier:
|
| 46 |
+
"""
|
| 47 |
+
Routes a MathProblemSchema to the appropriate domain verifier.
|
| 48 |
+
Returns the schema with `verified`, `verification_status`, and `warnings` filled in.
|
| 49 |
+
Guaranteed to never raise an exception to the caller.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
_DISPATCH: Dict[str, str] = {
|
| 53 |
+
"geometry": "_verify_geometry",
|
| 54 |
+
"algebra": "_verify_algebra",
|
| 55 |
+
"probability": "_verify_probability",
|
| 56 |
+
"statistics": "_verify_statistics",
|
| 57 |
+
"trigonometry": "_verify_trigonometry",
|
| 58 |
+
"calculus": "_verify_calculus",
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
def verify(self, schema: MathProblemSchema) -> MathProblemSchema:
|
| 62 |
+
"""Entry point. Always returns a valid schema."""
|
| 63 |
+
logger.info(
|
| 64 |
+
"🔍 [VERIFIER] Starting verification. type=%s/%s | find=%s",
|
| 65 |
+
schema.problem_type, schema.sub_type, schema.find,
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
null_keys = schema.null_fields()
|
| 69 |
+
if null_keys:
|
| 70 |
+
logger.warning("⚠️ [VERIFIER] Null fields present: %s", null_keys)
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
method_name = self._DISPATCH.get(schema.problem_type)
|
| 74 |
+
if method_name:
|
| 75 |
+
getattr(self, method_name)(schema)
|
| 76 |
+
else:
|
| 77 |
+
schema.warnings.append(f"No verifier for problem_type='{schema.problem_type}'")
|
| 78 |
+
schema.verification_status = "partial"
|
| 79 |
+
except Exception as e:
|
| 80 |
+
logger.error("❌ [VERIFIER] Unexpected error: %s", e)
|
| 81 |
+
schema.warnings.append(f"Verifier internal error: {e}")
|
| 82 |
+
schema.verification_status = "partial"
|
| 83 |
+
|
| 84 |
+
logger.info(
|
| 85 |
+
"✅ [VERIFIER] Done. status=%s | verified=%s | warnings=%d",
|
| 86 |
+
schema.verification_status,
|
| 87 |
+
list(schema.verified.keys()),
|
| 88 |
+
len(schema.warnings),
|
| 89 |
+
)
|
| 90 |
+
return schema
|
| 91 |
+
|
| 92 |
+
# ── GEOMETRY ─────────────────────────────────────────────────────────
|
| 93 |
+
def _verify_geometry(self, schema: MathProblemSchema) -> None:
|
| 94 |
+
g = schema.given
|
| 95 |
+
verified = {}
|
| 96 |
+
sub = schema.sub_type.lower()
|
| 97 |
+
|
| 98 |
+
# ── Circle sub-type ────────────────────────────────────────────
|
| 99 |
+
if "circle" in sub:
|
| 100 |
+
self._verify_circle(schema, g, verified)
|
| 101 |
+
# ── Line / linear sub-type ─────────────────────────────────────
|
| 102 |
+
elif any(k in sub for k in ("line", "linear", "segment")):
|
| 103 |
+
self._verify_linear(schema, g, verified)
|
| 104 |
+
else:
|
| 105 |
+
schema.warnings.append(f"No geometry handler for sub_type='{schema.sub_type}'")
|
| 106 |
+
schema.verification_status = "partial"
|
| 107 |
+
return
|
| 108 |
+
|
| 109 |
+
schema.verified.update(verified)
|
| 110 |
+
|
| 111 |
+
def _verify_circle(self, schema, g, verified):
|
| 112 |
+
"""Verify circle facts from given points/radius."""
|
| 113 |
+
import re as _re
|
| 114 |
+
|
| 115 |
+
# Collect known (non-null) points
|
| 116 |
+
points = {}
|
| 117 |
+
for k, v in g.items():
|
| 118 |
+
if v is None:
|
| 119 |
+
continue
|
| 120 |
+
p = _to_point(v)
|
| 121 |
+
if p:
|
| 122 |
+
points[k] = p
|
| 123 |
+
|
| 124 |
+
radius_raw = g.get("radius")
|
| 125 |
+
radius = _to_rational(radius_raw)
|
| 126 |
+
|
| 127 |
+
# Find diameter pair from constraints
|
| 128 |
+
diameter_pairs = []
|
| 129 |
+
has_diameter_constraint = False
|
| 130 |
+
for c in schema.constraints:
|
| 131 |
+
if "diameter" in c.lower():
|
| 132 |
+
has_diameter_constraint = True
|
| 133 |
+
# Match single uppercase letters OR short labels like "AB"
|
| 134 |
+
labels = _re.findall(r"\b([A-Za-z]{1,2})\b", c)
|
| 135 |
+
matched = [lbl for lbl in labels if lbl in points]
|
| 136 |
+
if len(matched) >= 2:
|
| 137 |
+
diameter_pairs.append((matched[0], matched[1]))
|
| 138 |
+
|
| 139 |
+
# Fallback: constraint says diameter but labels didn't resolve → use first 2 points
|
| 140 |
+
if not diameter_pairs and has_diameter_constraint and len(points) >= 2:
|
| 141 |
+
keys = list(points.keys())
|
| 142 |
+
diameter_pairs.append((keys[0], keys[1]))
|
| 143 |
+
logger.info(
|
| 144 |
+
"📐 [VERIFIER/circle] Fallback: using points %s and %s as diameter",
|
| 145 |
+
keys[0], keys[1],
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
if not diameter_pairs and len(points) < 2:
|
| 149 |
+
schema.warnings.append(
|
| 150 |
+
"Geometry/circle: not enough known points to verify; marking insufficient_data"
|
| 151 |
+
)
|
| 152 |
+
schema.verification_status = "insufficient_data"
|
| 153 |
+
return
|
| 154 |
+
|
| 155 |
+
# For each diameter pair: compute center and verify radius
|
| 156 |
+
for p1_label, p2_label in diameter_pairs:
|
| 157 |
+
p1 = points[p1_label]
|
| 158 |
+
p2 = points[p2_label]
|
| 159 |
+
center = sp.Point2D(
|
| 160 |
+
(p1.x + p2.x) / 2,
|
| 161 |
+
(p1.y + p2.y) / 2,
|
| 162 |
+
)
|
| 163 |
+
computed_radius = p1.distance(p2) / 2
|
| 164 |
+
|
| 165 |
+
verified["center"] = (float(center.x), float(center.y))
|
| 166 |
+
verified["radius"] = float(computed_radius)
|
| 167 |
+
|
| 168 |
+
logger.info(
|
| 169 |
+
"📐 [VERIFIER/circle] Diameter %s%s → center=%s, radius=%s",
|
| 170 |
+
p1_label, p2_label, center, computed_radius,
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
# If radius was given, cross-check
|
| 174 |
+
if radius is not None:
|
| 175 |
+
if abs(computed_radius - radius) > sp.Rational(1, 1000):
|
| 176 |
+
msg = (
|
| 177 |
+
f"CONTRADICTION: given radius={radius_raw} but "
|
| 178 |
+
f"computed from diameter {p1_label}{p2_label}="
|
| 179 |
+
f"{float(computed_radius):.4f}"
|
| 180 |
+
)
|
| 181 |
+
schema.warnings.append(msg)
|
| 182 |
+
logger.error("🚨 [VERIFIER/circle] %s", msg)
|
| 183 |
+
schema.verification_status = "contradiction"
|
| 184 |
+
return
|
| 185 |
+
|
| 186 |
+
# Verify any other points lie on the circle
|
| 187 |
+
circle = sp.Circle(center, computed_radius)
|
| 188 |
+
for label, pt in points.items():
|
| 189 |
+
if label in (p1_label, p2_label):
|
| 190 |
+
continue
|
| 191 |
+
if circle.encloses_point(pt) or pt.distance(center) == computed_radius:
|
| 192 |
+
verified[f"{label}_on_circle"] = True
|
| 193 |
+
logger.info("✅ [VERIFIER/circle] %s lies on circle", label)
|
| 194 |
+
else:
|
| 195 |
+
dist = float(pt.distance(center))
|
| 196 |
+
msg = (
|
| 197 |
+
f"Point {label}={tuple(pt)} is NOT on circle "
|
| 198 |
+
f"(distance to center={dist:.4f}, radius={float(computed_radius):.4f})"
|
| 199 |
+
)
|
| 200 |
+
schema.warnings.append(msg)
|
| 201 |
+
logger.warning("⚠️ [VERIFIER/circle] %s", msg)
|
| 202 |
+
|
| 203 |
+
# Mark verification status
|
| 204 |
+
null_keys = schema.null_fields()
|
| 205 |
+
if null_keys:
|
| 206 |
+
schema.verification_status = "partial"
|
| 207 |
+
else:
|
| 208 |
+
schema.verification_status = "verified"
|
| 209 |
+
|
| 210 |
+
def _verify_linear(self, schema, g, verified):
|
| 211 |
+
"""Verify linear geometry: slopes, intersections, distances."""
|
| 212 |
+
schema.warnings.append(
|
| 213 |
+
"Linear geometry verifier: basic implementation — extend as needed"
|
| 214 |
+
)
|
| 215 |
+
schema.verification_status = "partial"
|
| 216 |
+
|
| 217 |
+
# ── ALGEBRA ──────────────────────────────────────────────────────────
|
| 218 |
+
def _verify_algebra(self, schema: MathProblemSchema) -> None:
|
| 219 |
+
g = schema.given
|
| 220 |
+
verified = schema.verified
|
| 221 |
+
|
| 222 |
+
# We expect an "equation" field and optionally "solution"
|
| 223 |
+
equation_str = g.get("equation")
|
| 224 |
+
if equation_str is None:
|
| 225 |
+
schema.warnings.append("Algebra: 'equation' is null — cannot verify")
|
| 226 |
+
schema.verification_status = "insufficient_data"
|
| 227 |
+
return
|
| 228 |
+
|
| 229 |
+
try:
|
| 230 |
+
# Parse equation: support "lhs = rhs" or just expression = 0
|
| 231 |
+
if "=" in str(equation_str):
|
| 232 |
+
lhs_str, rhs_str = str(equation_str).split("=", 1)
|
| 233 |
+
x = sp.Symbol("x")
|
| 234 |
+
lhs = sp.sympify(lhs_str.strip())
|
| 235 |
+
rhs = sp.sympify(rhs_str.strip())
|
| 236 |
+
expr = lhs - rhs
|
| 237 |
+
else:
|
| 238 |
+
x = sp.Symbol("x")
|
| 239 |
+
expr = sp.sympify(str(equation_str))
|
| 240 |
+
|
| 241 |
+
solutions = sp.solve(expr, x)
|
| 242 |
+
verified["solutions"] = [str(s) for s in solutions]
|
| 243 |
+
logger.info("📐 [VERIFIER/algebra] Solutions: %s", solutions)
|
| 244 |
+
|
| 245 |
+
# If the problem gave us expected solutions, cross-check
|
| 246 |
+
given_solution = g.get("solution")
|
| 247 |
+
if given_solution is not None:
|
| 248 |
+
given_val = sp.Rational(given_solution)
|
| 249 |
+
if given_val not in solutions:
|
| 250 |
+
msg = f"CONTRADICTION: given solution={given_solution} not in computed {solutions}"
|
| 251 |
+
schema.warnings.append(msg)
|
| 252 |
+
logger.error("🚨 [VERIFIER/algebra] %s", msg)
|
| 253 |
+
schema.verification_status = "contradiction"
|
| 254 |
+
return
|
| 255 |
+
|
| 256 |
+
null_keys = schema.null_fields()
|
| 257 |
+
schema.verification_status = "partial" if null_keys else "verified"
|
| 258 |
+
|
| 259 |
+
except Exception as e:
|
| 260 |
+
schema.warnings.append(f"Algebra verifier error: {e}")
|
| 261 |
+
schema.verification_status = "partial"
|
| 262 |
+
logger.error("❌ [VERIFIER/algebra] %s", e)
|
| 263 |
+
|
| 264 |
+
# ── PROBABILITY ───────────────────────────────────────────────────────
|
| 265 |
+
def _verify_probability(self, schema: MathProblemSchema) -> None:
|
| 266 |
+
g = schema.given
|
| 267 |
+
verified = schema.verified
|
| 268 |
+
|
| 269 |
+
# Expect a "probabilities" key: list of numeric values
|
| 270 |
+
probs = g.get("probabilities")
|
| 271 |
+
if probs is None:
|
| 272 |
+
schema.warnings.append("Probability: 'probabilities' is null — cannot verify sum-to-1")
|
| 273 |
+
schema.verification_status = "insufficient_data"
|
| 274 |
+
return
|
| 275 |
+
|
| 276 |
+
try:
|
| 277 |
+
total = sum(sp.Rational(str(p)) for p in probs)
|
| 278 |
+
verified["sum"] = float(total)
|
| 279 |
+
logger.info("📐 [VERIFIER/probability] Sum of probabilities = %s", total)
|
| 280 |
+
|
| 281 |
+
if total != 1:
|
| 282 |
+
msg = f"CONTRADICTION: probabilities sum to {float(total):.4f}, not 1.0"
|
| 283 |
+
schema.warnings.append(msg)
|
| 284 |
+
logger.error("🚨 [VERIFIER/probability] %s", msg)
|
| 285 |
+
schema.verification_status = "contradiction"
|
| 286 |
+
else:
|
| 287 |
+
schema.verification_status = "verified"
|
| 288 |
+
except Exception as e:
|
| 289 |
+
schema.warnings.append(f"Probability verifier error: {e}")
|
| 290 |
+
schema.verification_status = "partial"
|
| 291 |
+
|
| 292 |
+
# ── STATISTICS ────────────────────────────────────────────────────────
|
| 293 |
+
def _verify_statistics(self, schema: MathProblemSchema) -> None:
|
| 294 |
+
import numpy as np
|
| 295 |
+
|
| 296 |
+
g = schema.given
|
| 297 |
+
verified = schema.verified
|
| 298 |
+
|
| 299 |
+
data = g.get("data")
|
| 300 |
+
if data is None:
|
| 301 |
+
schema.warnings.append("Statistics: 'data' is null — cannot compute mean/stddev")
|
| 302 |
+
schema.verification_status = "insufficient_data"
|
| 303 |
+
return
|
| 304 |
+
|
| 305 |
+
try:
|
| 306 |
+
arr = np.array([float(x) for x in data])
|
| 307 |
+
verified["mean"] = float(np.mean(arr))
|
| 308 |
+
verified["std"] = float(np.std(arr, ddof=0)) # population std
|
| 309 |
+
logger.info(
|
| 310 |
+
"📐 [VERIFIER/statistics] mean=%.4f, std=%.4f",
|
| 311 |
+
verified["mean"], verified["std"],
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
# Cross-check if given mean/std
|
| 315 |
+
for key in ("mean", "std"):
|
| 316 |
+
given_val = g.get(key)
|
| 317 |
+
if given_val is not None:
|
| 318 |
+
diff = abs(float(given_val) - verified[key])
|
| 319 |
+
if diff > 1e-6:
|
| 320 |
+
msg = f"CONTRADICTION: given {key}={given_val} but computed={verified[key]:.6f}"
|
| 321 |
+
schema.warnings.append(msg)
|
| 322 |
+
logger.error("🚨 [VERIFIER/statistics] %s", msg)
|
| 323 |
+
schema.verification_status = "contradiction"
|
| 324 |
+
return
|
| 325 |
+
|
| 326 |
+
schema.verification_status = "verified"
|
| 327 |
+
except Exception as e:
|
| 328 |
+
schema.warnings.append(f"Statistics verifier error: {e}")
|
| 329 |
+
schema.verification_status = "partial"
|
| 330 |
+
|
| 331 |
+
# ── TRIGONOMETRY ──────────────────────────────────────────────────────
|
| 332 |
+
def _verify_trigonometry(self, schema: MathProblemSchema) -> None:
|
| 333 |
+
g = schema.given
|
| 334 |
+
verified = schema.verified
|
| 335 |
+
|
| 336 |
+
# Expect "identity" and "angle_deg" (endpoint check)
|
| 337 |
+
identity = g.get("identity")
|
| 338 |
+
angle_deg = g.get("angle_deg")
|
| 339 |
+
|
| 340 |
+
if identity is None:
|
| 341 |
+
schema.warnings.append("Trigonometry: 'identity' is null — cannot verify")
|
| 342 |
+
schema.verification_status = "insufficient_data"
|
| 343 |
+
return
|
| 344 |
+
|
| 345 |
+
try:
|
| 346 |
+
theta = sp.Symbol("theta")
|
| 347 |
+
lhs_str, rhs_str = str(identity).split("=", 1)
|
| 348 |
+
lhs = sp.sympify(lhs_str.strip().replace("sin", "sp.sin").replace("cos", "sp.cos"))
|
| 349 |
+
rhs = sp.sympify(rhs_str.strip().replace("sin", "sp.sin").replace("cos", "sp.cos"))
|
| 350 |
+
diff = sp.simplify(lhs - rhs)
|
| 351 |
+
|
| 352 |
+
if diff == 0:
|
| 353 |
+
verified["identity_valid"] = True
|
| 354 |
+
logger.info("✅ [VERIFIER/trig] Identity verified symbolically")
|
| 355 |
+
schema.verification_status = "verified"
|
| 356 |
+
else:
|
| 357 |
+
# Try numeric check at endpoint angle if provided
|
| 358 |
+
if angle_deg is not None:
|
| 359 |
+
angle_rad = sp.Rational(angle_deg) * sp.pi / 180
|
| 360 |
+
val = float(diff.subs(theta, angle_rad))
|
| 361 |
+
verified["identity_at_angle"] = (abs(val) < 1e-9)
|
| 362 |
+
if abs(val) < 1e-9:
|
| 363 |
+
schema.verification_status = "partial"
|
| 364 |
+
else:
|
| 365 |
+
schema.warnings.append(f"Trig identity fails at angle={angle_deg}°")
|
| 366 |
+
schema.verification_status = "contradiction"
|
| 367 |
+
else:
|
| 368 |
+
schema.warnings.append("Trig identity not simplified to zero — partial")
|
| 369 |
+
schema.verification_status = "partial"
|
| 370 |
+
except Exception as e:
|
| 371 |
+
schema.warnings.append(f"Trigonometry verifier error: {e}")
|
| 372 |
+
schema.verification_status = "partial"
|
| 373 |
+
|
| 374 |
+
# ── CALCULUS ──────────────────────────────────────────────────────────
|
| 375 |
+
def _verify_calculus(self, schema: MathProblemSchema) -> None:
|
| 376 |
+
g = schema.given
|
| 377 |
+
verified = schema.verified
|
| 378 |
+
|
| 379 |
+
func_str = g.get("function")
|
| 380 |
+
point_raw = g.get("point")
|
| 381 |
+
|
| 382 |
+
if func_str is None:
|
| 383 |
+
schema.warnings.append("Calculus: 'function' is null — cannot verify derivative")
|
| 384 |
+
schema.verification_status = "insufficient_data"
|
| 385 |
+
return
|
| 386 |
+
|
| 387 |
+
try:
|
| 388 |
+
x = sp.Symbol("x")
|
| 389 |
+
f = sp.sympify(str(func_str))
|
| 390 |
+
deriv = sp.diff(f, x)
|
| 391 |
+
verified["derivative"] = str(deriv)
|
| 392 |
+
logger.info("📐 [VERIFIER/calculus] f'(x) = %s", deriv)
|
| 393 |
+
|
| 394 |
+
if point_raw is not None:
|
| 395 |
+
pt = _to_rational(point_raw)
|
| 396 |
+
val = float(deriv.subs(x, pt))
|
| 397 |
+
verified["derivative_at_point"] = val
|
| 398 |
+
logger.info("📐 [VERIFIER/calculus] f'(%s) = %s", point_raw, val)
|
| 399 |
+
|
| 400 |
+
given_deriv_val = g.get("derivative_at_point")
|
| 401 |
+
if given_deriv_val is not None:
|
| 402 |
+
diff_val = abs(float(given_deriv_val) - val)
|
| 403 |
+
if diff_val > 1e-9:
|
| 404 |
+
msg = (
|
| 405 |
+
f"CONTRADICTION: given f'({point_raw})={given_deriv_val} "
|
| 406 |
+
f"but computed={val:.6f}"
|
| 407 |
+
)
|
| 408 |
+
schema.warnings.append(msg)
|
| 409 |
+
schema.verification_status = "contradiction"
|
| 410 |
+
return
|
| 411 |
+
|
| 412 |
+
schema.verification_status = "verified"
|
| 413 |
+
except Exception as e:
|
| 414 |
+
schema.warnings.append(f"Calculus verifier error: {e}")
|
| 415 |
+
schema.verification_status = "partial"
|
backend/variable_pinner.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/variable_pinner.py
|
| 2 |
+
# BuddyMath V2.0 — Stage 3: Variable Pinning
|
| 3 |
+
#
|
| 4 |
+
# PURPOSE:
|
| 5 |
+
# Converts verified data from MathProblemSchema into a hard constraint block
|
| 6 |
+
# that is injected into the LLM's prompt as GROUND TRUTH.
|
| 7 |
+
#
|
| 8 |
+
# THE MESSAGE TO THE LLM (verbatim from spec):
|
| 9 |
+
# "The values below were verified algebraically. They are Ground Truth.
|
| 10 |
+
# Do NOT recalculate them. If your intuition contradicts them — the table is correct."
|
| 11 |
+
# ─────────────────────────────────────────────────────────────────────────
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import logging
|
| 16 |
+
from typing import Optional
|
| 17 |
+
|
| 18 |
+
from backend.math_schema import MathProblemSchema
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
_PIN_HEADER = """
|
| 23 |
+
╔══════════════════════════════════════════════════════════════════════╗
|
| 24 |
+
║ 🔒 ALGEBRAICALLY VERIFIED — GROUND TRUTH ║
|
| 25 |
+
║ The values below were computed deterministically (SymPy). ║
|
| 26 |
+
║ DO NOT recalculate them. DO NOT trust visual intuition over them. ║
|
| 27 |
+
║ If your reasoning contradicts any value here — the table wins. ║
|
| 28 |
+
╚══════════════════════════════════════════════════════════════════════╝
|
| 29 |
+
""".strip()
|
| 30 |
+
|
| 31 |
+
_NULL_WARNING = """
|
| 32 |
+
⚠️ INSUFFICIENT_DATA WARNING:
|
| 33 |
+
The following fields could NOT be verified due to unclear OCR/input:
|
| 34 |
+
{null_list}
|
| 35 |
+
For these fields, reason carefully from the available constraints.
|
| 36 |
+
DO NOT invent values. State explicitly that these are assumed/estimated.
|
| 37 |
+
""".strip()
|
| 38 |
+
|
| 39 |
+
_CONTRADICTION_WARNING = """
|
| 40 |
+
🚨 CONTRADICTION DETECTED:
|
| 41 |
+
The verifier found inconsistencies between the extracted data and
|
| 42 |
+
algebraic facts. Proceed with the ALGEBRAICALLY COMPUTED values,
|
| 43 |
+
NOT the extracted ones.
|
| 44 |
+
""".strip()
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class VariablePinner:
|
| 48 |
+
"""
|
| 49 |
+
Generates the pinning block that is injected into the LLM prompt
|
| 50 |
+
BEFORE the main pedagogical reasoning begins.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
def build_pin_block(self, schema: MathProblemSchema) -> str:
|
| 54 |
+
"""
|
| 55 |
+
Build the full pinning block string.
|
| 56 |
+
|
| 57 |
+
This string must be injected into the LLM prompt verbatim,
|
| 58 |
+
before the main solver / explainer instruction.
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
str: Formatted pinning block ready for prompt injection.
|
| 62 |
+
"""
|
| 63 |
+
lines: list[str] = [_PIN_HEADER, ""]
|
| 64 |
+
|
| 65 |
+
# ── Status-specific header ────────────────────────────────────
|
| 66 |
+
if schema.verification_status == "contradiction":
|
| 67 |
+
lines.append(_CONTRADICTION_WARNING)
|
| 68 |
+
lines.append("")
|
| 69 |
+
elif schema.verification_status == "insufficient_data":
|
| 70 |
+
null_keys = schema.null_fields()
|
| 71 |
+
null_list = "\n".join(f" • {k}" for k in null_keys)
|
| 72 |
+
lines.append(_NULL_WARNING.format(null_list=null_list))
|
| 73 |
+
lines.append("")
|
| 74 |
+
|
| 75 |
+
# ── Verified facts table ──────────────────────────────────────
|
| 76 |
+
if schema.verified:
|
| 77 |
+
lines.append("📌 PINNED VALUES (Ground Truth):")
|
| 78 |
+
for key, val in schema.verified.items():
|
| 79 |
+
lines.append(f" {key:30s} = {val}")
|
| 80 |
+
lines.append("")
|
| 81 |
+
|
| 82 |
+
# ── What to find ─────────────────────────────────────────────
|
| 83 |
+
if schema.find:
|
| 84 |
+
lines.append("🎯 QUESTION TARGETS (what to find):")
|
| 85 |
+
for target in schema.find:
|
| 86 |
+
lines.append(f" • {target}")
|
| 87 |
+
lines.append("")
|
| 88 |
+
|
| 89 |
+
# ── Constraints ───────────────────────────────────────────────
|
| 90 |
+
if schema.constraints:
|
| 91 |
+
lines.append("📋 PROBLEM CONSTRAINTS:")
|
| 92 |
+
for c in schema.constraints:
|
| 93 |
+
lines.append(f" • {c}")
|
| 94 |
+
lines.append("")
|
| 95 |
+
|
| 96 |
+
# ── Non-fatal warnings ────────────────────────────────────────
|
| 97 |
+
if schema.warnings:
|
| 98 |
+
lines.append("⚠️ WARNINGS (for LLM awareness):")
|
| 99 |
+
for w in schema.warnings:
|
| 100 |
+
lines.append(f" ! {w}")
|
| 101 |
+
lines.append("")
|
| 102 |
+
|
| 103 |
+
pin_block = "\n".join(lines)
|
| 104 |
+
|
| 105 |
+
logger.info(
|
| 106 |
+
"📌 [PINNER] Built pin block | status=%s | pinned_keys=%s | null_fields=%s",
|
| 107 |
+
schema.verification_status,
|
| 108 |
+
list(schema.verified.keys()),
|
| 109 |
+
schema.null_fields(),
|
| 110 |
+
)
|
| 111 |
+
logger.debug("📌 [PINNER] Full block:\n%s", pin_block)
|
| 112 |
+
|
| 113 |
+
return pin_block
|
docs/legacy_prompts_backup.txt
ADDED
|
@@ -0,0 +1,937 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# prompts.py - V275.0 (Enhanced OCR for Nested Fractions)
|
| 2 |
+
# Based on BUDDYMATH_COMPLETE_GUIDE V230.8 §1.3, §4.1, §6.1
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def _get_grade_features(grade: str, category: str = "") -> dict:
|
| 7 |
+
"""התאמת רמת הפירוט לפי יחידות לימוד (§5.1)"""
|
| 8 |
+
is_investigation = (category == "INVESTIGATION")
|
| 9 |
+
|
| 10 |
+
if "5 יח\"ל" in grade:
|
| 11 |
+
style = "הוכחה דקדקנית של כל מעבר, כולל נגזרות שנייה, קמירות ואסימפטוטות" if is_investigation else "הוכחה דקדקנית של כל מעבר ודיוק אלגברי מירבי"
|
| 12 |
+
return {
|
| 13 |
+
"depth": "אקדמי ומעמיק",
|
| 14 |
+
"style": style,
|
| 15 |
+
"tone": "מעצים ומאתגר, כביר של בגרות 5 יח\"ל"
|
| 16 |
+
}
|
| 17 |
+
elif "4 יח\"ל" in grade:
|
| 18 |
+
style = "הסבר ברור של חוקי האלגברה עם דגש על כלל שרשרת" if is_investigation else "הסבר ברור של חוקי האלגברה ופירוט תהליכי הפתרון"
|
| 19 |
+
return {
|
| 20 |
+
"depth": "מפורט ותומך",
|
| 21 |
+
"style": style,
|
| 22 |
+
"tone": "מעודד ומפורט, שלב אחר שלב"
|
| 23 |
+
}
|
| 24 |
+
else:
|
| 25 |
+
return {
|
| 26 |
+
"depth": "פשוט, ברור ומדובר 'בגובה העיניים' (מבחן הילד בן ה-16)",
|
| 27 |
+
"style": "צעד אחר צעד, בקריאה שוטפת. חובה להשתמש במשפטי קישור ומעבר מילוליים (כמו 'נציב את הנתונים בנוסחה:', 'כעת נבדוק את תחום ההגדרה:'). אסור להרצות.",
|
| 28 |
+
"tone": "חם, סבלני, מלא התלהבות ועידוד תמידי. כמו מורה אהובה שעוזרת באופן פרטי."
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
def _detect_relevant_rules(text: str, category: str = "") -> list:
|
| 32 |
+
"""זיהוי כללים רלוונטיים לפי קטגוריה — מבוסס-הקשר (§4.1, V231.1)"""
|
| 33 |
+
rules = []
|
| 34 |
+
t = text.lower()
|
| 35 |
+
|
| 36 |
+
# === GEOMETRY rules (only for GEOMETRY category) ===
|
| 37 |
+
if category != "INVESTIGATION":
|
| 38 |
+
if any(x in t for x in ["מעגל", "מרכז", "רדיוס", "מקום גיאומטרי"]):
|
| 39 |
+
rules.append(r"משוואת מעגל: $(x-a)^2 + (y-b)^2 = r^2$")
|
| 40 |
+
rules.append(r"היקף מעגל: $2\\pi r$")
|
| 41 |
+
if "מרחק" in t or "d =" in t or "מקום גיאומטרי" in t:
|
| 42 |
+
rules.append(r"דיסטנס: $d = \\sqrt{(x_2-x_1)^2 + (y_2-y_1)^2}$")
|
| 43 |
+
if any(x in t for x in ["ישר", "שיפוע"]):
|
| 44 |
+
rules.append(r"משוואת ישר: $y = mx + b$")
|
| 45 |
+
if any(x in t for x in ["משולש", "שטח"]):
|
| 46 |
+
rules.append(r"שטח משולש: $S = \\frac{1}{2} \\cdot a \\cdot h$")
|
| 47 |
+
if any(x in t for x in ["משיק", "אנך"]):
|
| 48 |
+
rules.append(r"שיפועים אנכיים: $m_1 \\cdot m_2 = -1$")
|
| 49 |
+
if "מקום גיאומטרי" in t:
|
| 50 |
+
rules.append(r"פרבולה: מרחק מנקודה שווה למרחק מישר.")
|
| 51 |
+
rules.append(r"אליפסה: סכום מרחקים מ-2 נקודות קבוע ($d_1+d_2=k$).")
|
| 52 |
+
rules.append(r"היפרבולה: הפרש מרחקים מ-2 נקודות קבוע ($|d_1-d_2|=k$).")
|
| 53 |
+
if "מקום גיאומטרי" in t:
|
| 54 |
+
rules.append(r"⚠️ הנחיה קריטית: אל תנחש את הצורה! פיתוח המשוואה חייב להיעשות צעד-אחר-צעד מההגדרה המילולית.")
|
| 55 |
+
rules.append(r"דוגמה לשימוש בהגדרה: אם רשום שמרחק נקודה $P(x,y)$ ממיקום א' שווה למרחקה ממיקום ב', רשום משוואה מהצורה $d_1 = d_2$. הצב את נוסחאות המרחק, העלה בריבוע ופשט.")
|
| 56 |
+
rules.append(r"בביטויים עם שורשים: בודד שורש אחד -> עלה בריבוע -> בודד את השורש השני -> עלה בריבוע שוב.")
|
| 57 |
+
|
| 58 |
+
# === PROOF-specific rules (V282.0) ===
|
| 59 |
+
if any(x in t for x in ["הוכח", "הוכיח", "הוכיחו", "הראה כי", "הראי כי", "הוכח כי", "הוכיחי"]):
|
| 60 |
+
rules.append(r"⚠️ זוהי שאלת הוכחה! חובה להראות את כל שרשרת ההיגיון, לא רק את התוצאה.")
|
| 61 |
+
rules.append(r"מבנה הוכחה: נתון -> מסקנה ביניים (+ נימוק/משפט) -> מסקנה הבאה -> ... -> מ.ש.ל.")
|
| 62 |
+
rules.append(r"לכל מעבר לוגי חובה לציין את הנימוק: שם המשפט, התכונה, או הכלל.")
|
| 63 |
+
rules.append(r"משולש שווה שוקיים -> זוויות בסיס שוות, חוצה זווית = תיכון = גובה לבסיס.")
|
| 64 |
+
rules.append(r"משולשים חופפים: צ.צ.צ / צ.ז.צ / ז.צ.ז - ציין איזה קריטריון נבחר ולמה.")
|
| 65 |
+
rules.append(r"אם צריך להוכיח שוויון קטעים - חפש משולשים חופפים, או תכ��נות של צורות מיוחדות.")
|
| 66 |
+
|
| 67 |
+
rules.append(r"בגיאומטריה אנליטית: לפני חישוב, בצע 'בדיקת שפיות' (Sanity Check).")
|
| 68 |
+
rules.append(r"אם נתון ש-AC קוטר, המרכז M *חייב* להיות האמצע שלו. אם החישוב מראה אחרת - הנתונים שהוצאת שגויים! נסה לקרוא שוב.")
|
| 69 |
+
rules.append(r"עדיפות לנתונים: טקסט כתוב > משוואות > שרטוט ויזואלי.")
|
| 70 |
+
rules.append(r"הימנע משימוש ב-\\\\ בתוך בלוקים של מתמטיקה, השתמש בצעדים נפרדים.")
|
| 71 |
+
|
| 72 |
+
# V8.6.3: Contextual Logic Guardrails
|
| 73 |
+
if category == "GEOMETRY":
|
| 74 |
+
rules.append(r"ZERO ASSUMPTIONS RULE: NEVER assume, guess, or invent ANY geometric property (e.g., 'this segment is a diameter', 'this angle is 90 degrees', 'this triangle is isosceles') unless it is EXPLICITLY WRITTEN in the provided text. You are strictly forbidden from fabricating data or relationships just because the coordinates look convenient. Work ONLY with the exact facts stated in the OCR text and Data Anchor.")
|
| 75 |
+
rules.append(r"CRITICAL GEOMETRY RULE: You MUST strictly adhere to the geometric layout described in the problem text. If your calculation leads to a paradox (e.g., length 0, points colliding, or negative area), YOUR assumption is wrong. Recalculate based strictly on the given explicit constraints. DO NOT change the given equations.")
|
| 76 |
+
|
| 77 |
+
if category in ["CALCULUS", "FUNCTION_ANALYSIS", "INVESTIGATION"]:
|
| 78 |
+
rules.append(r"GRAPH MATCHING PROTOCOL: When asked to select a correct graph from multiple options (I, II, III, IV), you MUST first perform a visual scan: explicitly describe what you see in EACH given graph (e.g., 'Graph I shows asymptotes at... Graph II shows a hole at...'). Only AFTER describing all options, match your mathematical deductions to the correct visual description and select the final answer.")
|
| 79 |
+
|
| 80 |
+
if category == "CALCULUS":
|
| 81 |
+
if any(x in t for x in ["נגזרת", "גזור", "f'", "חקור"]):
|
| 82 |
+
rules.append(r"כלל החזקה: $(x^n)' = nx^{n-1}$")
|
| 83 |
+
if any(x in t for x in ["/", "frac", "מנה", "חילוק"]):
|
| 84 |
+
rules.append(r"נגזרת מנה: $(\\frac{u}{v})' = \\frac{u'v - uv'}{v^2}$")
|
| 85 |
+
if any(x in t for x in ["מכפלה", "כפל"]):
|
| 86 |
+
rules.append(r"נגזרת מכפלה: $(u \\cdot v)' = u'v + uv'$")
|
| 87 |
+
if any(x in t for x in ["שרשרת", "הרכבה", "sin", "cos"]):
|
| 88 |
+
rules.append(r"כלל שרשרת: $[f(g(x))]' = f'(g(x)) \\cdot g'(x)$")
|
| 89 |
+
if any(x in t for x in ["אינטגרל", "שטח מתחת"]):
|
| 90 |
+
rules.append(r"אינטגרל חזקה: $\\int x^n dx = \\frac{x^{n+1}}{n+1} + C$")
|
| 91 |
+
if any(x in t for x in ["דיפרנציאלית", "y'", "dy/dx"]):
|
| 92 |
+
rules.append(r"משוואה דיפרנציאלית: הפרד משתנים, אינטגרל משני הצדדים.")
|
| 93 |
+
|
| 94 |
+
# === SEQUENCES rules ===
|
| 95 |
+
if any(x in t for x in ["סדרה", "חשבונית", "הנדסית", "סכום חלקי", "הפרש"]):
|
| 96 |
+
rules.append(r"סדרה חשבונית: $a_n = a_1 + (n-1)d$, סכום: $S_n = \\frac{n}{2}(a_1 + a_n)$")
|
| 97 |
+
rules.append(r"סדרה הנדסית: $a_n = a_1 \\cdot q^{n-1}$, סכום: $S_n = a_1 \\cdot \\frac{q^n - 1}{q - 1}$")
|
| 98 |
+
if "אינסופי" in t or "גבול" in t:
|
| 99 |
+
rules.append(r"סכום סדרה הנדסית אינסופית ($|q|<1$): $S = \\frac{a_1}{1-q}$")
|
| 100 |
+
|
| 101 |
+
# === COMPLEX NUMBERS rules ===
|
| 102 |
+
if any(x in t for x in ["מרוכב", "מדומה", "i²", "i^2"]):
|
| 103 |
+
rules.append(r"$i^2 = -1$, מספר מרוכב: $z = a + bi$")
|
| 104 |
+
rules.append(r"מודולוס: $|z| = \\sqrt{a^2 + b^2}$, צמוד: $\\bar{z} = a - bi$")
|
| 105 |
+
if any(x in t for x in ["קוטבי", "דה-מואבר", "טריגונומטרי"]):
|
| 106 |
+
rules.append(r"צורה טריגונומטרית: $z = r(\\cos\\theta + i\\sin\\theta)$")
|
| 107 |
+
rules.append(r"דה-מואבר: $z^n = r^n(\\cos(n\\theta) + i\\sin(n\\theta))$")
|
| 108 |
+
|
| 109 |
+
# === VECTORS rules ===
|
| 110 |
+
if any(x in t for x in ["וקטור", "וקטורים"]):
|
| 111 |
+
rules.append(r"$\\vec{u} = (a,b)$, $|\\vec{u}| = \\sqrt{a^2+b^2}$")
|
| 112 |
+
rules.append(r"מכפלה סקלרית: $\\vec{u}\\cdot\\vec{v} = a_1 a_2 + b_1 b_2 = |u||v|\\cos\\alpha$")
|
| 113 |
+
rules.append(r"וקטורים מאונכים: $\\vec{u}\\cdot\\vec{v} = 0$")
|
| 114 |
+
# V286.0: 3D Vectors (5 יח"ל)
|
| 115 |
+
if any(x in t for x in ["מרחב", "מישור", "תלת", "z", "פרמטרי", "ניצב למישור"]):
|
| 116 |
+
rules.append(r"וקטור במרחב: $\vec{u} = (a,b,c)$, $|\vec{u}| = \sqrt{a^2+b^2+c^2}$")
|
| 117 |
+
rules.append(r"מכפלה סקלרית 3D: $\vec{u}\cdot\vec{v} = a_1 a_2 + b_1 b_2 + c_1 c_2 = |u||v|\cos\alpha$")
|
| 118 |
+
rules.append(r"מכפלה וקטורית (Cross Product): $\vec{u}\times\vec{v} = (b_1 c_2 - c_1 b_2,\; c_1 a_2 - a_1 c_2,\; a_1 b_2 - b_1 a_2)$. הוק��ור שמתקבל ניצב לשני הוקטורים המקוריים.")
|
| 119 |
+
rules.append(r"מציאת נורמלי למישור: השתמש במכפלה וקטורית של שני וקטורי כיוון הפורשים את המישור.")
|
| 120 |
+
rules.append(r"משוואת מישור: $Ax + By + Cz + D = 0$. הנורמלי הוא $\vec{n} = (A,B,C)$.")
|
| 121 |
+
rules.append(r"ישר במרחב (הצגה פרמטרית): $\vec{r} = \vec{p} + t\vec{v}$")
|
| 122 |
+
rules.append(r"מרחק נקודה $(x_0,y_0,z_0)$ ממישור $Ax+By+Cz+D=0$: $d = \frac{|Ax_0+By_0+Cz_0+D|}{\sqrt{A^2+B^2+C^2}}$")
|
| 123 |
+
rules.append(r"זווית $\alpha$ בין שני מישורים: $\cos\alpha = \frac{|\vec{n_1}\cdot\vec{n_2}|}{|\vec{n_1}||\vec{n_2}|}$")
|
| 124 |
+
rules.append(r"זווית $\beta$ בין ישר למישור: $\sin\beta = \frac{|\vec{v}\cdot\vec{n}|}{|\vec{v}||\vec{n}|}$ (כאשר $\vec{n}$ הוא נורמלי למישור).")
|
| 125 |
+
rules.append(r"מכפלה משולשת (נפח מקבילון): $V = |\vec{a}\cdot(\vec{b}\times\vec{c})|$")
|
| 126 |
+
|
| 127 |
+
# === STATISTICS rules ===
|
| 128 |
+
if any(x in t for x in ["ממוצע", "חציון", "שכיח", "סטיית תקן", "שונות"]):
|
| 129 |
+
rules.append(r"ממוצע: $\\bar{x} = \\frac{\\sum x_i}{n}$")
|
| 130 |
+
rules.append(r"שונות: $\\sigma^2 = \\frac{\\sum(x_i - \\bar{x})^2}{n}$, סטיית תקן: $\\sigma = \\sqrt{\\sigma^2}$")
|
| 131 |
+
if any(x in t for x in ["התפלגות", "נורמלית", "בינומית"]):
|
| 132 |
+
rules.append(r"התפלגות בינומית: $P(X=k) = \\binom{n}{k}p^k(1-p)^{n-k}$")
|
| 133 |
+
rules.append(r"התפלגות נורמלית: כלל 68-95-99.7 (אחוזים בטווח 1-2-3 סטיות תקן)")
|
| 134 |
+
|
| 135 |
+
# === INDUCTION rules ===
|
| 136 |
+
if any(x in t for x in ["אינדוקציה", "הוכח כי לכל", "n טבעי"]):
|
| 137 |
+
rules.append(r"שלב 1 (בסיס): הוכח עבור $n=1$. שלב 2 (הנחה): הנח עבור $n=k$. שלב 3 (צעד): הוכח עבור $n=k+1$.")
|
| 138 |
+
rules.append(r"⚠️ חובה לרשום בפירוש: 'מה צריך להוכיח', 'הנחת האינדוקציה', 'מ.ש.ל'.")
|
| 139 |
+
|
| 140 |
+
# === TRIGONOMETRY LAW rules ===
|
| 141 |
+
if any(x in t for x in ["סינוסים", "קוסינוסים", "משולש", "זווית"]):
|
| 142 |
+
if "סינוסים" in t:
|
| 143 |
+
rules.append(r"משפט הסינוסים: $\\frac{a}{\\sin A} = \\frac{b}{\\sin B} = \\frac{c}{\\sin C}$")
|
| 144 |
+
if "קוסינוסים" in t:
|
| 145 |
+
rules.append(r"משפט הקוסינוסים: $c^2 = a^2 + b^2 - 2ab\\cos C$")
|
| 146 |
+
rules.append(r"שטח משולש לפי שתי צלעות וזווית: $S = \\frac{1}{2}ab\\sin C$")
|
| 147 |
+
if any(x in t for x in ["זהות", "טריגונומטרית"]):
|
| 148 |
+
rules.append(r"$\\sin^2(x) + \\cos^2(x) = 1$")
|
| 149 |
+
rules.append(r"$\\sin(2x) = 2\\sin(x)\\cos(x)$, $\\cos(2x) = \\cos^2(x) - \\sin^2(x)$")
|
| 150 |
+
# V286.0: Advanced Trigonometry (5 יח"ל)
|
| 151 |
+
if any(x in t for x in ["sin", "cos", "tan", "trig", "טריגונומטר", "sin(", "cos("]):
|
| 152 |
+
if any(x in t for x in ["סכום", "הפרש", "α+β", "α-β", "alpha"]):
|
| 153 |
+
rules.append(r"$\\sin(\\alpha \\pm \\beta) = \\sin\\alpha\\cos\\beta \\pm \\cos\\alpha\\sin\\beta$")
|
| 154 |
+
rules.append(r"$\\cos(\\alpha \\pm \\beta) = \\cos\\alpha\\cos\\beta \\mp \\sin\\alpha\\sin\\beta$")
|
| 155 |
+
rules.append(r"$\\tan(\\alpha + \\beta) = \\frac{\\tan\\alpha + \\tan\\beta}{1 - \\tan\\alpha\\tan\\beta}$")
|
| 156 |
+
if any(x in t for x in ["חצי זווית", "sin²", "cos²"]):
|
| 157 |
+
rules.append(r"$\\sin^2\\frac{x}{2} = \\frac{1-\\cos x}{2}$, $\\cos^2\\frac{x}{2} = \\frac{1+\\cos x}{2}$")
|
| 158 |
+
if any(x in t for x in ["פתור משוואה טריגונומטרית", "sinx=", "cosx=", "sin x =", "cos x ="]):
|
| 159 |
+
rules.append(r"$\\sin x = a \\Rightarrow x = (-1)^n \\arcsin a + \\pi n$, $n \\in \\mathbb{Z}$")
|
| 160 |
+
rules.append(r"$\\cos x = a \\Rightarrow x = \\pm \\arccos a + 2\\pi n$, $n \\in \\mathbb{Z}$")
|
| 161 |
+
rules.append(r"$\\tan x = a \\Rightarrow x = \\arctan a + \\pi n$, $n \\in \\mathbb{Z}$")
|
| 162 |
+
|
| 163 |
+
# === VOLUME & SURFACE AREA rules ===
|
| 164 |
+
if any(x in t for x in ["נפח", "שטח פנים", "גליל", "חרוט", "כדור"]):
|
| 165 |
+
rules.append(r"נפח גליל: $V = \\pi r^2 h$, נפח חרוט: $V = \\frac{1}{3}\\pi r^2 h$")
|
| 166 |
+
rules.append(r"נפח כדור: $V = \\frac{4}{3}\\pi r^3$, שטח פנים כדור: $S = 4\\pi r^2$")
|
| 167 |
+
|
| 168 |
+
# V286.0: Solid Geometry (גיאומטריה מרחבית — 5 יח"ל)
|
| 169 |
+
if any(x in t for x in ["פירמידה", "מנסרה", "תיבה", "חתך", "אפותם", "מקצוע צדדי", "פאות"]):
|
| 170 |
+
rules.append(r"נפח פירמידה: $V = \frac{1}{3} S_{base} \cdot h$")
|
| 171 |
+
rules.append(r"נפח מנסרה: $V = S_{base} \cdot h$")
|
| 172 |
+
rules.append(r"שטח פנים של פירמידה: $S = S_{base} + \sum S_{faces}$")
|
| 173 |
+
rules.append(r"זווית $\alpha$ בין מקצוע צדדי לבסיס: זווית במשולש ישר-זווית הנוצר ע\"י המקצוע, הטלו על הבסיס והגובה.")
|
| 174 |
+
rules.append(r"זווית $\beta$ בין פאה צדדית לבסיס: זווית במשולש ישר-זווית הנוצר ע\"י האפותם של הפאה, הטלו על הבסיס והגובה.")
|
| 175 |
+
rules.append(r"חישובים מרחביים: השתמש תמיד במשפט פיתגורס וטריגונומטריה בתוך משולשים ישרי-זווית המקשרים פנים וחוץ.")
|
| 176 |
+
|
| 177 |
+
# === INEQUALITY rules ===
|
| 178 |
+
if any(x in t for x in ["אי-שוויון", "אי שוויון"]):
|
| 179 |
+
rules.append(r"⚠️ בכפל/חילוק באי-שוויון במספר שלילי — הפוך את כיוון הסימן!")
|
| 180 |
+
if "ריבועי" in t:
|
| 181 |
+
rules.append(r"אי-שוויון ריבועי: פתור כמשוואה, בדוק סימן בקטעים.")
|
| 182 |
+
|
| 183 |
+
# V286.0: Logarithms & Exponentials (לוגריתמים — 5 יח"ל)
|
| 184 |
+
if any(x in t for x in ["לוגריתם", "log", "ln", "מעריכי", "e^"]):
|
| 185 |
+
rules.append(r"$\\log_a(xy) = \\log_a x + \\log_a y$")
|
| 186 |
+
rules.append(r"$\\log_a\\left(\\frac{x}{y}\\right) = \\log_a x - \\log_a y$")
|
| 187 |
+
rules.append(r"$\\log_a(x^n) = n\\log_a x$")
|
| 188 |
+
rules.append(r"החלפת בסיס: $\\log_a b = \\frac{\\ln b}{\\ln a}$")
|
| 189 |
+
rules.append(r"$a^x = e^{x\\ln a}$, $\\log_a a = 1$, $\\log_a 1 = 0$")
|
| 190 |
+
rules.append(r"$\\ln e = 1$, $e^{\\ln x} = x$, $\\ln(e^x) = x$")
|
| 191 |
+
|
| 192 |
+
# V286.0: Optimization Problems (בעיות קיצון — 5 יח"ל)
|
| 193 |
+
if any(x in t for x in ["קיצון", "מקסימום", "מינימום", "שטח גדול ביותר", "נפח מקס", "ערך מרבי", "ערך מזערי", "אופטימיזציה"]):
|
| 194 |
+
rules.append(r"אסטרטגיה לבעיות אופטימיזציה (5 יח\"ל):")
|
| 195 |
+
rules.append(r"1) הגדר משתנה (x) ובטא את כל הגדלים האחרים בעזרתו.")
|
| 196 |
+
rules.append(r"2) בנה פונקציית מטרה $f(x)$ עבור הגודל שצריך למקסם/למזער.")
|
| 197 |
+
rules.append(r"3) השתמש באילוצים כדי להגיע לפונקציה של משתנה אחד בלבד.")
|
| 198 |
+
rules.append(r"4) גזור את פונקציית המטרה, השווה ל-0 ומצא את הנקודות החשודות.")
|
| 199 |
+
rules.append(r"5) הוכח את סוג הקיצון (Max/Min) בעזרת טבלה או נגזרת שנייה.")
|
| 200 |
+
rules.append(r"6) בדוק תמיד נקודות קצה של התחום אם הבעיה מוגדרת בקטע סגור.")
|
| 201 |
+
|
| 202 |
+
# V286.0: Probability & Combinatorics (הסתברות וקומבינטוריקה — 5 יח"ל)
|
| 203 |
+
if any(x in t for x in ["הסתברות", "קומבינטור", "עצי", "עץ", "תמורה", "צירוף", "בייס", "מותנה", "בלתי תלוי"]):
|
| 204 |
+
rules.append(r"$P(A \\cup B) = P(A) + P(B) - P(A \\cap B)$")
|
| 205 |
+
rules.append(r"הסתברות מותנית: $P(A|B) = \\frac{P(A \\cap B)}{P(B)}$")
|
| 206 |
+
rules.append(r"נוסחת בייס: $P(B_i|A) = \\frac{P(A|B_i)P(B_i)}{\\sum_j P(A|B_j)P(B_j)}$")
|
| 207 |
+
rules.append(r"חוק ההסתברות השלמה: $P(A) = \\sum_i P(A|B_i)P(B_i)$")
|
| 208 |
+
rules.append(r"תמורות: $P(n,k) = \\frac{n!}{(n-k)!}$, צירופים: $\\binom{n}{k} = \\frac{n!}{k!(n-k)!}$")
|
| 209 |
+
rules.append(r"אירועים בלתי תלויים: $P(A \\cap B) = P(A) \\cdot P(B)$")
|
| 210 |
+
|
| 211 |
+
# V286.0: Advanced Calculus (חדו"א מתקדם — 5 יח"ל)
|
| 212 |
+
if any(x in t for x in ["אינטגרל", "שטח מתחת", "נפח סיבוב", "אינטגרציה"]):
|
| 213 |
+
if any(x in t for x in ["חלקי", "חלקים", "by parts"]):
|
| 214 |
+
rules.append(r"אינטגרציה בחלקים: $\\int u\\,dv = uv - \\int v\\,du$")
|
| 215 |
+
if any(x in t for x in ["הצבה", "substitution"]):
|
| 216 |
+
rules.append(r"אינטגרציה בהצבה: $\\int f(g(x))g'(x)dx = \\int f(u)du$ כאשר $u=g(x)$")
|
| 217 |
+
rules.append(r"שטח בין שני גרפים: $S = \\int_a^b |f(x) - g(x)|\\,dx$")
|
| 218 |
+
if any(x in t for x in ["סיבוב", "גוף סיבוב"]):
|
| 219 |
+
rules.append(r"נפח גוף סיבוב סביב ציר $x$: $V = \\pi\\int_a^b [f(x)]^2\\,dx$")
|
| 220 |
+
if any(x in t for x in ["לא אמיתי", "מוכלל", "improper"]):
|
| 221 |
+
rules.append(r"אינטגרל מוכלל: $\\int_a^{\\infty} f(x)dx = \\lim_{b\\to\\infty} \\int_a^b f(x)dx$")
|
| 222 |
+
|
| 223 |
+
# V286.0: Limits (גבולות — 5 יח"ל)
|
| 224 |
+
if any(x in t for x in ["גבול", "lim", "שואף", "אינסוף", "לופיטל"]):
|
| 225 |
+
rules.append(r"גבולות נחשבים: $\\lim_{x\\to 0}\\frac{\\sin x}{x} = 1$")
|
| 226 |
+
rules.append(r"$\\lim_{x\\to\\infty}\\left(1+\\frac{1}{x}\\right)^x = e$")
|
| 227 |
+
rules.append(r"כלל לופיטל: אם $\\frac{0}{0}$ או $\\frac{\\infty}{\\infty}$, אז $\\lim\\frac{f(x)}{g(x)} = \\lim\\frac{f'(x)}{g'(x)}$")
|
| 228 |
+
rules.append(r"אסטרטגיה ל-$\\frac{0}{0}$: פרק לגורמים, רציונליזציה, או לופיטל.")
|
| 229 |
+
rules.append(r"אסטרטגיה ל-$\\frac{\\infty}{\\infty}$ עם פולינומים: חלק במעלה הגבוהה ביותר.")
|
| 230 |
+
|
| 231 |
+
return rules
|
| 232 |
+
|
| 233 |
+
def get_data_extraction_prompt(problem_text: str) -> str:
|
| 234 |
+
"""V231.15: Template-based extraction to avoid f-string escape hell."""
|
| 235 |
+
template = r"""
|
| 236 |
+
### [SACRED TRUTH: IMAGE OVER OCR]
|
| 237 |
+
You are provided with an IMAGE and its rough OCR transcription.
|
| 238 |
+
⚠️ CRITICAL WARNING: The OCR text is a WEAK HINT and is often WRONG, truncated, or mangled.
|
| 239 |
+
💎 SACRED TRUTH: The IMAGE is the absolute source of truth.
|
| 240 |
+
|
| 241 |
+
YOUR TASK:
|
| 242 |
+
1. PERCEPTUAL PRIORITY: Visually inspect the IMAGE meticulously. Every pixel of the formula counts.
|
| 243 |
+
2. TOP-DOWN HIERARCHY: Start from the very top of the image. The main function (e.g., f(x)=...) is usually the first mathematical line. DO NOT MISS IT.
|
| 244 |
+
3. OCR SKEPTICISM: The OCR text is often a "hallucination hint" for formulas. If the OCR text says h(x) but the IMAGE shows f(x) at the top, the IMAGE'S f(x) is the SACRED TRUTH.
|
| 245 |
+
4. LTR MATHEMATICAL FLOW: Equations are NOT Hebrew. They are read horizontally LEFT-TO-RIGHT. Ensure multipliers, exponents, and fractions are extracted in their visual LTR order.
|
| 246 |
+
5. NO INVENTIONS: Do not "fix" the math. If it looks incomplete, extract what is there.
|
| 247 |
+
|
| 248 |
+
Extract:
|
| 249 |
+
1. **function_equations**: ALL equations with '=' sign
|
| 250 |
+
- ⚠️ **ABSOLUTE PRIORITY**: Extract the primary function (f(x), y=...) first.
|
| 251 |
+
- Functions: f(x) = ..., g(x) = ...
|
| 252 |
+
- Circles: x² + y² = r², (x-a)² + (y-b)² = r²
|
| 253 |
+
- Lines: y = mx + b, ax + by = c
|
| 254 |
+
- ANY equation with '=' sign!
|
| 255 |
+
|
| 256 |
+
**Mathematical Scanning Directionality (V8.9.3):**
|
| 257 |
+
- **LTR PRIORITY:** Mathematical equations are STRICTLY Left-To-Right.
|
| 258 |
+
- When extracting formulas embedded in Hebrew text, IGNORE the RTL flow.
|
| 259 |
+
- Read the characters in their literal horizontal visual order from LEFT to RIGHT.
|
| 260 |
+
- Ensure multipliers, exponents, and signs are placed exactly where they appear visually.
|
| 261 |
+
|
| 262 |
+
**Visual Context & Continuity (V8.9.5):**
|
| 263 |
+
- **SPLIT EQUATIONS:** If an equation starts on one line and ends on another, merge them into a single coherent formula.
|
| 264 |
+
- **SEGMENTED CONSTRAINTS:** Look for constraints (like x > 0) near equations; they are often visually separated by space or Hebrew words but belong to the formula.
|
| 265 |
+
- **MULTI-PART ANCHORING:** Maintain consistency between sub-questions (א, ב, ג). If a variable 'm' is defined in the preamble, it applies to all sub-questions.
|
| 266 |
+
|
| 267 |
+
2. **points**: Named points like A, B, M(3,5), P(x,y)
|
| 268 |
+
|
| 269 |
+
3. **specific_values**: Numbers like r=5, a=3, m=2
|
| 270 |
+
|
| 271 |
+
4. **constraints**: Conditions like x>0, x≠2, domain restrictions
|
| 272 |
+
|
| 273 |
+
5. **sub_questions**: Parts א, ב, ג or a, b, c
|
| 274 |
+
|
| 275 |
+
6. **geometric_anchors** (CRITICAL FOR VALIDATION):
|
| 276 |
+
- center: [x,y] coordinates if a circle center is given
|
| 277 |
+
- radius: numeric value if radius is given
|
| 278 |
+
- point_a, point_b: Strings like "(x,y)" if specific points are given for distance/lines
|
| 279 |
+
|
| 280 |
+
JSON format (STRUCTURE ONLY - DO NOT USE THESE EXACT VALUES):
|
| 281 |
+
{{
|
| 282 |
+
"function_equations": ["<equation_1>", "<equation_2>"],
|
| 283 |
+
"points": ["<point_name>", "<point_name>(<x>,<y>)"],
|
| 284 |
+
"specific_values": ["<variable>=<value>"],
|
| 285 |
+
"constraints": ["<constraint_1>"],
|
| 286 |
+
"sub_questions": ["<sub_question_1>"],
|
| 287 |
+
"center": [null, null],
|
| 288 |
+
"radius": null,
|
| 289 |
+
"point_a": "(null, null)",
|
| 290 |
+
"point_b": "(null, null)"
|
| 291 |
+
}}
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
**CRITICAL JSON STRUCTURE RULE (V8.9.4):**
|
| 295 |
+
- You MUST output a SINGLE, flat JSON object.
|
| 296 |
+
- EVERY mathematical expression, function, or parameter MUST have a UNIQUE, highly descriptive key.
|
| 297 |
+
- DO NOT use a generic key like "equation" multiple times, as this will overwrite the data.
|
| 298 |
+
- DO NOT output an array/list of objects.
|
| 299 |
+
- **Correct Example:** { "main_function_f": "f(x)=...", "function_h": "h(x)=...", "given_extremum_x": "x=1/e" }
|
| 300 |
+
- **Wrong Example:** { "equation": "f(x)=...", "equation": "h(x)=..." }
|
| 301 |
+
|
| 302 |
+
CRITICAL INSTRUCTIONS:
|
| 303 |
+
1. Include ALL equations with '=' sign in function_equations!
|
| 304 |
+
2. **DATA INTEGRITY (V4.3.0):** Use the data below verbatim.
|
| 305 |
+
3. **VARIABLE ENFORCEMENT (V4.0.1):** ALL equations must use standard single-letter mathematical variables (x, y, z, a, b, c, m, n, k).
|
| 306 |
+
- DO NOT extract descriptive English names like "number_of_notebooks".
|
| 307 |
+
- Map descriptive names from text to single letters (e.g., if text says "cost is x", use x. If it says "cost of notebook", map to 'c' or 'x').
|
| 308 |
+
- Equations with descriptive multi-letter variables will crash the calculator.
|
| 309 |
+
4. DO NOT HALLUCINATE OR GUESS! 🚫 If an equation or point is NOT explicitly written in the problem, DO NOT invent it.
|
| 310 |
+
5. **Mathematical Logic over OCR (V8.6.6):** In case of OCR contradictions (e.g. $e^x$ vs $e^{{-x}}$ in the same problem), use mathematical logic to choose the correct formula based on the problem's context (e.g., if a question asks for a limit at $\infty$ that only exists for $e^{{-x}}$, prefer that).
|
| 311 |
+
6. Example: Do not assume `f(x) = ax - x^2` just because it looks like a standard problem. Only extract what is literally there.
|
| 312 |
+
7. **SUB-QUESTION MAPPING (CRITICAL - V231.14):** You MUST map ALL sub-questions present in the OCR text (e.g., א, ב, ג, ד). Do NOT group them into one question and do NOT stop at the first one. Each sub-question letter requires its own entry in the `sub_questions` array.
|
| 313 |
+
8. **DATA SCOPING (V8.6.9 - CRITICAL):** Only extract data that is truly GLOBAL (applies to all sections). If a value or constraint is explicitly tied to a specific section (e.g., "בסעיף ב' נתון כי a=1"), do NOT put it in the `specific_values` of the main anchor. The problem understanding phase will handle the local data.
|
| 314 |
+
9. **CHRONOLOGICAL ISOLATION (V310.0):** If a variable is assigned a value in one sub-question, it MUST NOT be used in previous sub-questions.
|
| 315 |
+
r"""
|
| 316 |
+
|
| 317 |
+
def get_specialist_prompt(category, problem_text, solver_hint, grade, student_name, student_gender="M", data_anchor=None):
|
| 318 |
+
"""בניית הפרומפט המלכותי — המורה למתמטיקה V231.6 (Data Anchor)"""
|
| 319 |
+
features = _get_grade_features(grade, category)
|
| 320 |
+
relevant_rules = _detect_relevant_rules(problem_text, category)
|
| 321 |
+
rules_str = "\n".join([f" - {r}" for r in relevant_rules]) if relevant_rules else f" (לא זוהו כללים ספציפיים — בחר רק כללים רלוונטיים לקטגוריה {category})"
|
| 322 |
+
|
| 323 |
+
# Anchor Block — DATA INTEGRITY RULE
|
| 324 |
+
anchor_block = ""
|
| 325 |
+
geo_verified_block = "" # V1.0: Geometric Sanity Engine output
|
| 326 |
+
if data_anchor:
|
| 327 |
+
# Extract and remove internal sanity keys before JSON serialization
|
| 328 |
+
geo_prompt_block = data_anchor.pop("_geo_prompt_block", "")
|
| 329 |
+
data_anchor.pop("_verified_facts", None)
|
| 330 |
+
data_anchor.pop("_geometry_warnings", None)
|
| 331 |
+
|
| 332 |
+
anchor_block = f"""
|
| 333 |
+
══════════════════════════════════════════════════════
|
| 334 |
+
📜 DATA INTEGRITY RULE (ABSOLUTE TRUTH):
|
| 335 |
+
══════════════════════════════════════════════════════
|
| 336 |
+
The data below is the ABSOLUTE TRUTH extracted from the student's image.
|
| 337 |
+
If it contains function_equations or equations — those ARE the problem's data.
|
| 338 |
+
YOU MUST use them EXACTLY AS GIVEN. Never claim data is "missing" if it is in the data.
|
| 339 |
+
Never assume, invent, or substitute a different function under ANY circumstances.
|
| 340 |
+
Violating this rule is a critical pedagogical failure.
|
| 341 |
+
══════════════════════════════════════════════════════
|
| 342 |
+
נתוני שאלת המקור (השתמש רק בהם):
|
| 343 |
+
{json.dumps(data_anchor, indent=2, ensure_ascii=False)}
|
| 344 |
+
|
| 345 |
+
CONSTRAINT: If the data says A(0,5), use A(0,5). If it contains f(x), solve that f(x).
|
| 346 |
+
"""
|
| 347 |
+
# V1.0: Inject verified geometric facts (if the sanity engine found anything)
|
| 348 |
+
if geo_prompt_block:
|
| 349 |
+
geo_verified_block = geo_prompt_block
|
| 350 |
+
|
| 351 |
+
# V231.5: Gender-aware phrases
|
| 352 |
+
if student_gender == "F":
|
| 353 |
+
g = {
|
| 354 |
+
"royal": "נסיכה",
|
| 355 |
+
"come": "בואי",
|
| 356 |
+
"ready": "מוכנה",
|
| 357 |
+
"great": "מעולה",
|
| 358 |
+
"solved": "פתרת",
|
| 359 |
+
"proud": "גאה בך",
|
| 360 |
+
"try_again": "בואי ננסה שוב יחד",
|
| 361 |
+
"example_open": f"כל הכבוד {student_name} נסיכה! 👑",
|
| 362 |
+
"example_close": f"כל הכבוד {student_name}! 👑 {{'פתרת' if student_gender == 'F' else 'פתרת'}} מעולה. אני גאה בך!"
|
| 363 |
+
}
|
| 364 |
+
else:
|
| 365 |
+
g = {
|
| 366 |
+
"royal": "נסיך",
|
| 367 |
+
"come": "בוא",
|
| 368 |
+
"ready": "מוכן",
|
| 369 |
+
"great": "מעולה",
|
| 370 |
+
"solved": "פתרת",
|
| 371 |
+
"proud": "גאה בך",
|
| 372 |
+
"try_again": "בוא ננסה שוב יחד",
|
| 373 |
+
"example_open": f"כל הכבוד {student_name} נסיך! 👑",
|
| 374 |
+
"example_close": f"כל הכבוד {student_name}! 👑 פתרת מעולה. אני גאה בך!"
|
| 375 |
+
}
|
| 376 |
+
# V282.0: Proof-specific instructions (outside f-string to avoid backslash issues in Python 3.11)
|
| 377 |
+
proof_keywords = ["הוכח", "הוכיח", "הוכיחו", "הראה כי", "הראי כי"]
|
| 378 |
+
is_proof = any(x in problem_text for x in proof_keywords)
|
| 379 |
+
proof_block = ""
|
| 380 |
+
if is_proof:
|
| 381 |
+
proof_block = """
|
| 382 |
+
═══════════════════════════════════════════════════
|
| 383 |
+
📐 הוראות מיוחדות להוכחות (V282.0):
|
| 384 |
+
═══════════════════════════════════════════════════
|
| 385 |
+
|
| 386 |
+
🔴 זוהי שאלת הוכחה! החוקים הבאים הם חובה:
|
| 387 |
+
1. **אסור לקפוץ לתשובה.** ההוכחה היא הדרך, לא התוצאה. התלמיד צריך לראות כל צעד.
|
| 388 |
+
2. **מבנה חובה לכל צעד:** "נתון ש-[X]. לפי [שם המשפט/תכונה], מתקיים [Y]."
|
| 389 |
+
3. **נימוק לכל מעבר:** לכל שוויון/אי-שוויון, ציין למה הוא נכון.
|
| 390 |
+
4. **שרשרת לוגית:** כל צעד חייב להסתמך על צעד קודם או על נתון.
|
| 391 |
+
5. **ציין שיטת הוכחה:** חפוש משולשים חופפים? תכונות של שווה שוקיים? זוויות מתחלפות?
|
| 392 |
+
6. **סיום:** בסוף חובה לכתוב "ולכן הוכחנו ש-[מה שנדרש]. מ.ש.ל."
|
| 393 |
+
7. **דוגמה למבנה טוב:**
|
| 394 |
+
- "נתון שהמשולש ABC שווה שוקיים (AB = AC). לפי תכונה: זוויות בסיס שוות."
|
| 395 |
+
- "חישבנו שזווית CDF שווה לזווית DCF. לפי משפט: במשולש ששתי זוויות בו שוות, הצלעות מולן שוות, DC = CF."
|
| 396 |
+
8. **אסור:** לכתוב "DC = CF" בלי להסביר למה. חובה לבנות את כל שרשרת ההוכחה.
|
| 397 |
+
"""
|
| 398 |
+
# V285.1: Investigation Table (Table UI)
|
| 399 |
+
investigation_keywords = ["חקור", "חקירת", "קיצון", "אסימפטוט", "עליה", "ירידה"]
|
| 400 |
+
is_investigation = "INVESTIGATION" in category or any(x in problem_text for x in investigation_keywords)
|
| 401 |
+
investigation_block = ""
|
| 402 |
+
if is_investigation:
|
| 403 |
+
investigation_block = """
|
| 404 |
+
═══════════════════════════════════════════════════
|
| 405 |
+
📈 הוראות מיוחדות לחקירת פונקציה (Table UI):
|
| 406 |
+
═══════════════════════════════════════════════════
|
| 407 |
+
|
| 408 |
+
בנוסף לשדות הרגילים ב-JSON, באחריותך להוסיף בשורש ה-JSON את השדה "investigation" המילוני הזה:
|
| 409 |
+
"investigation": {
|
| 410 |
+
"function": "הפונקציה ב-LaTeX",
|
| 411 |
+
"derivative": "הנגזרת ב-LaTeX",
|
| 412 |
+
"second_derivative": "נגזרת שנייה ב-LaTeX (השאר ריק אם אין צורך)",
|
| 413 |
+
"domain": "תחום הגדרה עטוף ב-LaTeX",
|
| 414 |
+
"critical_points": [
|
| 415 |
+
{"x": "ערך x", "y": "ערך y", "f_prime": "0", "behavior": "מקסימום/מינימום"}
|
| 416 |
+
],
|
| 417 |
+
"increasing": ["(a, b)", "(1, \\infty)"],
|
| 418 |
+
"decreasing": ["(-\\infty, a)"],
|
| 419 |
+
"concave_up": ["(a, b)"],
|
| 420 |
+
"concave_down": ["(b, c)"]
|
| 421 |
+
}
|
| 422 |
+
"""
|
| 423 |
+
prompt = f"""
|
| 424 |
+
DEPTH: {features['depth']}
|
| 425 |
+
STYLE: {features['style']} (בנימה של '{g['royal']}').
|
| 426 |
+
TONE: {features['tone']}
|
| 427 |
+
|
| 428 |
+
{anchor_block}
|
| 429 |
+
{geo_verified_block}
|
| 430 |
+
|
| 431 |
+
🎯 המשימה: פתור את התרגיל בצורה פדגוגית, מעצימה ובשלבים ברורים.
|
| 432 |
+
|
| 433 |
+
📜 כללי הפדגוגיה של BuddyMath (חובה!):
|
| 434 |
+
{rules_str}
|
| 435 |
+
|
| 436 |
+
{proof_block}
|
| 437 |
+
{investigation_block}
|
| 438 |
+
|
| 439 |
+
═══════════════════════════════════════════════════
|
| 440 |
+
הנחיות לפתרון (Solver Hint):
|
| 441 |
+
{solver_hint}
|
| 442 |
+
═══════════════════════════════════════════════════
|
| 443 |
+
|
| 444 |
+
השאלה לפתרון:
|
| 445 |
+
{problem_text}
|
| 446 |
+
|
| 447 |
+
דגימת סגנון פנייה:
|
| 448 |
+
- פתיחה: "{g['example_open']}"
|
| 449 |
+
- סיום: "{g['example_close']}"
|
| 450 |
+
"""
|
| 451 |
+
return prompt
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
# prompts.py - V275.1 (Safe OCR - Technique over Examples)
|
| 455 |
+
def get_transcription_prompt():
|
| 456 |
+
"""
|
| 457 |
+
V275.1: Safe OCR Prompt - Teaches TECHNIQUE, not specific examples.
|
| 458 |
+
|
| 459 |
+
Why V275.1?
|
| 460 |
+
- V275.0 had specific examples like "\\frac{1}{x}(a + \\frac{(\\ln x)^n}{n})"
|
| 461 |
+
- This caused the LLM to "fit" different functions to the example
|
| 462 |
+
- Now we teach HOW to recognize patterns, not WHAT pattern to expect
|
| 463 |
+
"""
|
| 464 |
+
return """
|
| 465 |
+
TRANSCRIBE the FULL text from the image. Include ALL Hebrew text and ALL mathematical expressions.
|
| 466 |
+
|
| 467 |
+
🎯 **YOUR ONLY JOB: Copy EXACTLY what you see. Do NOT interpret, simplify, or guess.**
|
| 468 |
+
|
| 469 |
+
📐 **TRANSCRIPTION TECHNIQUE (How to be accurate):**
|
| 470 |
+
|
| 471 |
+
1. **ZERO TRUNCATION RULE (CRITICAL - V310.0)**
|
| 472 |
+
- **NEVER skip the beginning of an equation.** If a formula starts with `f(x) =`, `1/x`, or a constant, you MUST include it.
|
| 473 |
+
- Look at the very left edge of the image/expression. Truncating the starting terms is a FATAL ERROR.
|
| 474 |
+
- **FULL CAPTURE:** Transcribe from the first character to the last. No summaries.
|
| 475 |
+
|
| 476 |
+
2. **VISUAL DISAMBIGUATION (V310.0 - NEW)**
|
| 477 |
+
- **1 vs a:** Double-check characters that look like a straight line or small loop. In fractions like `1/x` or `a/x`, verify via context.
|
| 478 |
+
- **2 vs z:** Look for the horizontal base of the `2` vs the sharp angles of the `z`.
|
| 479 |
+
- **0 vs o / O:** Check if it's a number (zero) or a letter (o).
|
| 480 |
+
- **5 vs s / S:** Distinguish the top bar of the `5` from the curves of the `s`.
|
| 481 |
+
- **MATH CONTEXT:** Parameter letters like `a, b, c` are usually distinct from constants like `1, 2, 3`. If a formula has both, look for subtle stylistic differences.
|
| 482 |
+
|
| 483 |
+
3. **FRACTION & NESTING RIGOR**
|
| 484 |
+
- **WORK OUTSIDE-IN:** Identify the outermost structure (e.g., a large fraction) before transcribing the contents.
|
| 485 |
+
- **Numerator/Denominator:** Transcribe EVERYTHING above and below the line. Use `\\frac{num}{den}`.
|
| 486 |
+
- **Parentheses:** Use `\\left(` and `\\right)` for nested or large expressions.
|
| 487 |
+
|
| 488 |
+
4. **SCAN THE EDGES**
|
| 489 |
+
- Check if the expression starts or ends very close to the Bounding Box edge. Truncation usually happens at the very start.
|
| 490 |
+
|
| 491 |
+
✅ **OUTPUT:** The EXACT text from the image, with proper LaTeX for math.
|
| 492 |
+
"""
|
| 493 |
+
|
| 494 |
+
# ==================== V260.0 PEDAGOGICAL PROMPTS ====================
|
| 495 |
+
|
| 496 |
+
def get_strategy_card_prompt(problem_text: str, data_anchor: dict) -> str:
|
| 497 |
+
"""V260.1: Generate high-level strategy as HINTS to encourage self-solving."""
|
| 498 |
+
return f"""
|
| 499 |
+
ROLE: Elite Math Tutor (Pedagogical Architect).
|
| 500 |
+
TASK: Analyze this problem and provide a high-level STRATEGY guide filled with HINTS.
|
| 501 |
+
|
| 502 |
+
PROBLEM:
|
| 503 |
+
{problem_text}
|
| 504 |
+
|
| 505 |
+
DATA (Context):
|
| 506 |
+
{json.dumps(data_anchor, ensure_ascii=False)}
|
| 507 |
+
|
| 508 |
+
INSTRUCTIONS:
|
| 509 |
+
1. Explain the LOGIC of how to approach this problem, but frame it as hints.
|
| 510 |
+
2. === STRATEGY CARD PEDAGOGY RULE ===
|
| 511 |
+
CRITICAL: DO NOT solve the problem here. DO NOT use actual equations or numbers.
|
| 512 |
+
Speak conceptually. Give the student the "blueprint" so they can try it themselves.
|
| 513 |
+
End the `content` section with an encouraging call to action to try it alone first! (e.g., "קחו דף ועט, נסו לפתור לבד לפי הרמזים, ואם נתקעתם - הפתרון המלא מחכה לכם למטה!").
|
| 514 |
+
3. Provide 3-4 bullet points acting as stepping stones/hints.
|
| 515 |
+
4. CRITICAL: Output MUST be in HEBREW (עברית).
|
| 516 |
+
5. Tone: Encouraging, challenging, empowering.
|
| 517 |
+
|
| 518 |
+
OUTPUT JSON:
|
| 519 |
+
{{
|
| 520 |
+
"title": "איך ניגשים לשאלה הזו? 💡",
|
| 521 |
+
"content": "הסבר מילולי קצר המעודד עבודה עצמאית...",
|
| 522 |
+
"steps": [
|
| 523 |
+
"רמז 1: מכיוון שנתונה נקודת קיצון, נסו לחשוב מה זה אומר על הנגזרת...",
|
| 524 |
+
"רמז 2: חיתוך עם הצירים דורש מכם..."
|
| 525 |
+
]
|
| 526 |
+
}}
|
| 527 |
+
"""
|
| 528 |
+
|
| 529 |
+
def get_visual_context_prompt(problem_text: str, category: str) -> str:
|
| 530 |
+
"""V260.0: Generate visual description for the sketch card."""
|
| 531 |
+
return f"""
|
| 532 |
+
ROLE: Visual describer for blind students / schematic generator.
|
| 533 |
+
TASK: Describe the VISUAL SETUP of this math problem.
|
| 534 |
+
|
| 535 |
+
PROBLEM:
|
| 536 |
+
{problem_text}
|
| 537 |
+
|
| 538 |
+
CATEGORY: {category}
|
| 539 |
+
|
| 540 |
+
INSTRUCTIONS:
|
| 541 |
+
1. If GEOMETRY: Describe the shapes, how they connect, what is tangent to what.
|
| 542 |
+
- **CRITICAL**: Populate "geometric_entities" with PRECISE COORDINATES for plotting.
|
| 543 |
+
- If no coordinates are given, ESTIMATE logical coordinates (e.g., A(0,0), B(3,0) for a base).
|
| 544 |
+
2. If FUNCTION: Describe the graph shape (parabola opening up/down), intersection points, asymptotes.
|
| 545 |
+
- **CRITICAL GRAPH DESCRIPTION (V300.5):** כאשר יש גרפים משורטטים בתמונה (כמו פונקציה ונגזרת), עליך לפרט ב-description את התכונות הקריטיות של כל גרף בנפרד (גרף I, גרף II וכו'). חובה לציין: האם הגרף חותך את ראשית הצירים (0,0)? האם הוא מתחיל בערך חיובי/שלילי על ציר ה-y? כמה נקודות חיתוך יש לו בערך עם ציר ה-x? האם יש לו נקודות קיצון בולטות? מידע זה קריטי כדי שהמודל הבא יוכל להתאים נכונה את המשוואות לגרפים.
|
| 546 |
+
- **MULTIPLE CHOICE GRAPHS (V310.0):** בשאלות של "התאם בין גרף לפונקציה", חובה לתת תיאור מתמטי קצר (אסימפטוטות, חיתוך עם צירים) לכל אחד מהגרפים הממוספרים בתמונה בנפרד.
|
| 547 |
+
3. Goal: Help a student "see" the problem before solving.
|
| 548 |
+
4. Keep it simple and descriptive.
|
| 549 |
+
5. CRITICAL: Output MUST be in HEBREW (עברית). Explain the visuals in Hebrew.
|
| 550 |
+
|
| 551 |
+
OUTPUT JSON (STRUCTURE ONLY - USE ACTUAL PROBLEM DATA):
|
| 552 |
+
{{
|
| 553 |
+
"title": "המחשה חזותית ✏️",
|
| 554 |
+
"description": "הסבר מילולי קצר...",
|
| 555 |
+
"geometric_entities": {{
|
| 556 |
+
"points": [{{"label": "<point_name>", "x": 0.0, "y": 0.0}}],
|
| 557 |
+
"segments": [{{"start": "<point_name>", "end": "<point_name>", "color": "blue"}}],
|
| 558 |
+
"circles": [{{"center_x": 0.0, "center_y": 0.0, "radius": 0.0}}]
|
| 559 |
+
}},
|
| 560 |
+
"latex_input": "ONLY the raw mathematical expression. NO 'f(x)=' prefixes. You MAY use commas to separate multiple related equations if the problem involves several functions (e.g. \\\\ln(x), \\\\frac{{1}}{{x}}). Just the single expression or comma-separated expressions. If no graph is needed, leave empty string."
|
| 561 |
+
}}
|
| 562 |
+
|
| 563 |
+
🚨 GRAPH PERSISTENCE RULES (V300.3 — CRITICAL):
|
| 564 |
+
- 'latex_input' is MANDATORY. You MUST provide it. NEVER leave it as null or empty string "".
|
| 565 |
+
- NEVER say "I cannot draw" or "no graph possible". Always provide your best equation.
|
| 566 |
+
- For GEOMETRY: use the main circle/line equation (e.g. "(x-3)^2 + (y-5)^2 = 39").
|
| 567 |
+
- For FUNCTION: use the function equation (e.g. "\\frac{{\\ln(x)}}{{x^2-4}}").
|
| 568 |
+
- For LOCUS: write the final locus equation.
|
| 569 |
+
- If you are TRULY unsure: use "x" as a placeholder — the server will handle it.
|
| 570 |
+
- The latex_input must use valid LaTeX WITHOUT $$ wrappers (e.g. "x^2 + y^2 = 25" not "$$x^2+y^2=25$$").
|
| 571 |
+
|
| 572 |
+
🚨 HELPER SKETCH RULE (V300.3 — NEW):
|
| 573 |
+
If no image is provided (blind setup):
|
| 574 |
+
- Read the verbal description of the geometry/trigonometry problem carefully.
|
| 575 |
+
- Extract coordinates, lines, or functions from the text to generate an illustration sketch.
|
| 576 |
+
- Your goal is to CREATE the visual intuition that the student is missing.
|
| 577 |
+
- Even without an image, you MUST populate "geometric_entities" and "latex_input".
|
| 578 |
+
"""
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
# ==================== V8.6.8 MASTER PROMPT (THE ANCHOR STABILITY FIX) ====================
|
| 582 |
+
|
| 583 |
+
def get_master_prompt_v860(category: str = "", problem_text: str = ""):
|
| 584 |
+
"""
|
| 585 |
+
V8.6.9: The Anchor Stability Fix + Dynamic JSON for Graphs.
|
| 586 |
+
"""
|
| 587 |
+
graph_field = ""
|
| 588 |
+
is_graph = category == "GRAPH_IDENTIFICATION" or ("גרף" in problem_text and "איזה" in problem_text)
|
| 589 |
+
if is_graph:
|
| 590 |
+
graph_field = """
|
| 591 |
+
"graph_analysis": {
|
| 592 |
+
"function_analysis": "תיאור מתמטי מפורט של הפונקציה (נקודות חיתוך, קיצון, אסימפטוטות)",
|
| 593 |
+
"graph_options": [{"id": "I", "description": "..."}, {"id": "II", "description": "..."}],
|
| 594 |
+
"matching_logic": "הסבר המקשר בין תכונות הפונקציה למאפייני הגרף הנבחר",
|
| 595 |
+
"final_match": "התשובה הסופית (למשל: גרף II)"
|
| 596 |
+
},"""
|
| 597 |
+
|
| 598 |
+
prompt_template = r"""
|
| 599 |
+
🔴 MASTER PROMPT BLOCK — VERSION V8.6.8 (THE ANCHOR STABILITY)
|
| 600 |
+
|
| 601 |
+
CRITICAL: You MUST output ONLY valid JSON. Absolutely NO conversational text before or after the JSON block.
|
| 602 |
+
Do NOT wrap the JSON in markdown code blocks like ```json.
|
| 603 |
+
|
| 604 |
+
ROLE:
|
| 605 |
+
אתה מורה פרטי למתמטיקה (הטוב ביותר בארץ!). המטרה שלך היא להסביר לתלמיד לא רק *איך* פותרים, אלא *למה* עושים כל צעד. רמת ההסבר צריכה להיות כזו שגם תתלמיד שרואה את החומר פעם ראשונה יבין 100% מהדרך. הגישה שלך היא חמה, מעודדת ומעצימה (סגנון 'הנסיך/הנסיכה').
|
| 606 |
+
|
| 607 |
+
═══════════════════════════════════════════
|
| 608 |
+
ABSOLUTE RULES (violations cause immediate rejection):
|
| 609 |
+
═══════════════════════════════════════════
|
| 610 |
+
1. **DATA ANCHOR SUPREMACY (V8.9.2):** The equations in the JSON Data Anchor are the ABSOLUTE TRUTH. You MUST solve the exact math provided in the Anchor. Do NOT attempt to re-read the image for the main function; trust the pre-validated Data Anchor.
|
| 611 |
+
2. **ZERO MAGIC MATH (BABY STEPS):**
|
| 612 |
+
- NEVER skip algebraic steps. Show moving sides, dividing, and expanding brackets.
|
| 613 |
+
- ALWAYS explain the mathematical rule/theorem *BEFORE* applying it.
|
| 614 |
+
* Bad: "נגזור ונשווה לאפס: f'(x) = 2x"
|
| 615 |
+
* Good: "כדי למצוא נקודת קיצון נגזור את הפונקציה. מכיוון שזו מנה, נשתמש בכלל המנה האומר ש... נגזור את המונה בנפרד ואת המכנה בנפרד:"
|
| 616 |
+
3. **COMPLETE THE MISSION (ANTI-TRUNCATION):** Never abandon the task. You MUST reach the final numeric/algebraic answer. If asked for extrema, you must find x, y, and classify (max/min).
|
| 617 |
+
4. **ANTI-ASCII-ART RULE (V8.8.8):** NEVER attempt to draw or sketch a graph using text characters, keyboard symbols, slashes, or ASCII art (e.g., do not use |, /, \\, -, _ to make a picture). If asked to sketch a graph, ONLY describe its mathematical properties in text (e.g., intersections, asymptotes).
|
| 618 |
+
5. **CONTENT TYPE SEPARATION (IRON LAW - V9.1.0):**
|
| 619 |
+
- All LaTeX math and steps must go inside the `block_math` list for clear UI display.
|
| 620 |
+
- `block_math` MUST contain ONLY PURE MATH. Absolutely NO HEBREW LETTERS, no words, no explanations.
|
| 621 |
+
- Do NOT put coordinate lists or points like `A(0,1), B(2,3)` in `block_math`. Points and their names MUST stay in `content_mixed`.
|
| 622 |
+
- EXTREME SIMPLICITY MANDATE FOR MATH BLOCKS:
|
| 623 |
+
* `block_math` MUST contain exactly ONE mathematical operation per step.
|
| 624 |
+
* NEVER use commas (,) to separate multiple equations.
|
| 625 |
+
* NEVER chain equations (e.g., NO `x = 2 + 2 = 4`). Break EVERY '=' into a new JSON step.
|
| 626 |
+
* MAXIMUM LENGTH: A single `block_math` string MUST NEVER exceed 40 characters. If a calculation is longer (like a complex fraction), you MUST break it down into intermediate variables across multiple steps.
|
| 627 |
+
- Descriptions and casual variables must stay in `content_mixed`.
|
| 628 |
+
- NEVER write Hebrew inside `block_math` or `final_answer`. (CRITICAL: violates the algebraic validator contract).
|
| 629 |
+
6. **ANTI-TABLE RULE & EXTREMA ANALYSIS:**
|
| 630 |
+
- NEVER use Markdown tables (e.g., using | and -).
|
| 631 |
+
- When finding Extrema (Max/Min), DO NOT suggest using a table. Instead, use explicit text steps to test the intervals.
|
| 632 |
+
- Example: "נבדוק את תחומי העלייה והירידה: עבור $x=\pi/4$ הנגזרת חיובית, ועבור $x=3\pi/4$ היא שלילית. לכן זו נקודת מקסימום".
|
| 633 |
+
- You MUST complete the calculation, find the Y values ($y=f(x)$), and explicitly state the classification (Max or Min).
|
| 634 |
+
7. **STRICT JSON ONLY:** No preamble, no post-amble, no markdown.
|
| 635 |
+
7. **ANTI-PARADOX PROTOCOL (V8.6.9):**
|
| 636 |
+
- If your mathematical calculation seems to lead to a paradox (e.g., length is 0, or points C and D are the exact same), DO NOT STOP THE SOLUTION.
|
| 637 |
+
- 99% of the time, this is a calculation error on your end. RECALCULATE your steps using a different geometric or algebraic approach.
|
| 638 |
+
- NEVER give up. NEVER output "אני מזהה סתירה בנתונים". You MUST find a valid path and provide the final answer for the student.
|
| 639 |
+
8. **LATEX SYNTAX RULE (V8.6.9 — CRITICAL):**
|
| 640 |
+
- You MUST strictly use LaTeX macros for all mathematical functions.
|
| 641 |
+
- You MUST write \ln (with a backslash) and NEVER just ln.
|
| 642 |
+
- You MUST write \sin, \cos, \tan, \log.
|
| 643 |
+
- Failure to use the backslash will crash the KaTeX renderer and Fail validation.
|
| 644 |
+
9. **ANTI-NEWLINE & BIDI SAFETY:**
|
| 645 |
+
- In the `final_answer` field, NEVER use `\\` or `\newline` for line breaks.
|
| 646 |
+
- The `final_answer` field MUST BE 100% PURE MATH. Absolutely NO Hebrew letters, words, or explanations. If the answer requires a textual explanation, put the text in `content_mixed` of the final step, and leave `final_answer` with just the numbers/math, or empty.
|
| 647 |
+
- If there are multiple answers, separate them ONLY with English commas (e.g., "x=1, x=2").
|
| 648 |
+
10. **VARIABLE CONSISTENCY (V8.6.8):**
|
| 649 |
+
- Always use standard mathematical variables ($x, y, z, m, n$) as provided in context. Never invent new variable names not found in the Data Anchor or original problem.
|
| 650 |
+
10. **STRATEGY CARD NO-SPOILER RULE (CRITICAL):**
|
| 651 |
+
- The `strategy_card` MUST NOT contain any numbers, final equations, derivatives, or solutions.
|
| 652 |
+
- It must ONLY contain high-level conceptual hints ("רמז 1: כדי למצוא קיצון, חשבו על...").
|
| 653 |
+
11. **NO HEBREW OR LOGICAL ARROWS IN LATEX:**
|
| 654 |
+
- Within `block_math` and all LaTeX parts, DO NOT use Hebrew text.
|
| 655 |
+
- DO NOT use logical arrows like `\implies`, `\Rightarrow`, or `\iff` as they cause parsing errors. Use descriptive words in `content_mixed` instead.
|
| 656 |
+
12. **PEDAGOGICAL HIGHLIGHTING:**
|
| 657 |
+
- Use `\\color{red}{...}` for highlighting ONLY inside the `content_mixed` field when explaining inline math.
|
| 658 |
+
- NEVER use color tags inside `block_math` or `final_answer` as it will crash the backend algebraic validator.
|
| 659 |
+
13. **MULTI-IMAGE & GRADING LOGIC (V308.0 - CRITICAL)**:
|
| 660 |
+
- אם קיבלת תמונה אחת בלבד: עליך להניח שהתלמיד מבקש פתרון מלא מההתחלה, או הסבר רגיל שלבים שלבים כפי שביקש.
|
| 661 |
+
- אם קיבלת יותר מתמונה אחת: התמונה הראשונה היא השאלה המקורית. שאר התמונות הן נסיונות הפתרון של התלמיד בכתב יד.
|
| 662 |
+
- במצב של יותר מתמונה אחת עליך לעבור ל**מצב 'בדיקת שיעורי בית' (Grading Mode)** בו אתה ראשית בודק היכן התלמיד טעה בפתרון שלו מהמחברת, מתקן אותו נקודתית וחולק לו שבחים על מה שכן הצליח, ואז מחזיר אותו למסלול הנכון להמשך התרגיל. אל תוציא מיד פתרון מלא במצב זה, תן לו להבין קודם איפה הטעות!
|
| 663 |
+
14. **AI ASSESSMENT TELEMETRY (CRITICAL FOR ANALYTICS):**
|
| 664 |
+
- בסיום הניתוח, עליך להוסיף אובייקט `assessment` חבוי ב-JSON.
|
| 665 |
+
- בחר את ה-`primary_skill` מתוך הרשימה הסגורה הבאה בלבד: ["אלגברה", "גיאומטריה", "טריגונומטריה", "הסתברות", "חשבון דיפרנציאלי"]. בשום פנים ואופן אל תמציא נושאים חדשים.
|
| 666 |
+
- `sub_skill`: תיאור חופשי קצר של תת-הנושא הספציפי בתרגיל (למשל: "חוקי חזקות").
|
| 667 |
+
- `mastery_score`: ציון מ-1 עד 100 על סמך איכות הפתרון של התלמיד בתמונה. אם אין תמונת פתרון, תן ציון על פי הבנת התרגיל או שים ציון ריק.
|
| 668 |
+
- `parent_note`: משפט אחד קצר וחיובי המיועד להוריו של התלמיד, המסכם את חוזקותיו (למשל: "התלמיד גילה הבנה טובה בבידוד משתנים").
|
| 669 |
+
15. **GRAPH ANTI-HALLUCINATION (CRITICAL - V310.0):**
|
| 670 |
+
- When asked to match a function to a graph, NEVER rely on visual size estimation or "gut feeling" from the image.
|
| 671 |
+
- You MUST find mathematical anchors: intersection points with axes ($x=0$ or $y=0$), extrema, or asymptotes.
|
| 672 |
+
- Cross-reference your algebraic findings with the provided graphs.
|
| 673 |
+
- If the image is ambiguous, state clearly: "בגלל שהשרטוט אינו חד-משמעי, עלינו לוודא את נכונות הגרף באמצעות הצבת נקודות חיתוך".
|
| 674 |
+
16. **CHRONOLOGICAL LOGIC & DATA ISOLATION (V8.6.9 - CRITICAL):**
|
| 675 |
+
- You MUST solve sub-questions strictly in order.
|
| 676 |
+
- NEVER use data, parameters (like $a=1$), or specific values that explicitly belong to a LATER sub-question (e.g., Section ב') to solve an EARLIER sub-question (e.g., Section א').
|
| 677 |
+
- Solve earlier sections algebraically using general variables unless the data is part of the global question anchor.
|
| 678 |
+
- If a student's finding in Section א' is required for Section ב', you may use it, but NEVER the other way around.
|
| 679 |
+
17. **VISUAL GRAPH ANALYSIS (V8.9.1):** When asked to identify a graph from an image containing multiple options (e.g., I, II, III, IV), you MUST explicitly describe what you see in the image for EACH option before making a choice. Base your final selection strictly on matching your mathematical deductions (domain, asymptotes, roots) to the visual features of the graphs in the image. Ensure these descriptions are in `content_mixed` to maintain logical transparency for the student.
|
| 680 |
+
18. **NO GUESSING RULE (V310.0):** אם הנתונים בתמונה אינם מספיקים כדי לקבוע בוודאות איזה גרף מתאים לאזו פונקציה, אל תנחש! הצג את הניתוח המתמטי והסבר מה חסר כדי להגיע להכרעה. ניתן להוסיף: "מומלץ לבחון את הגרף המצורף (אם קיים) כדי לראות את המאפיינים שחישבנו."
|
| 681 |
+
|
| 682 |
+
|
| 683 |
+
═══════════════════════════════════════════════════
|
| 684 |
+
REQUIRED JSON STRUCTURE (EXACT KEYS):
|
| 685 |
+
═══════════════════════════════════════════════════
|
| 686 |
+
{{
|
| 687 |
+
{graph_field}
|
| 688 |
+
"strategy_card": {{
|
| 689 |
+
"title": "איך ניגשים לשאלה הזו? 🧭",
|
| 690 |
+
"intro": "היי! נראה שיש לנו פה שאלת [נושא] מצוינת...",
|
| 691 |
+
"bullets": ["רמז 1: [רמז מוכוון פעולה ללא ספוילרים או תשובות]", "רמז 2: [רמז נוסף]"],
|
| 692 |
+
"call_to_action": "קחו דף ועט, נסו לפתור לבד לפי הרמזים, ואם נתקעתם – פתחו את הסעיפים למטה!"
|
| 693 |
+
},
|
| 694 |
+
"approach": "הקדמה מלאה: פתיח חם, אישי ומלמד שמסביר בשפה פתוחה מה נלך לעשות בתרגיל ולמה (למשל: 'כדי למצוא אסימפטוטות אנחנו נבדוק מה קורה בפלוס ומינוס אינסוף').",
|
| 695 |
+
"steps": [
|
| 696 |
+
{
|
| 697 |
+
"step_id": 1,
|
| 698 |
+
"content_mixed": "(הסבר פדגוגי מפורט ואמהי) כדי למצוא את הנגזרת נשתמש בכלל המנה, ונגזור את המונה בנפרד ואת המכנה בנפרד:",
|
| 699 |
+
"block_math": "y' = \\frac{\\color{blue}{2x} \\cdot (x-1) - x^2 \\cdot \\color{blue}{1}}{(x-1)^2}"
|
| 700 |
+
},
|
| 701 |
+
{
|
| 702 |
+
"step_id": 2,
|
| 703 |
+
"content_mixed": "(המשך הסבר חם ומפורט) נציב כעת $x=\\color{red}{5}$ במשוואה שצמצמנו:",
|
| 704 |
+
"block_math": "y' = \\frac{\\color{red}{5}^2 - 2\\cdot \\color{red}{5}}{(\\color{red}{5}-1)^2} = \\frac{15}{16}"
|
| 705 |
+
}
|
| 706 |
+
],
|
| 707 |
+
"final_answer": "התשובה הסופית נטו (LaTeX)",
|
| 708 |
+
"teacher_summary": {
|
| 709 |
+
"audio_pitch": "פיצ' שיחתי (כמו פודקאסט קצר) של 30-40 שניות שבו המורה מסכמת את התרגיל וחולקת תובנות. כתבי בטון אמהי, חם ומעודד.",
|
| 710 |
+
"key_concepts": ["סיכום מלמד 1: הפקת לקחים ותובנה אסטרטגית מהתרגיל (למשל 'שימו לב שתמיד לפני גזירת מנה נרצה לסדר את הפונקציה')", "סיכום מלמד 2: כלל אצבע שאפשר לקחת משאלה זו הלאה"],
|
| 711 |
+
"formulas": ["נוסחאות מרכזיות ��הן השתמשנו, בפורמט LaTeX נקי (ללא סוגרי דולר)"]
|
| 712 |
+
},
|
| 713 |
+
"assessment": {
|
| 714 |
+
"primary_skill": "אלגברה / גיאומטריה / טריגונומטריה / הסתברות / חשבון דיפרנציאלי",
|
| 715 |
+
"sub_skill": "תת הנושא הספציפי",
|
| 716 |
+
"mastery_score": 85,
|
| 717 |
+
"parent_note": "משפט על הצלחת התלמיד עבור דוח ההורים."
|
| 718 |
+
}
|
| 719 |
+
}}
|
| 720 |
+
|
| 721 |
+
CRITICAL: Output ONLY the JSON block. No text before, no text after. No markdown formatting.
|
| 722 |
+
"""
|
| 723 |
+
return prompt_template.replace("{graph_field}", graph_field)
|
| 724 |
+
|
| 725 |
+
def get_master_prompt_v430(category: str = "", problem_text: str = ""):
|
| 726 |
+
"""V4.3.0: Legacy placeholder, redirecting to V8.6.0 for 'The Golden Merge'"""
|
| 727 |
+
return get_master_prompt_v860(category=category, problem_text=problem_text)
|
| 728 |
+
|
| 729 |
+
|
| 730 |
+
# ==================== V285.0: CHECK ME PROMPT (HOMEWORK VERIFICATION) ====================
|
| 731 |
+
|
| 732 |
+
def get_check_me_prompt(grade: str, student_name: str, student_gender: str = "M", data_anchor: dict = None):
|
| 733 |
+
"""
|
| 734 |
+
V285.1: Dedicated prompt for the "Check Me" feature with DATA ANCHOR.
|
| 735 |
+
The LLM acts as a homework checker, NOT a solver.
|
| 736 |
+
"""
|
| 737 |
+
# Gender-aware phrases
|
| 738 |
+
if student_gender == "F":
|
| 739 |
+
g_addr = "את"
|
| 740 |
+
g_did = "עשית"
|
| 741 |
+
g_chose = "בחרת"
|
| 742 |
+
g_forgot = "שכחת"
|
| 743 |
+
g_started = "התחלת"
|
| 744 |
+
g_great = "מעולה"
|
| 745 |
+
g_dear = f"{student_name} יקרה"
|
| 746 |
+
else:
|
| 747 |
+
g_addr = "אתה"
|
| 748 |
+
g_did = "עשית"
|
| 749 |
+
g_chose = "בחרת"
|
| 750 |
+
g_forgot = "שכחת"
|
| 751 |
+
g_started = "התחלת"
|
| 752 |
+
g_great = "מעולה"
|
| 753 |
+
g_dear = f"{student_name} יקר"
|
| 754 |
+
|
| 755 |
+
anchor_block = ""
|
| 756 |
+
if data_anchor:
|
| 757 |
+
anchor_block = f"""
|
| 758 |
+
══════════════════════════════════════════════════════
|
| 759 |
+
📜 DATA INTEGRITY RULE (ABSOLUTE TRUTH):
|
| 760 |
+
══════════════════════════════════════════════════════
|
| 761 |
+
הנתונים להלן הם נתוני השאלה המקוריים כפי שזוהו בשלב הניתוח המוקדם.
|
| 762 |
+
עליך לבדוק את פתרון התלמיד אל מול הנתונים האלו בדיוק!
|
| 763 |
+
{json.dumps(data_anchor, indent=2, ensure_ascii=False)}
|
| 764 |
+
══════════════════════════════════════════════════════
|
| 765 |
+
"""
|
| 766 |
+
|
| 767 |
+
return f"""
|
| 768 |
+
🎓 תפקיד: אתה בודקת שיעורי בית — מורה פרטית חמה שבודקת את העבודה של תלמיד.
|
| 769 |
+
🚫 אתה לא פותר את התרגיל מחדש! אתה מנתח את מה שהתלמיד כתב.
|
| 770 |
+
|
| 771 |
+
📸 היררכיית תמונות:
|
| 772 |
+
1. התמונה הראשונה (image_00) היא השאלה המקורית מהספר/מבחן.
|
| 773 |
+
2. כל שאר התמונות (image_01 ומעלה) מכילות את שלבי הפתרון שכתב התלמיד בכתב יד.
|
| 774 |
+
|
| 775 |
+
👤 התלמיד: {student_name}, כיתה {grade}.
|
| 776 |
+
👑 מגדר: {"נקבה" if student_gender == "F" else "זכר"}. השתמש/י בלשון מתאימה.
|
| 777 |
+
|
| 778 |
+
{anchor_block}
|
| 779 |
+
|
| 780 |
+
═══════════════════════════════════════════════════
|
| 781 |
+
📐 שלוש שלבי הבדיקה (חובה לבצע לפי הסדר):
|
| 782 |
+
═══════════════════════════════════════════════════
|
| 783 |
+
|
| 784 |
+
שלב א' — בדיקת מתודולוגיה (אסטרטגיה):
|
| 785 |
+
• זהה את התרגיל מתוך התמונה.
|
| 786 |
+
• בדוק: האם התלמיד בכלל בחר בשיטת פתרון נכונה?
|
| 787 |
+
• למשל: האם השתמש בנוסחת השורשים כשצריך פירוק? האם גזר כשצריך אינטגרל?
|
| 788 |
+
• אם השיטה שגויה מיסודה — עצור כאן. הסבר את הטעות התפיסתית בלבד.
|
| 789 |
+
|
| 790 |
+
שלב ב' — אימות אלגברי צעד-אחר-צעד:
|
| 791 |
+
• סרוק את שורות הפתרון שהתלמיד כתב.
|
| 792 |
+
• בדוק כל מעבר: העברת אגפים, כינוס איברים, סימנים, חזקות.
|
| 793 |
+
• ברגע שמזהה שבירה של חוק אלגברי — בודד את השורה המדויקת.
|
| 794 |
+
• ציין מה היה צריך להיות ולמה.
|
| 795 |
+
|
| 796 |
+
שלב ג' — רכיבים ויזואליים:
|
| 797 |
+
• אם התלמיד צייר גרף, שרטוט גיאומטרי, או טבלת סימנים שגויים — ציין מה שגוי.
|
| 798 |
+
• אם אין רכיב ויזואלי — דלג על שלב זה.
|
| 799 |
+
|
| 800 |
+
════════════════════════════════════════════════���══
|
| 801 |
+
🎯 כללי ברזל:
|
| 802 |
+
═══════════════════════════════════════════════════
|
| 803 |
+
1. אל תפתור את התרגיל מחדש! רק בדוק את מה שהתלמיד כתב.
|
| 804 |
+
2. אם הכל נכון — תן חיזוק חיובי אמיתי ומפורט.
|
| 805 |
+
3. אם יש טעות — הצבע על השורה המדויקת, הסבר מה שגוי, ומה היה צריך להיות.
|
| 806 |
+
4. טון: חם, מעודד, מקצועי. כמו מורה פרטית שבודקת מבחן עם העט האדום, אבל בלב חם.
|
| 807 |
+
5. כל התשובה בעברית.
|
| 808 |
+
6. אם אתה לא מצליח לזהות את כתב היד — ציין זאת בנימוס ובקש צילום ברור יותר.
|
| 809 |
+
|
| 810 |
+
═══════════════════════════════════════════════════
|
| 811 |
+
📋 פורמט JSON נדרש (STRICT — ללא טקסט לפני או אחרי):
|
| 812 |
+
═══════════════════════════════════════════════════
|
| 813 |
+
{{
|
| 814 |
+
"verdict": "correct" | "has_errors" | "methodology_error" | "unreadable",
|
| 815 |
+
"score": ציון מ-0 עד 100 על העבודה (100 אם הכל נכון),
|
| 816 |
+
"problem_identified": "מה התרגיל שזוהה מהתמונה (LaTeX)",
|
| 817 |
+
"methodology_ok": true | false,
|
| 818 |
+
"methodology_note": "הערה על השיטה שנבחרה (ריק אם הכל תקין)",
|
| 819 |
+
"mistakes": [
|
| 820 |
+
{{
|
| 821 |
+
"mistake_description": "תיאור קצר של הטעות",
|
| 822 |
+
"correction_tip": "טיפ איך לתקן"
|
| 823 |
+
}}
|
| 824 |
+
],
|
| 825 |
+
"feedback_steps": [
|
| 826 |
+
{{
|
| 827 |
+
"step_id": 1,
|
| 828 |
+
"student_wrote": "מה שהתלמיד כתב בשורה זו (LaTeX)",
|
| 829 |
+
"is_correct": true | false,
|
| 830 |
+
"error_description": "אם שגוי: מה הטעות ולמה",
|
| 831 |
+
"should_be": "מה היה צריך להיות (LaTeX, ריק אם נכון)"
|
| 832 |
+
}}
|
| 833 |
+
],
|
| 834 |
+
"visual_note": "הערה על שרטוט/גרף אם רלוונטי, אחרת null",
|
| 835 |
+
"encouragement": "משפט חיזוק חיובי אישי ל{student_name}",
|
| 836 |
+
"correct_final_answer": "התשובה הנכונה של התרגיל (LaTeX)"
|
| 837 |
+
}}
|
| 838 |
+
|
| 839 |
+
CRITICAL: Output ONLY the JSON block. No text before, no text after. No markdown formatting.
|
| 840 |
+
"""
|
| 841 |
+
|
| 842 |
+
|
| 843 |
+
# ==================== V285.1: TEACHER SUMMARY PROMPT (PEDAGOGICAL) ====================
|
| 844 |
+
|
| 845 |
+
def get_teacher_summary_prompt(student_name: str, student_gender: str = "M"):
|
| 846 |
+
"""
|
| 847 |
+
V285.1: Prompt for generating a pedagogical teacher summary.
|
| 848 |
+
The LLM summarizes what was learned, key concepts, formulas, and generates TTS text.
|
| 849 |
+
"""
|
| 850 |
+
if student_gender == "F":
|
| 851 |
+
g_addr = "את"
|
| 852 |
+
g_learned = "למדת"
|
| 853 |
+
g_solved = "פתרת"
|
| 854 |
+
g_dear = student_name
|
| 855 |
+
else:
|
| 856 |
+
g_addr = "אתה"
|
| 857 |
+
g_learned = "למדת"
|
| 858 |
+
g_solved = "פתרת"
|
| 859 |
+
g_dear = student_name
|
| 860 |
+
|
| 861 |
+
return f"""
|
| 862 |
+
אתה מורה למתמטיקה שמסכמת שיעור. קיבלת את הבעיה ואת הפתרון שנוצר.
|
| 863 |
+
המשימה שלך: ליצור סיכום פדגוגי שמתמקד בנושא שנלמד, לא בשאלה הספציפית.
|
| 864 |
+
|
| 865 |
+
👤 התלמיד: {student_name}
|
| 866 |
+
👑 מגדר: {"נקבה" if student_gender == "F" else "זכר"}
|
| 867 |
+
|
| 868 |
+
═══════════════════════════════════════════════════
|
| 869 |
+
📋 מה לכלול בסיכום:
|
| 870 |
+
═══════════════════════════════════════════════════
|
| 871 |
+
|
| 872 |
+
1. topic_summary: סיכום קצר של הנושא המתמטי שעסקנו בו (לא השאלה עצמה).
|
| 873 |
+
למשל: "גזירת פונקציות פולינומיאליות" או "חישוב שטח מתחת לגרף".
|
| 874 |
+
|
| 875 |
+
2. key_concepts: רשימה של 2-4 תובנות מפתח ונקודות חשובות שכדאי לזכור לתרגילים הבאים.
|
| 876 |
+
למשל: "כשגוזרים חזקה, מורידים את המעריך ומחסרים 1" — כללי, לא ספציפי.
|
| 877 |
+
|
| 878 |
+
3. formulas_to_remember: רשימה של 1-3 נוסחאות גנריות שהשתמשנו בהן.
|
| 879 |
+
לכתוב ב-LaTeX.
|
| 880 |
+
למשל: "\\\\frac{{d}}{{dx}} x^n = n \\\\cdot x^{{n-1}}"
|
| 881 |
+
אל תכלול ביטויים מספריים ספציפיים מהשאלה! רק נוסחאות כלליות.
|
| 882 |
+
|
| 883 |
+
4. tts_speech: טקסט דיבור קצר וחם למורה (4-6 משפטים).
|
| 884 |
+
⚠️ כללי ברזל ל-TTS:
|
| 885 |
+
- עברית בלבד, ללא LaTeX, ללא סימנים מתמטיים, ללא אימוג'ים
|
| 886 |
+
- אל תקרא לתלמיד "נסיך", "נסיכה", "גיבור", "אלוף" - רק בשם {student_name}
|
| 887 |
+
- תדבר בגוף שני {"נקבה" if student_gender == "F" else "זכר"}
|
| 888 |
+
- טון: חם ומקצועי, כמו מורה פרטית שמסכמת שיעור
|
| 889 |
+
- ציין את הנושא שלמדנו, תובנה מרכזית אחת, ומשפט עידוד
|
| 890 |
+
|
| 891 |
+
═══════════════════════════════════════════════════
|
| 892 |
+
📋 פורמט JSON נדרש:
|
| 893 |
+
═══════════════════════════════════════════════════
|
| 894 |
+
{{
|
| 895 |
+
"topic_summary": "שם הנושא שנלמד (קצר)",
|
| 896 |
+
"key_concepts": ["תובנה 1", "תובנה 2", "תובנה 3"],
|
| 897 |
+
"formulas_to_remember": ["LaTeX נוסחה 1", "LaTeX נוסחה 2"],
|
| 898 |
+
"tts_speech": "טקסט דיבור עברי נקי ל-TTS"
|
| 899 |
+
}}
|
| 900 |
+
"""
|
| 901 |
+
|
| 902 |
+
def get_anchor_validation_prompt(ocr_json: dict) -> str:
|
| 903 |
+
"""
|
| 904 |
+
V8.9.2: Dedicated prompt for the Orchestrator's Data Anchor Validator.
|
| 905 |
+
Takes the raw OCR JSON and the image to produce a syntactically perfect 'Absolute Truth'.
|
| 906 |
+
"""
|
| 907 |
+
ocr_str = json.dumps(ocr_json, indent=2, ensure_ascii=False)
|
| 908 |
+
|
| 909 |
+
return f"""
|
| 910 |
+
You are a strict Mathematical Transcriber & Validator.
|
| 911 |
+
Look at the provided OCR JSON and the original image.
|
| 912 |
+
|
| 913 |
+
OCR JSON (Raw Extraction):
|
| 914 |
+
{ocr_str}
|
| 915 |
+
|
| 916 |
+
⚠️ MISSION:
|
| 917 |
+
The OCR often corrupts complex fractions, missing brackets, or mangling operators.
|
| 918 |
+
Your ONLY job is to output a verified, syntactically perfect JSON containing the main mathematical functions and parameters exactly as they appear in the image.
|
| 919 |
+
|
| 920 |
+
**CRITICAL VISION PROTOCOL:**
|
| 921 |
+
You are a strict Visual Validator, NOT a text auto-completer. When evaluating the extracted math, you MUST cross-reference the text directly against the ORIGINAL IMAGE.
|
| 922 |
+
If the input string is syntactically broken, truncated, or missing components (e.g., dropped fractions, missing multipliers outside parentheses), DO NOT delete terms to artificially 'fix' the equation.
|
| 923 |
+
You MUST visually reconstruct the EXACT mathematical expression pixel-for-pixel as it appears in the image. Do not hallucinate values (e.g., changing $e$ to $\sqrt{e}$). MATCH THE PIXELS EXACTLY.
|
| 924 |
+
|
| 925 |
+
RULES:
|
| 926 |
+
1. Fix any hanging operators (e.g., 'a+', 'x-').
|
| 927 |
+
2. Restore missing multipliers or fractions (e.g., if image shows '1/x' but OCR missed it).
|
| 928 |
+
3. Ensure all brackets are closed and standard variables are used.
|
| 929 |
+
4. If the OCR is correct, keep it as is.
|
| 930 |
+
5. CRITICAL: Do NOT solve the problem. Do NOT explain.
|
| 931 |
+
6. Output ONLY the corrected JSON following the exact structure of the input.
|
| 932 |
+
7. V8.9.2: If you fix a critical syntax error (like 'a+' -> 'a+1/x^2'), ensure the final result is mathematically plausible based on the visual evidence.
|
| 933 |
+
8. **HORIZONTAL VERIFICATION (V8.9.3):** Check the horizontal order of elements. If an element is to the left of a bracket in the image, it MUST be to the left of the bracket in your JSON. Do NOT let the Hebrew text flow (RTL) swap the positions of multipliers or terms.
|
| 934 |
+
9. **CRITICAL JSON STRUCTURE RULE (V8.9.4):** Every output MUST be a flat JSON dictionary with UNIQUE keys. Do NOT use duplicate keys like "equation" multiple times. If there are multiple equations, use "equation_1", "equation_2", etc.
|
| 935 |
+
|
| 936 |
+
Return ONLY the corrected JSON.
|
| 937 |
+
"""
|