Spaces:
Runtime error
Runtime error
Deploy DictaLM 2.0 API server
Browse filesThis view is limited to 50 files because it contains too many changes. ย See raw diff
- .env.example +0 -5
- .gcloudignore +0 -16
- .gitignore +0 -9
- Dockerfile +15 -24
- README.md +4 -32
- all_users.json +0 -34
- analytics.py +0 -77
- audio_generator.py +0 -267
- backend/__init__.py +0 -2
- backend/extractor.py +0 -127
- backend/math_schema.py +0 -98
- backend/pipeline.py +0 -102
- backend/tests/test_pipeline.py +0 -397
- backend/universal_verifier.py +0 -480
- backend/variable_pinner.py +0 -113
- config.py +0 -63
- constraint_checks.py +0 -159
- cost_tracker.py +0 -93
- create_mock_user.py +0 -49
- curriculum_engine.py +0 -115
- curriculum_israel.py +0 -579
- debug_llm_payload.py +0 -42
- deploy_dev.ps1 +0 -31
- deploy_dev.sh +0 -40
- deploy_hf +0 -1
- docs/legacy_prompts_backup.txt +0 -937
- domain/curriculum_classifier.py +0 -94
- domain/math_normalizer.py +0 -38
- domain/math_validator.py +0 -316
- domain/ontology.py +0 -37
- domain/pedagogical_renderer.py +0 -264
- domain/processing_strategy.py +0 -7
- domain/proposal_engine.py +0 -189
- domain/risk_engine.py +0 -66
- domain/schemas.py +0 -19
- domain/semantic_bank.py +0 -366
- domain/step_types.py +0 -71
- domain/telemetry.py +0 -177
- domain/validator.py +0 -148
- exercise_generator.py +0 -89
- explanation_math_firewall.py +0 -60
- find_admin.py +0 -16
- find_dotan.py +0 -25
- firebase_manager.py +0 -184
- fix_dotan_balance_script.py +0 -54
- fix_now.py +0 -37
- fix_welcome_audio.py +0 -35
- gcs_lifecycle_rule.json +0 -11
- generate_dotan_report.py +0 -97
- generate_new_welcome.py +0 -102
.env.example
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
GOOGLE_API_KEY=your-gemini-api-key-here
|
| 2 |
-
|
| 3 |
-
# OCR Mode: development = Stitch & Strip engine (new, single-pass)
|
| 4 |
-
# production = Triple-Pass legacy (safe, proven)
|
| 5 |
-
OCR_STRIP_MODE=development
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gcloudignore
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
.gcloudignore
|
| 2 |
-
.git
|
| 3 |
-
.gitignore
|
| 4 |
-
venv/
|
| 5 |
-
.venv/
|
| 6 |
-
.venv_fix/
|
| 7 |
-
__pycache__/
|
| 8 |
-
*.pyc
|
| 9 |
-
*.pyo
|
| 10 |
-
*.pyd
|
| 11 |
-
.pytest_cache/
|
| 12 |
-
tests/
|
| 13 |
-
test_results/
|
| 14 |
-
logs/
|
| 15 |
-
server.log
|
| 16 |
-
deploy_hf/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
__pycache__/
|
| 2 |
-
*.pyc
|
| 3 |
-
*.pyo
|
| 4 |
-
serviceAccountKey*.json
|
| 5 |
-
*credentials.json
|
| 6 |
-
.env
|
| 7 |
-
output_locus.txt
|
| 8 |
-
*.log
|
| 9 |
-
.DS_Store
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -1,31 +1,22 @@
|
|
| 1 |
-
|
| 2 |
-
FROM python:3.11-slim
|
| 3 |
|
| 4 |
-
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
fonts-dejavu-core \
|
| 11 |
-
fontconfig \
|
| 12 |
-
&& rm -rf /var/lib/apt/lists/* \
|
| 13 |
-
&& fc-cache -f -v
|
| 14 |
|
| 15 |
-
|
| 16 |
-
COPY requirements.txt .
|
| 17 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
|
| 22 |
-
#
|
| 23 |
-
RUN
|
| 24 |
-
USER user
|
| 25 |
-
ENV PATH="/home/user/.local/bin:$PATH"
|
| 26 |
|
| 27 |
-
|
| 28 |
-
EXPOSE 7860
|
| 29 |
|
| 30 |
-
#
|
| 31 |
-
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
|
|
|
| 2 |
|
| 3 |
+
RUN apt-get update && apt-get install -y wget build-essential && rm -rf /var/lib/apt/lists/*
|
| 4 |
|
| 5 |
+
RUN useradd -m -u 1000 user
|
| 6 |
+
USER user
|
| 7 |
+
ENV HOME=/home/user \
|
| 8 |
+
PATH=/home/user/.local/bin:$PATH
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
WORKDIR $HOME/app
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
COPY --chown=user requirements.txt .
|
| 13 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 14 |
|
| 15 |
+
# Download DictaLM 2.0 Q4_K_M GGUF
|
| 16 |
+
RUN wget -q -O model.gguf "https://huggingface.co/dicta-il/dictalm2.0-instruct-GGUF/resolve/main/dictalm2.0-instruct.Q4_K_M.gguf"
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
COPY --chown=user . .
|
|
|
|
| 19 |
|
| 20 |
+
# Run llama-cpp-python server
|
| 21 |
+
# Port 7860 is required for HF Spaces
|
| 22 |
+
CMD ["python", "-m", "llama_cpp.server", "--model", "model.gguf", "--host", "0.0.0.0", "--port", "7860", "--n_ctx", "4096", "--chat_format", "llama-2"]
|
README.md
CHANGED
|
@@ -1,37 +1,9 @@
|
|
| 1 |
---
|
| 2 |
-
title: BuddyMath
|
| 3 |
emoji: ๐งฎ
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
---
|
| 9 |
-
|
| 10 |
-
# BuddyMath Server V3.1
|
| 11 |
-
|
| 12 |
-
ืืืจื ืคืจืื AI ืืืชืืืืงื ืืชืืืืืื ืืฉืจืืืืื ๐ฎ๐ฑ
|
| 13 |
-
|
| 14 |
-
## API Endpoints
|
| 15 |
-
|
| 16 |
-
| Endpoint | Method | Description |
|
| 17 |
-
|----------|--------|-------------|
|
| 18 |
-
| `/` | GET | Health check |
|
| 19 |
-
| `/health` | GET | Server status |
|
| 20 |
-
| `/solve_stream` | POST | ืคืชืจืื ืืขืื (SSE) |
|
| 21 |
-
|
| 22 |
-
## Modes
|
| 23 |
-
|
| 24 |
-
- `solve` - ืคืชืืจ ืื
|
| 25 |
-
- `teach_me` - ืืื ืืืชื
|
| 26 |
-
- `check` - ืืืืง ืืืชื
|
| 27 |
-
|
| 28 |
-
## Parameters
|
| 29 |
-
|
| 30 |
-
```
|
| 31 |
-
image: File (required)
|
| 32 |
-
grade: string (required)
|
| 33 |
-
mode: string (default: "solve")
|
| 34 |
-
student_name: string (default: "ืชืืืื/ื")
|
| 35 |
-
student_gender: string (default: "male")
|
| 36 |
-
user_note: string (optional)
|
| 37 |
-
```
|
|
|
|
| 1 |
---
|
| 2 |
+
title: BuddyMath Dicta API
|
| 3 |
emoji: ๐งฎ
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
---
|
| 9 |
+
API Server for DictaLM 2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
all_users.json
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"test_user_123": {
|
| 3 |
-
"last_seen": "2026-02-15T12:49:04.304849",
|
| 4 |
-
"id": "test_user_123"
|
| 5 |
-
},
|
| 6 |
-
"\u05d3\u05d5\u05ea\u05df": {
|
| 7 |
-
"last_seen": "2026-03-08T15:18:08.252094",
|
| 8 |
-
"id": "\u05d3\u05d5\u05ea\u05df"
|
| 9 |
-
},
|
| 10 |
-
"stress_user_1": {
|
| 11 |
-
"last_seen": "2026-02-27T12:03:10.171108",
|
| 12 |
-
"id": "stress_user_1"
|
| 13 |
-
},
|
| 14 |
-
"stress_user_2": {
|
| 15 |
-
"last_seen": "2026-02-27T12:03:10.221285",
|
| 16 |
-
"id": "stress_user_2"
|
| 17 |
-
},
|
| 18 |
-
"stress_user_3": {
|
| 19 |
-
"last_seen": "2026-02-27T12:03:10.263698",
|
| 20 |
-
"id": "stress_user_3"
|
| 21 |
-
},
|
| 22 |
-
"stress_user_4": {
|
| 23 |
-
"last_seen": "2026-02-27T12:03:10.332969",
|
| 24 |
-
"id": "stress_user_4"
|
| 25 |
-
},
|
| 26 |
-
"stress_user_0": {
|
| 27 |
-
"last_seen": "2026-02-27T12:03:10.382946",
|
| 28 |
-
"id": "stress_user_0"
|
| 29 |
-
},
|
| 30 |
-
"diagnostic": {
|
| 31 |
-
"last_seen": "2026-02-28T11:19:56.770735",
|
| 32 |
-
"id": "diagnostic"
|
| 33 |
-
}
|
| 34 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
analytics.py
DELETED
|
@@ -1,77 +0,0 @@
|
|
| 1 |
-
import datetime
|
| 2 |
-
import logging
|
| 3 |
-
from firebase_admin import firestore
|
| 4 |
-
from firebase_manager import firebase_manager
|
| 5 |
-
|
| 6 |
-
logger = logging.getLogger("HamoraServer")
|
| 7 |
-
|
| 8 |
-
class AnalyticsManager:
|
| 9 |
-
"""
|
| 10 |
-
Manages AI Assessment Telemetry and Weekly Reports.
|
| 11 |
-
"""
|
| 12 |
-
def _get_db(self):
|
| 13 |
-
return firebase_manager.get_db()
|
| 14 |
-
|
| 15 |
-
def update_weekly_analytics(self, uid: str, assessment: dict):
|
| 16 |
-
"""
|
| 17 |
-
Updates the weekly analytics document for a user using atomic operations.
|
| 18 |
-
Runs silently.
|
| 19 |
-
"""
|
| 20 |
-
if not uid or not assessment:
|
| 21 |
-
return
|
| 22 |
-
|
| 23 |
-
db = self._get_db()
|
| 24 |
-
if not db:
|
| 25 |
-
logger.error("โ [ANALYTICS] Firestore DB not available.")
|
| 26 |
-
return
|
| 27 |
-
|
| 28 |
-
try:
|
| 29 |
-
# 1. Parse assessment data
|
| 30 |
-
primary_skill = assessment.get("primary_skill", "Unknown")
|
| 31 |
-
sub_skill = assessment.get("sub_skill", "")
|
| 32 |
-
mastery_score = assessment.get("mastery_score", 0)
|
| 33 |
-
parent_note = assessment.get("parent_note", "")
|
| 34 |
-
|
| 35 |
-
# Ensure score is an integer
|
| 36 |
-
try:
|
| 37 |
-
mastery_score = int(mastery_score)
|
| 38 |
-
except (ValueError, TypeError):
|
| 39 |
-
mastery_score = 0
|
| 40 |
-
|
| 41 |
-
# 2. Compute week_id (e.g., week_11_2026)
|
| 42 |
-
today = datetime.date.today()
|
| 43 |
-
year, week_num, _ = today.isocalendar()
|
| 44 |
-
week_id = f"week_{week_num:02d}_{year}"
|
| 45 |
-
|
| 46 |
-
# 3. Construct the document reference
|
| 47 |
-
# Path: users/{uid}/analytics/{week_id}
|
| 48 |
-
doc_ref = db.collection("users").document(uid).collection("analytics").document(week_id)
|
| 49 |
-
|
| 50 |
-
# 4. Construct payload with FieldValue operations for atomic updates
|
| 51 |
-
# Create the nested skill payload
|
| 52 |
-
skill_payload = {
|
| 53 |
-
"sum_scores": firestore.Increment(mastery_score),
|
| 54 |
-
"count": firestore.Increment(1)
|
| 55 |
-
}
|
| 56 |
-
if sub_skill:
|
| 57 |
-
skill_payload["sub_skills"] = firestore.ArrayUnion([sub_skill])
|
| 58 |
-
|
| 59 |
-
# Combine into final payload
|
| 60 |
-
payload = {
|
| 61 |
-
"total_exercises": firestore.Increment(1),
|
| 62 |
-
f"skills_data.{primary_skill}": skill_payload,
|
| 63 |
-
"last_updated": firestore.SERVER_TIMESTAMP
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
if parent_note:
|
| 67 |
-
payload["parent_notes"] = firestore.ArrayUnion([parent_note])
|
| 68 |
-
|
| 69 |
-
# 5. Set with merge=True
|
| 70 |
-
doc_ref.set(payload, merge=True)
|
| 71 |
-
logger.info(f"๐ [ANALYTICS] Successfully updated telemetry for {uid} in {week_id}.")
|
| 72 |
-
|
| 73 |
-
except Exception as e:
|
| 74 |
-
logger.error(f"โ [ANALYTICS] Error updating analytics for {uid}: {e}")
|
| 75 |
-
|
| 76 |
-
# Global instance
|
| 77 |
-
analytics_manager = AnalyticsManager()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
audio_generator.py
DELETED
|
@@ -1,267 +0,0 @@
|
|
| 1 |
-
# audio_generator.py - V273.0 (Google Cloud TTS - High Quality Hebrew)
|
| 2 |
-
import asyncio
|
| 3 |
-
import base64
|
| 4 |
-
import os
|
| 5 |
-
import tempfile
|
| 6 |
-
import logging
|
| 7 |
-
|
| 8 |
-
# Configure Logging
|
| 9 |
-
logger = logging.getLogger(__name__)
|
| 10 |
-
|
| 11 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 12 |
-
# ๐๏ธ Google Cloud TTS Configuration
|
| 13 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 14 |
-
#
|
| 15 |
-
# ืงืืืืช ืขืืจืืื ืืืื ืื:
|
| 16 |
-
# - he-IL-Wavenet-A (ื ืงืื, ืืืืืช ืืืืื) โญ ืืืืืฅ
|
| 17 |
-
# - he-IL-Wavenet-B (ืืืจ, ืืืืืช ืืืืื)
|
| 18 |
-
# - he-IL-Standard-A (ื ืงืื, ืืืืืช ืจืืืื)
|
| 19 |
-
# - he-IL-Standard-B (ืืืจ, ืืืืืช ืจืืืื)
|
| 20 |
-
#
|
| 21 |
-
# Free Tier: 1 ืืืืืื ืชืืืื/ืืืืฉ (WaveNet: 1M, Standard: 4M)
|
| 22 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 23 |
-
|
| 24 |
-
GOOGLE_VOICE_NAME = "he-IL-Wavenet-A" # Female, high quality
|
| 25 |
-
GOOGLE_LANGUAGE_CODE = "he-IL"
|
| 26 |
-
SPEAKING_RATE = 0.95 # ืืขื ืืืชืจ ืืืื ืืืืืจืืช
|
| 27 |
-
PITCH = 1.0 # ืืืื ืงืื ืจืืื
|
| 28 |
-
|
| 29 |
-
# Fallback to edge-tts if Google Cloud not configured
|
| 30 |
-
USE_EDGE_TTS_FALLBACK = True
|
| 31 |
-
EDGE_TTS_VOICE = "he-IL-HilaNeural"
|
| 32 |
-
|
| 33 |
-
from firebase_manager import firebase_manager # V261.17
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def _is_google_cloud_configured() -> bool:
|
| 37 |
-
"""ืืืืงื ืื Google Cloud ืืืืืจ"""
|
| 38 |
-
# Option 1: Environment variable
|
| 39 |
-
if os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"):
|
| 40 |
-
return True
|
| 41 |
-
# Option 2: Check for credentials file in common locations
|
| 42 |
-
common_paths = [
|
| 43 |
-
"/app/google-credentials.json",
|
| 44 |
-
"./google-credentials.json",
|
| 45 |
-
os.path.expanduser("~/.config/gcloud/application_default_credentials.json")
|
| 46 |
-
]
|
| 47 |
-
for path in common_paths:
|
| 48 |
-
if os.path.exists(path):
|
| 49 |
-
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = path
|
| 50 |
-
return True
|
| 51 |
-
return False
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
async def _generate_with_google_cloud(text: str, output_path: str) -> bool:
|
| 55 |
-
"""
|
| 56 |
-
ืืฆืืจืช ืืืืื ืขื Google Cloud TTS
|
| 57 |
-
ืืืืืจ True ืื ืืฆืืื, False ืื ื ืืฉื
|
| 58 |
-
"""
|
| 59 |
-
try:
|
| 60 |
-
from google.cloud import texttospeech
|
| 61 |
-
|
| 62 |
-
# Create client
|
| 63 |
-
client = texttospeech.TextToSpeechClient()
|
| 64 |
-
|
| 65 |
-
# Build the voice request
|
| 66 |
-
voice = texttospeech.VoiceSelectionParams(
|
| 67 |
-
language_code=GOOGLE_LANGUAGE_CODE,
|
| 68 |
-
name=GOOGLE_VOICE_NAME,
|
| 69 |
-
)
|
| 70 |
-
|
| 71 |
-
# Select the audio format
|
| 72 |
-
audio_config = texttospeech.AudioConfig(
|
| 73 |
-
audio_encoding=texttospeech.AudioEncoding.MP3,
|
| 74 |
-
speaking_rate=SPEAKING_RATE,
|
| 75 |
-
pitch=PITCH,
|
| 76 |
-
)
|
| 77 |
-
|
| 78 |
-
# Build the synthesis input
|
| 79 |
-
synthesis_input = texttospeech.SynthesisInput(text=text)
|
| 80 |
-
|
| 81 |
-
# Perform the text-to-speech request
|
| 82 |
-
logger.info(f"๐๏ธ Google Cloud TTS: Generating audio for {len(text)} chars...")
|
| 83 |
-
|
| 84 |
-
# Run in thread pool to not block async
|
| 85 |
-
loop = asyncio.get_running_loop()
|
| 86 |
-
response = await loop.run_in_executor(
|
| 87 |
-
None,
|
| 88 |
-
lambda: client.synthesize_speech(
|
| 89 |
-
input=synthesis_input,
|
| 90 |
-
voice=voice,
|
| 91 |
-
audio_config=audio_config
|
| 92 |
-
)
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
# Write the audio content to file
|
| 96 |
-
with open(output_path, "wb") as out:
|
| 97 |
-
out.write(response.audio_content)
|
| 98 |
-
|
| 99 |
-
logger.info(f"โ
Google Cloud TTS: Audio saved to {output_path}")
|
| 100 |
-
return True
|
| 101 |
-
|
| 102 |
-
except ImportError:
|
| 103 |
-
logger.warning("โ ๏ธ google-cloud-texttospeech not installed. Run: pip install google-cloud-texttospeech")
|
| 104 |
-
return False
|
| 105 |
-
except Exception as e:
|
| 106 |
-
logger.error(f"โ Google Cloud TTS failed: {e}")
|
| 107 |
-
return False
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
async def _generate_with_edge_tts(text: str, output_path: str) -> bool:
|
| 111 |
-
"""
|
| 112 |
-
ืืฆืืจืช ืืืืื ืขื edge-tts (Fallback)
|
| 113 |
-
"""
|
| 114 |
-
try:
|
| 115 |
-
import edge_tts
|
| 116 |
-
|
| 117 |
-
logger.info(f"๐๏ธ Edge TTS (Fallback): Generating audio...")
|
| 118 |
-
communicate = edge_tts.Communicate(text, EDGE_TTS_VOICE)
|
| 119 |
-
await communicate.save(output_path)
|
| 120 |
-
|
| 121 |
-
logger.info(f"โ
Edge TTS: Audio saved to {output_path}")
|
| 122 |
-
return True
|
| 123 |
-
|
| 124 |
-
except Exception as e:
|
| 125 |
-
logger.error(f"โ Edge TTS failed: {e}")
|
| 126 |
-
return False
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
async def generate_teacher_audio(text: str, output_path: str = None) -> str:
|
| 130 |
-
"""
|
| 131 |
-
V273.0: ืืฆืืจืช ืืืืื ืขื Google Cloud TTS (ืืืืืช ืืืืื)
|
| 132 |
-
|
| 133 |
-
ืื ืกื ืงืืื Google Cloud TTS, ืื ืื ืืืืืจ/ื ืืฉื โ edge-tts fallback
|
| 134 |
-
|
| 135 |
-
Returns:
|
| 136 |
-
- Public URL (if Firebase upload success)
|
| 137 |
-
- Base64 string (fallback)
|
| 138 |
-
- None (if all failed)
|
| 139 |
-
"""
|
| 140 |
-
try:
|
| 141 |
-
if not text:
|
| 142 |
-
return None
|
| 143 |
-
|
| 144 |
-
# Clean text for TTS (remove emojis and special chars that cause issues)
|
| 145 |
-
clean_text = _clean_text_for_tts(text)
|
| 146 |
-
|
| 147 |
-
if not clean_text:
|
| 148 |
-
return None
|
| 149 |
-
|
| 150 |
-
logger.info(f"๐๏ธ TTS Request: {clean_text[:50]}...")
|
| 151 |
-
|
| 152 |
-
# Determine output path
|
| 153 |
-
if output_path:
|
| 154 |
-
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 155 |
-
final_path = output_path
|
| 156 |
-
else:
|
| 157 |
-
timestamp = int(asyncio.get_event_loop().time() * 1000)
|
| 158 |
-
final_path = os.path.join(tempfile.gettempdir(), f"audio_{timestamp}.mp3")
|
| 159 |
-
|
| 160 |
-
# Try Google Cloud TTS first
|
| 161 |
-
success = False
|
| 162 |
-
if _is_google_cloud_configured():
|
| 163 |
-
success = await _generate_with_google_cloud(clean_text, final_path)
|
| 164 |
-
else:
|
| 165 |
-
logger.info("โน๏ธ Google Cloud not configured, using Edge TTS")
|
| 166 |
-
|
| 167 |
-
# Fallback to edge-tts
|
| 168 |
-
if not success and USE_EDGE_TTS_FALLBACK:
|
| 169 |
-
success = await _generate_with_edge_tts(clean_text, final_path)
|
| 170 |
-
|
| 171 |
-
if not success:
|
| 172 |
-
logger.error("โ All TTS methods failed")
|
| 173 |
-
return None
|
| 174 |
-
|
| 175 |
-
# Try Firebase Upload
|
| 176 |
-
try:
|
| 177 |
-
blob_name = f"audio/{os.path.basename(final_path)}"
|
| 178 |
-
loop = asyncio.get_running_loop()
|
| 179 |
-
|
| 180 |
-
public_url = await loop.run_in_executor(
|
| 181 |
-
None,
|
| 182 |
-
lambda: firebase_manager.upload_file(final_path, blob_name)
|
| 183 |
-
)
|
| 184 |
-
|
| 185 |
-
if public_url:
|
| 186 |
-
logger.info(f"โ๏ธ Firebase URL: {public_url}")
|
| 187 |
-
# Clean up local file
|
| 188 |
-
if not output_path:
|
| 189 |
-
os.remove(final_path)
|
| 190 |
-
return public_url
|
| 191 |
-
except Exception as fb_err:
|
| 192 |
-
logger.warning(f"โ ๏ธ Firebase upload failed ({fb_err}). Using Base64.")
|
| 193 |
-
|
| 194 |
-
# Fallback: Return Base64
|
| 195 |
-
with open(final_path, "rb") as audio_file:
|
| 196 |
-
audio_bytes = audio_file.read()
|
| 197 |
-
audio_base64 = base64.b64encode(audio_bytes).decode('utf-8')
|
| 198 |
-
|
| 199 |
-
# Clean up temp file
|
| 200 |
-
if not output_path:
|
| 201 |
-
os.remove(final_path)
|
| 202 |
-
|
| 203 |
-
return audio_base64
|
| 204 |
-
|
| 205 |
-
except Exception as e:
|
| 206 |
-
logger.error(f"โ TTS Generation Failed: {e}")
|
| 207 |
-
return None
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
def _clean_text_for_tts(text: str) -> str:
|
| 211 |
-
"""
|
| 212 |
-
ื ืืงืื ืืงืกื ืืคื ื TTS - ืืกืจืช ืืืืื'ืื ืืกืืื ืื ืืขืืืชืืื
|
| 213 |
-
"""
|
| 214 |
-
import re
|
| 215 |
-
|
| 216 |
-
if not text:
|
| 217 |
-
return ""
|
| 218 |
-
|
| 219 |
-
# Remove emojis
|
| 220 |
-
emoji_pattern = re.compile("["
|
| 221 |
-
u"\U0001F600-\U0001F64F" # emoticons
|
| 222 |
-
u"\U0001F300-\U0001F5FF" # symbols & pictographs
|
| 223 |
-
u"\U0001F680-\U0001F6FF" # transport & map symbols
|
| 224 |
-
u"\U0001F1E0-\U0001F1FF" # flags
|
| 225 |
-
u"\U00002702-\U000027B0"
|
| 226 |
-
u"\U000024C2-\U0001F251"
|
| 227 |
-
"]+", flags=re.UNICODE)
|
| 228 |
-
|
| 229 |
-
clean = emoji_pattern.sub('', text)
|
| 230 |
-
|
| 231 |
-
# Remove multiple spaces
|
| 232 |
-
clean = re.sub(r'\s+', ' ', clean)
|
| 233 |
-
|
| 234 |
-
# Remove LaTeX remnants that might have slipped through
|
| 235 |
-
clean = clean.replace('$', '').replace('\\', '')
|
| 236 |
-
|
| 237 |
-
return clean.strip()
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 241 |
-
# ๐งช Testing
|
| 242 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 243 |
-
|
| 244 |
-
if __name__ == "__main__":
|
| 245 |
-
async def main():
|
| 246 |
-
text = """
|
| 247 |
-
ืืืื ืืืคื ืฉื ืชืจืืื! ืืืื ื ืฆืจืืืื ืืืฆืื ืืช ื ืงืืืืช ืืงืืฆืื ืฉื ืืคืื ืงืฆืื.
|
| 248 |
-
ืืฉืชืืฉื ื ืื ืืืจืช ืจืืฉืื ื ืืื ืืืฆืื ืืืคื ืืฉืืคืืข ืืชืืคืก.
|
| 249 |
-
ืืืจืืง ืืืืืจ - ื ืืืจืช ืืคืก ืชืืื ืืกืื ืช ื ืงืืืช ืงืืฆืื ืืคืฉืจืืช.
|
| 250 |
-
ืื ืืืืื ืขื ืืืชืืื!
|
| 251 |
-
"""
|
| 252 |
-
|
| 253 |
-
print(f"๐๏ธ Testing TTS...")
|
| 254 |
-
print(f"๐ Text length: {len(text)} chars")
|
| 255 |
-
print(f"โ๏ธ Google Cloud configured: {_is_google_cloud_configured()}")
|
| 256 |
-
|
| 257 |
-
result = await generate_teacher_audio(text)
|
| 258 |
-
|
| 259 |
-
if result:
|
| 260 |
-
if result.startswith("http"):
|
| 261 |
-
print(f"โ
Got URL: {result}")
|
| 262 |
-
else:
|
| 263 |
-
print(f"โ
Got Base64: {len(result)} chars")
|
| 264 |
-
else:
|
| 265 |
-
print("โ TTS failed")
|
| 266 |
-
|
| 267 |
-
asyncio.run(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/__init__.py
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
# backend/__init__.py
|
| 2 |
-
# BuddyMath V2.0 backend package
|
|
|
|
|
|
|
|
|
backend/extractor.py
DELETED
|
@@ -1,127 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,98 +0,0 @@
|
|
| 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, ConfigDict, 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 |
-
model_config = ConfigDict(
|
| 26 |
-
# Allow None values in `given` dict (explicit null support)
|
| 27 |
-
arbitrary_types_allowed=True,
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
# โโ What type of problem โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 31 |
-
problem_type: Literal[
|
| 32 |
-
"geometry",
|
| 33 |
-
"algebra",
|
| 34 |
-
"probability",
|
| 35 |
-
"statistics",
|
| 36 |
-
"trigonometry",
|
| 37 |
-
"calculus",
|
| 38 |
-
]
|
| 39 |
-
sub_type: str # e.g. "circle", "quadratic", "normal_distribution"
|
| 40 |
-
|
| 41 |
-
# โโ What is given (Extractor fills this) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 42 |
-
# null value = "was expected but OCR/text was unclear" (not the same as absent key)
|
| 43 |
-
given: Dict[str, Any] = Field(
|
| 44 |
-
default_factory=dict,
|
| 45 |
-
description=(
|
| 46 |
-
"Key=label, Value=extracted value or null. "
|
| 47 |
-
"null means unclear, NOT missing from the problem."
|
| 48 |
-
),
|
| 49 |
-
examples=[
|
| 50 |
-
{"A": [0, 1], "D": [0, 9], "radius": None},
|
| 51 |
-
{"equation": "x^2 - 5x + 6 = 0", "domain": None},
|
| 52 |
-
],
|
| 53 |
-
)
|
| 54 |
-
|
| 55 |
-
# โโ What the question asks to find โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 56 |
-
find: List[str] = Field(
|
| 57 |
-
default_factory=list,
|
| 58 |
-
description="What the question explicitly asks to calculate or prove.",
|
| 59 |
-
examples=[["center of circle", "radius"], ["roots of equation"]],
|
| 60 |
-
)
|
| 61 |
-
|
| 62 |
-
# โโ Verbal constraints (from text / diagram labels) โโโโโโโโโโโโโโโโโ
|
| 63 |
-
constraints: List[str] = Field(
|
| 64 |
-
default_factory=list,
|
| 65 |
-
examples=[["AD is a diameter", "M is the center of the circle"]],
|
| 66 |
-
)
|
| 67 |
-
|
| 68 |
-
# โโ Populated ONLY by the Verifier โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 69 |
-
verified: Dict[str, Any] = Field(
|
| 70 |
-
default_factory=dict,
|
| 71 |
-
description=(
|
| 72 |
-
"Algebraically proven values. "
|
| 73 |
-
"These are Ground Truth โ the LLM must not re-derive them."
|
| 74 |
-
),
|
| 75 |
-
)
|
| 76 |
-
|
| 77 |
-
# โโ Pipeline status โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 78 |
-
verification_status: Literal[
|
| 79 |
-
"verified", # all `find` targets are algebraically confirmed
|
| 80 |
-
"partial", # some targets verified, others not
|
| 81 |
-
"contradiction", # algebraic result contradicts extracted data
|
| 82 |
-
"insufficient_data", # too many null fields to verify
|
| 83 |
-
"pending", # default before Verifier runs
|
| 84 |
-
] = "pending"
|
| 85 |
-
|
| 86 |
-
warnings: List[str] = Field(
|
| 87 |
-
default_factory=list,
|
| 88 |
-
description="Non-fatal issues: OCR gaps, null substitutions, etc.",
|
| 89 |
-
)
|
| 90 |
-
|
| 91 |
-
def null_fields(self) -> List[str]:
|
| 92 |
-
"""Returns list of keys in `given` that are explicitly null."""
|
| 93 |
-
return [k for k, v in self.given.items() if v is None]
|
| 94 |
-
|
| 95 |
-
def is_ready_to_verify(self) -> bool:
|
| 96 |
-
"""True if no critical null fields block verification."""
|
| 97 |
-
# Verifier can still run on partial data; it will set status accordingly
|
| 98 |
-
return True # Always try; Verifier decides the status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/pipeline.py
DELETED
|
@@ -1,102 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,397 +0,0 @@
|
|
| 1 |
-
# backend/tests/test_pipeline.py
|
| 2 |
-
# BuddyMath V2.0 โ Acceptance Tests (v2 โ Post Priority Fix)
|
| 3 |
-
#
|
| 4 |
-
# CRITICAL RULE (from review):
|
| 5 |
-
# M=(0,5) via midpoint of chord AD is the HALLUCINATION we are preventing.
|
| 6 |
-
# M=(3,5) via intersection of diameter lines is the ONLY correct answer.
|
| 7 |
-
#
|
| 8 |
-
# Run with: python -m pytest backend/tests/test_pipeline.py -v
|
| 9 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 10 |
-
|
| 11 |
-
from __future__ import annotations
|
| 12 |
-
|
| 13 |
-
import logging
|
| 14 |
-
import sys
|
| 15 |
-
import os
|
| 16 |
-
import unittest
|
| 17 |
-
|
| 18 |
-
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
| 19 |
-
|
| 20 |
-
from backend.math_schema import MathProblemSchema
|
| 21 |
-
from backend.universal_verifier import UniversalVerifier
|
| 22 |
-
from backend.variable_pinner import VariablePinner
|
| 23 |
-
|
| 24 |
-
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
| 25 |
-
|
| 26 |
-
verifier = UniversalVerifier()
|
| 27 |
-
pinner = VariablePinner()
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
# ============================================================
|
| 31 |
-
# Task 2/3: Verifier robustness โ always returns valid schema
|
| 32 |
-
# ============================================================
|
| 33 |
-
class TestVerifierRobustness(unittest.TestCase):
|
| 34 |
-
|
| 35 |
-
def test_circle_insufficient_data(self):
|
| 36 |
-
"""All-null geometry โ insufficient_data, never crashes."""
|
| 37 |
-
schema = MathProblemSchema(
|
| 38 |
-
problem_type="geometry",
|
| 39 |
-
sub_type="circle",
|
| 40 |
-
given={"A": None, "D": None, "radius": None},
|
| 41 |
-
find=["center of circle"],
|
| 42 |
-
constraints=["AD is a diameter"],
|
| 43 |
-
)
|
| 44 |
-
result = verifier.verify(schema)
|
| 45 |
-
self.assertIn(result.verification_status,
|
| 46 |
-
["insufficient_data", "partial", "contradiction"])
|
| 47 |
-
self.assertIsInstance(result.warnings, list)
|
| 48 |
-
|
| 49 |
-
def test_circle_chord_midpoint_fallback(self):
|
| 50 |
-
"""
|
| 51 |
-
FALLBACK ONLY test: A(0,1), D(0,9) named as diameter chord (no lines given).
|
| 52 |
-
Result must be M=(0.0, 5.0).
|
| 53 |
-
NOTE: This is the CORRECT result for this specific input.
|
| 54 |
-
It is WRONG to use midpoint when diameter lines are also available.
|
| 55 |
-
"""
|
| 56 |
-
schema = MathProblemSchema(
|
| 57 |
-
problem_type="geometry",
|
| 58 |
-
sub_type="circle",
|
| 59 |
-
given={"A": [0, 1], "D": [0, 9]},
|
| 60 |
-
find=["center of circle", "radius"],
|
| 61 |
-
constraints=["AD is a diameter"],
|
| 62 |
-
)
|
| 63 |
-
result = verifier.verify(schema)
|
| 64 |
-
# Chord midpoint fallback is always partial โ center is unconfirmed without line eqs
|
| 65 |
-
self.assertEqual(result.verification_status, "partial")
|
| 66 |
-
center = result.verified.get("center")
|
| 67 |
-
self.assertIsNotNone(center, "Center must be in verified")
|
| 68 |
-
self.assertAlmostEqual(center[0], 0.0, places=4)
|
| 69 |
-
self.assertAlmostEqual(center[1], 5.0, places=4)
|
| 70 |
-
self.assertAlmostEqual(result.verified.get("radius"), 4.0, places=4)
|
| 71 |
-
self.assertEqual(result.verified.get("verification_method"), "chord_midpoint_fallback")
|
| 72 |
-
|
| 73 |
-
def test_algebra_insufficient_data(self):
|
| 74 |
-
schema = MathProblemSchema(
|
| 75 |
-
problem_type="algebra",
|
| 76 |
-
sub_type="quadratic",
|
| 77 |
-
given={"equation": None},
|
| 78 |
-
find=["roots"],
|
| 79 |
-
constraints=[],
|
| 80 |
-
)
|
| 81 |
-
result = verifier.verify(schema)
|
| 82 |
-
self.assertEqual(result.verification_status, "insufficient_data")
|
| 83 |
-
|
| 84 |
-
def test_algebra_quadratic(self):
|
| 85 |
-
schema = MathProblemSchema(
|
| 86 |
-
problem_type="algebra",
|
| 87 |
-
sub_type="quadratic",
|
| 88 |
-
given={"equation": "x**2 - 5*x + 6"},
|
| 89 |
-
find=["roots"],
|
| 90 |
-
constraints=[],
|
| 91 |
-
)
|
| 92 |
-
result = verifier.verify(schema)
|
| 93 |
-
self.assertEqual(result.verification_status, "verified")
|
| 94 |
-
solutions = result.verified.get("solutions", [])
|
| 95 |
-
self.assertIn("2", solutions)
|
| 96 |
-
self.assertIn("3", solutions)
|
| 97 |
-
|
| 98 |
-
def test_probability_sum_to_one(self):
|
| 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 |
-
schema = MathProblemSchema(
|
| 111 |
-
problem_type="probability",
|
| 112 |
-
sub_type="basic",
|
| 113 |
-
given={"probabilities": [0.5, 0.6]},
|
| 114 |
-
find=["verify distribution"],
|
| 115 |
-
constraints=[],
|
| 116 |
-
)
|
| 117 |
-
result = verifier.verify(schema)
|
| 118 |
-
self.assertEqual(result.verification_status, "contradiction")
|
| 119 |
-
|
| 120 |
-
def test_statistics_verified(self):
|
| 121 |
-
schema = MathProblemSchema(
|
| 122 |
-
problem_type="statistics",
|
| 123 |
-
sub_type="descriptive",
|
| 124 |
-
given={"data": [2, 4, 6]},
|
| 125 |
-
find=["mean"],
|
| 126 |
-
constraints=[],
|
| 127 |
-
)
|
| 128 |
-
result = verifier.verify(schema)
|
| 129 |
-
self.assertAlmostEqual(result.verified.get("mean"), 4.0, places=4)
|
| 130 |
-
|
| 131 |
-
def test_calculus_derivative(self):
|
| 132 |
-
schema = MathProblemSchema(
|
| 133 |
-
problem_type="calculus",
|
| 134 |
-
sub_type="differentiation",
|
| 135 |
-
given={"function": "x**2", "point": 3},
|
| 136 |
-
find=["derivative at x=3"],
|
| 137 |
-
constraints=[],
|
| 138 |
-
)
|
| 139 |
-
result = verifier.verify(schema)
|
| 140 |
-
self.assertAlmostEqual(result.verified.get("derivative_at_point"), 6.0, places=4)
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
# ============================================================
|
| 144 |
-
# THE KEY TEST โ Task 4 "M=3,5" Acceptance Criterion
|
| 145 |
-
# ============================================================
|
| 146 |
-
class TestDiameterLineIntersection(unittest.TestCase):
|
| 147 |
-
"""
|
| 148 |
-
This is THE acceptance test.
|
| 149 |
-
|
| 150 |
-
Problem: A(0,1), D(0,9)
|
| 151 |
-
Two diameters: y = 4/3*x + 1 and y = -4/3*x + 9
|
| 152 |
-
Algebraic intersection: x=3, y=5 โ M=(3,5)
|
| 153 |
-
|
| 154 |
-
โ WRONG (hallucination): M=(0,5) โ midpoint of chord AD
|
| 155 |
-
โ
CORRECT (algebraic): M=(3,5) โ intersection of diameter lines
|
| 156 |
-
"""
|
| 157 |
-
|
| 158 |
-
def _make_schema(self) -> MathProblemSchema:
|
| 159 |
-
return MathProblemSchema(
|
| 160 |
-
problem_type="geometry",
|
| 161 |
-
sub_type="circle",
|
| 162 |
-
given={
|
| 163 |
-
"A": [0, 1],
|
| 164 |
-
"D": [0, 9],
|
| 165 |
-
"diameter_lines": ["y=4/3*x+1", "y=-4/3*x+9"],
|
| 166 |
-
},
|
| 167 |
-
find=["center of circle M"],
|
| 168 |
-
constraints=["AC and DB are diameters", "M is the center"],
|
| 169 |
-
)
|
| 170 |
-
|
| 171 |
-
def test_center_is_3_5_not_0_5(self):
|
| 172 |
-
"""
|
| 173 |
-
ACCEPTANCE TEST: M must be (3,5) โ intersection of diameters.
|
| 174 |
-
M=(0,5) means the verifier used midpoint of chord AD. This is the hallucination.
|
| 175 |
-
"""
|
| 176 |
-
schema = self._make_schema()
|
| 177 |
-
result = verifier.verify(schema)
|
| 178 |
-
|
| 179 |
-
M = result.verified.get("M")
|
| 180 |
-
self.assertIsNotNone(M, "M must be in verified dict")
|
| 181 |
-
self.assertAlmostEqual(M[0], 3.0, places=4,
|
| 182 |
-
msg=f"M.x should be 3.0 (got {M[0]}) โ midpoint fallback not allowed here")
|
| 183 |
-
self.assertAlmostEqual(M[1], 5.0, places=4,
|
| 184 |
-
msg=f"M.y should be 5.0 (got {M[1]})")
|
| 185 |
-
|
| 186 |
-
# Explicitly assert the WRONG answer doesn't appear
|
| 187 |
-
self.assertFalse(
|
| 188 |
-
abs(M[0] - 0.0) < 0.01 and abs(M[1] - 5.0) < 0.01,
|
| 189 |
-
"CRITICAL FAILURE: M=(0,5) detected โ this is the chord midpoint hallucination!"
|
| 190 |
-
)
|
| 191 |
-
|
| 192 |
-
def test_center_method_is_line_intersection(self):
|
| 193 |
-
"""Verify the method field is set correctly."""
|
| 194 |
-
schema = self._make_schema()
|
| 195 |
-
result = verifier.verify(schema)
|
| 196 |
-
method = result.verified.get("center_method")
|
| 197 |
-
self.assertEqual(method, "diameter_line_intersection")
|
| 198 |
-
|
| 199 |
-
def test_verification_status_is_verified(self):
|
| 200 |
-
schema = self._make_schema()
|
| 201 |
-
result = verifier.verify(schema)
|
| 202 |
-
self.assertIn(result.verification_status, ["verified", "partial"])
|
| 203 |
-
|
| 204 |
-
def test_pin_block_contains_correct_center(self):
|
| 205 |
-
"""The PIN block must show M=(3.0, 5.0), not (0.0, 5.0)."""
|
| 206 |
-
schema = self._make_schema()
|
| 207 |
-
result = verifier.verify(schema)
|
| 208 |
-
block = pinner.build_pin_block(result)
|
| 209 |
-
|
| 210 |
-
self.assertIn("GROUND TRUTH", block)
|
| 211 |
-
self.assertIn("3.0", block, "Pin block must contain x=3.0 for center M")
|
| 212 |
-
self.assertIn("5.0", block, "Pin block must contain y=5.0 for center M")
|
| 213 |
-
|
| 214 |
-
def test_pin_block_aggressive_on_contradiction(self):
|
| 215 |
-
"""Pin block flags CONTRADICTION when center_method = chord_midpoint with lines available."""
|
| 216 |
-
schema = MathProblemSchema(
|
| 217 |
-
problem_type="geometry",
|
| 218 |
-
sub_type="circle",
|
| 219 |
-
given={
|
| 220 |
-
"A": [0, 1],
|
| 221 |
-
"D": [0, 9],
|
| 222 |
-
"diameter_lines": ["y=4/3*x+1", "y=-4/3*x+9"],
|
| 223 |
-
},
|
| 224 |
-
find=["center"],
|
| 225 |
-
constraints=["AC and DB are diameters"],
|
| 226 |
-
# Simulate the hallucination being injected from outside
|
| 227 |
-
verified={"M": (0.0, 5.0), "center_method": "diameter_chord_midpoint"},
|
| 228 |
-
verification_status="contradiction",
|
| 229 |
-
warnings=["CRITICAL CONTRADICTION: LLM assumed M(0,5) from chord, correct is M(3,5)"],
|
| 230 |
-
)
|
| 231 |
-
block = pinner.build_pin_block(schema)
|
| 232 |
-
self.assertIn("CONTRADICTION", block)
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
# ============================================================
|
| 236 |
-
# Task 4: Pin block structure
|
| 237 |
-
# ============================================================
|
| 238 |
-
class TestVariablePinner(unittest.TestCase):
|
| 239 |
-
|
| 240 |
-
def test_pin_block_contains_ground_truth_header(self):
|
| 241 |
-
schema = MathProblemSchema(
|
| 242 |
-
problem_type="geometry",
|
| 243 |
-
sub_type="circle",
|
| 244 |
-
given={"A": [0, 1], "D": [0, 9]},
|
| 245 |
-
find=["center"],
|
| 246 |
-
constraints=["AD is a diameter"],
|
| 247 |
-
verified={"center": (0.0, 5.0), "radius": 4.0},
|
| 248 |
-
verification_status="verified",
|
| 249 |
-
)
|
| 250 |
-
block = pinner.build_pin_block(schema)
|
| 251 |
-
self.assertIn("GROUND TRUTH", block)
|
| 252 |
-
self.assertIn("center", block)
|
| 253 |
-
self.assertIn("radius", block)
|
| 254 |
-
|
| 255 |
-
def test_pin_block_null_warning(self):
|
| 256 |
-
schema = MathProblemSchema(
|
| 257 |
-
problem_type="geometry",
|
| 258 |
-
sub_type="circle",
|
| 259 |
-
given={"radius": None},
|
| 260 |
-
find=["center"],
|
| 261 |
-
constraints=[],
|
| 262 |
-
verification_status="insufficient_data",
|
| 263 |
-
)
|
| 264 |
-
block = pinner.build_pin_block(schema)
|
| 265 |
-
self.assertIn("INSUFFICIENT_DATA", block)
|
| 266 |
-
self.assertIn("radius", block)
|
| 267 |
-
|
| 268 |
-
def test_pin_block_contradiction_warning(self):
|
| 269 |
-
schema = MathProblemSchema(
|
| 270 |
-
problem_type="algebra",
|
| 271 |
-
sub_type="quadratic",
|
| 272 |
-
given={"equation": "x**2 - 5*x + 6", "solution": 99},
|
| 273 |
-
find=["roots"],
|
| 274 |
-
constraints=[],
|
| 275 |
-
verification_status="contradiction",
|
| 276 |
-
)
|
| 277 |
-
block = pinner.build_pin_block(schema)
|
| 278 |
-
self.assertIn("CONTRADICTION", block)
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
if __name__ == "__main__":
|
| 282 |
-
unittest.main(verbosity=2)
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
# ============================================================
|
| 286 |
-
# Phase C โ 5-Domain Acceptance Suite
|
| 287 |
-
# Each test verifies: (1) correct result, (2) verification_method field
|
| 288 |
-
# ============================================================
|
| 289 |
-
class TestPhaseCDomainCoverage(unittest.TestCase):
|
| 290 |
-
"""
|
| 291 |
-
Phase C acceptance criterion:
|
| 292 |
-
Each domain must report WHAT was verified AND HOW (verification_method).
|
| 293 |
-
5 exam-style exercises, one per remaining domain.
|
| 294 |
-
"""
|
| 295 |
-
|
| 296 |
-
# ๐ด ALGEBRA: substitution into original equation
|
| 297 |
-
def test_algebra_cubic_roots_and_method(self):
|
| 298 |
-
"""x^3 - 6x^2 + 11x - 6 = 0 โ roots [1, 2, 3]"""
|
| 299 |
-
schema = MathProblemSchema(
|
| 300 |
-
problem_type="algebra",
|
| 301 |
-
sub_type="cubic",
|
| 302 |
-
given={"equation": "x**3 - 6*x**2 + 11*x - 6"},
|
| 303 |
-
find=["roots"],
|
| 304 |
-
constraints=[],
|
| 305 |
-
)
|
| 306 |
-
result = verifier.verify(schema)
|
| 307 |
-
self.assertEqual(result.verification_status, "verified")
|
| 308 |
-
solutions = result.verified.get("solutions", [])
|
| 309 |
-
self.assertIn("1", solutions)
|
| 310 |
-
self.assertIn("2", solutions)
|
| 311 |
-
self.assertIn("3", solutions)
|
| 312 |
-
self.assertEqual(result.verified.get("verification_method"), "sympy_solve_substitution")
|
| 313 |
-
print(f" [algebra] method={result.verified.get('verification_method')}")
|
| 314 |
-
|
| 315 |
-
# ๐ด GEOMETRY: diameter line intersection (already covered, here as Phase C entry)
|
| 316 |
-
def test_geometry_circle_diameter_intersection_and_method(self):
|
| 317 |
-
"""y=2x+1 โฉ y=-2x+9 โ center (2, 5)"""
|
| 318 |
-
schema = MathProblemSchema(
|
| 319 |
-
problem_type="geometry",
|
| 320 |
-
sub_type="circle",
|
| 321 |
-
given={"diameter_lines": ["y=2*x+1", "y=-2*x+9"]},
|
| 322 |
-
find=["center"],
|
| 323 |
-
constraints=["given lines are diameters"],
|
| 324 |
-
)
|
| 325 |
-
result = verifier.verify(schema)
|
| 326 |
-
self.assertIn(result.verification_status, ["verified", "partial"])
|
| 327 |
-
M = result.verified.get("M")
|
| 328 |
-
self.assertIsNotNone(M)
|
| 329 |
-
self.assertAlmostEqual(M[0], 2.0, places=3)
|
| 330 |
-
self.assertAlmostEqual(M[1], 5.0, places=3)
|
| 331 |
-
self.assertEqual(result.verified.get("verification_method"), "diameter_line_intersection")
|
| 332 |
-
print(f" [geometry] M={M} method={result.verified.get('verification_method')}")
|
| 333 |
-
|
| 334 |
-
# ๐ก PROBABILITY: sum-to-1 check
|
| 335 |
-
def test_probability_three_event_distribution_and_method(self):
|
| 336 |
-
"""P(A)=1/3, P(B)=1/2, P(C)=1/6 โ sum=1"""
|
| 337 |
-
schema = MathProblemSchema(
|
| 338 |
-
problem_type="probability",
|
| 339 |
-
sub_type="discrete_distribution",
|
| 340 |
-
given={"probabilities": ["1/3", "1/2", "1/6"]},
|
| 341 |
-
find=["verify distribution is valid"],
|
| 342 |
-
constraints=["A, B, C are mutually exclusive and exhaustive"],
|
| 343 |
-
)
|
| 344 |
-
result = verifier.verify(schema)
|
| 345 |
-
self.assertEqual(result.verification_status, "verified")
|
| 346 |
-
self.assertAlmostEqual(result.verified.get("sum"), 1.0, places=9)
|
| 347 |
-
self.assertEqual(result.verified.get("verification_method"), "sympy_rational_sum")
|
| 348 |
-
print(f" [probability] sum={result.verified.get('sum')} method={result.verified.get('verification_method')}")
|
| 349 |
-
|
| 350 |
-
# ๐ก STATISTICS: mean and std via NumPy
|
| 351 |
-
def test_statistics_exam_scores_and_method(self):
|
| 352 |
-
"""Scores [70, 80, 90, 100] โ mean=85, std=11.18"""
|
| 353 |
-
schema = MathProblemSchema(
|
| 354 |
-
problem_type="statistics",
|
| 355 |
-
sub_type="descriptive",
|
| 356 |
-
given={"data": [70, 80, 90, 100]},
|
| 357 |
-
find=["mean", "standard deviation"],
|
| 358 |
-
constraints=[],
|
| 359 |
-
)
|
| 360 |
-
result = verifier.verify(schema)
|
| 361 |
-
self.assertEqual(result.verification_status, "verified")
|
| 362 |
-
self.assertAlmostEqual(result.verified.get("mean"), 85.0, places=4)
|
| 363 |
-
self.assertAlmostEqual(result.verified.get("std"), 11.1803, places=3)
|
| 364 |
-
self.assertEqual(result.verified.get("verification_method"), "numpy_population_stats")
|
| 365 |
-
print(f" [statistics] mean={result.verified.get('mean')} method={result.verified.get('verification_method')}")
|
| 366 |
-
|
| 367 |
-
# ๐ข CALCULUS: derivative at a point
|
| 368 |
-
def test_calculus_polynomial_derivative_and_method(self):
|
| 369 |
-
"""f(x) = x^3 - 4x โ f'(2) = 3*4 - 4 = 8"""
|
| 370 |
-
schema = MathProblemSchema(
|
| 371 |
-
problem_type="calculus",
|
| 372 |
-
sub_type="differentiation",
|
| 373 |
-
given={"function": "x**3 - 4*x", "point": 2},
|
| 374 |
-
find=["derivative at x=2"],
|
| 375 |
-
constraints=[],
|
| 376 |
-
)
|
| 377 |
-
result = verifier.verify(schema)
|
| 378 |
-
self.assertEqual(result.verification_status, "verified")
|
| 379 |
-
self.assertAlmostEqual(result.verified.get("derivative_at_point"), 8.0, places=4)
|
| 380 |
-
self.assertEqual(result.verified.get("verification_method"), "sympy_differentiation")
|
| 381 |
-
print(f" [calculus] f'(2)={result.verified.get('derivative_at_point')} method={result.verified.get('verification_method')}")
|
| 382 |
-
|
| 383 |
-
# ๐ข TRIGONOMETRY: identity verification
|
| 384 |
-
def test_trigonometry_pythagorean_identity_and_method(self):
|
| 385 |
-
"""sin(x)**2 + cos(x)**2 = 1 โ must simplify to 0 symbolically"""
|
| 386 |
-
schema = MathProblemSchema(
|
| 387 |
-
problem_type="trigonometry",
|
| 388 |
-
sub_type="identity",
|
| 389 |
-
given={"identity": "sin(x)**2 + cos(x)**2 = 1"},
|
| 390 |
-
find=["verify Pythagorean identity"],
|
| 391 |
-
constraints=[],
|
| 392 |
-
)
|
| 393 |
-
result = verifier.verify(schema)
|
| 394 |
-
self.assertEqual(result.verification_status, "verified")
|
| 395 |
-
self.assertTrue(result.verified.get("identity_valid"))
|
| 396 |
-
self.assertEqual(result.verified.get("verification_method"), "sympy_symbolic_simplification")
|
| 397 |
-
print(f" [trig] identity_valid={result.verified.get('identity_valid')} method={result.verified.get('verification_method')}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/universal_verifier.py
DELETED
|
@@ -1,480 +0,0 @@
|
|
| 1 |
-
# backend/universal_verifier.py
|
| 2 |
-
# BuddyMath V2.0 โ Stage 2: SymPy-based Universal Verifier
|
| 3 |
-
#
|
| 4 |
-
# PRIORITY ORDER FOR CIRCLE CENTER (non-negotiable):
|
| 5 |
-
# 1. diameter_lines in given โ center = algebraic intersection (ALWAYS preferred)
|
| 6 |
-
# 2. Midpoint of named diameter chord โ FALLBACK ONLY (no lines available)
|
| 7 |
-
#
|
| 8 |
-
# A chord midpoint is NOT a center unless it is proven to be a diameter.
|
| 9 |
-
# If diameter line equations exist, they override ALL midpoint calculations.
|
| 10 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 11 |
-
|
| 12 |
-
from __future__ import annotations
|
| 13 |
-
|
| 14 |
-
import logging
|
| 15 |
-
from typing import Any, Dict, List, Optional, Tuple
|
| 16 |
-
|
| 17 |
-
import sympy as sp
|
| 18 |
-
|
| 19 |
-
from backend.math_schema import MathProblemSchema
|
| 20 |
-
|
| 21 |
-
logger = logging.getLogger(__name__)
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
# โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 25 |
-
def _to_point(value: Any) -> Optional[sp.Point2D]:
|
| 26 |
-
"""Convert [x, y] list โ sympy.Point2D, or None if invalid."""
|
| 27 |
-
if isinstance(value, (list, tuple)) and len(value) == 2:
|
| 28 |
-
try:
|
| 29 |
-
return sp.Point2D(sp.Rational(value[0]), sp.Rational(value[1]))
|
| 30 |
-
except Exception:
|
| 31 |
-
pass
|
| 32 |
-
return None
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def _to_rational(value: Any) -> Optional[sp.Rational]:
|
| 36 |
-
"""Convert numeric value โ sympy.Rational, or None."""
|
| 37 |
-
if value is None:
|
| 38 |
-
return None
|
| 39 |
-
try:
|
| 40 |
-
return sp.Rational(value)
|
| 41 |
-
except Exception:
|
| 42 |
-
return None
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
# โโ Domain dispatchers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 46 |
-
class UniversalVerifier:
|
| 47 |
-
"""
|
| 48 |
-
Routes a MathProblemSchema to the appropriate domain verifier.
|
| 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. type=%s/%s | find=%s",
|
| 65 |
-
schema.problem_type, schema.sub_type, schema.find,
|
| 66 |
-
)
|
| 67 |
-
null_keys = schema.null_fields()
|
| 68 |
-
if null_keys:
|
| 69 |
-
logger.warning("โ ๏ธ [VERIFIER] Null fields: %s", null_keys)
|
| 70 |
-
|
| 71 |
-
try:
|
| 72 |
-
method_name = self._DISPATCH.get(schema.problem_type)
|
| 73 |
-
if method_name:
|
| 74 |
-
getattr(self, method_name)(schema)
|
| 75 |
-
else:
|
| 76 |
-
schema.warnings.append(f"No verifier for problem_type='{schema.problem_type}'")
|
| 77 |
-
schema.verification_status = "partial"
|
| 78 |
-
except Exception as e:
|
| 79 |
-
logger.error("โ [VERIFIER] Unexpected error: %s", e)
|
| 80 |
-
schema.warnings.append(f"Verifier internal error: {e}")
|
| 81 |
-
schema.verification_status = "partial"
|
| 82 |
-
|
| 83 |
-
logger.info(
|
| 84 |
-
"โ
[VERIFIER] Done. status=%s | verified=%s | warnings=%d",
|
| 85 |
-
schema.verification_status,
|
| 86 |
-
list(schema.verified.keys()),
|
| 87 |
-
len(schema.warnings),
|
| 88 |
-
)
|
| 89 |
-
return schema
|
| 90 |
-
|
| 91 |
-
# โโ GEOMETRY โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 92 |
-
def _verify_geometry(self, schema: MathProblemSchema) -> None:
|
| 93 |
-
g = schema.given
|
| 94 |
-
verified = {}
|
| 95 |
-
sub = schema.sub_type.lower()
|
| 96 |
-
if "circle" in sub:
|
| 97 |
-
self._verify_circle(schema, g, verified)
|
| 98 |
-
elif any(k in sub for k in ("line", "linear", "segment")):
|
| 99 |
-
self._verify_linear(schema, g, verified)
|
| 100 |
-
else:
|
| 101 |
-
schema.warnings.append(f"No geometry handler for sub_type='{schema.sub_type}'")
|
| 102 |
-
schema.verification_status = "partial"
|
| 103 |
-
|
| 104 |
-
def _verify_circle(self, schema: MathProblemSchema, g: dict, verified: dict) -> None:
|
| 105 |
-
"""
|
| 106 |
-
PRIORITY 1: diameter_lines โ center = algebraic intersection.
|
| 107 |
-
PRIORITY 2: diameter chord โ midpoint (only if no lines given).
|
| 108 |
-
"""
|
| 109 |
-
import re as _re
|
| 110 |
-
|
| 111 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 112 |
-
# PRIORITY 1: Diameter line equations โ MANDATORY if present
|
| 113 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ๏ฟฝ๏ฟฝโโโโโโโโโ
|
| 114 |
-
diameter_lines = g.get("diameter_lines")
|
| 115 |
-
if diameter_lines and len(diameter_lines) >= 2:
|
| 116 |
-
logger.info(
|
| 117 |
-
"๐ [VERIFIER/circle] diameter_lines detected โ using intersection method"
|
| 118 |
-
)
|
| 119 |
-
center, method = _find_center_from_diameter_lines(diameter_lines, schema)
|
| 120 |
-
if center is not None:
|
| 121 |
-
cx, cy = float(center.x), float(center.y)
|
| 122 |
-
verified["M"] = (cx, cy)
|
| 123 |
-
verified["center"] = (cx, cy)
|
| 124 |
-
verified["center_method"] = "diameter_line_intersection"
|
| 125 |
-
verified["verification_method"] = "diameter_line_intersection"
|
| 126 |
-
schema.verified.update(verified)
|
| 127 |
-
logger.info(
|
| 128 |
-
"โ
[VERIFIER/circle] M = (%s, %s) via %s (method=diameter_line_intersection)",
|
| 129 |
-
cx, cy, method,
|
| 130 |
-
)
|
| 131 |
-
|
| 132 |
-
# Verify radius from a known point
|
| 133 |
-
_compute_radius_from_center(g, center, verified, schema)
|
| 134 |
-
schema.verified.update(verified)
|
| 135 |
-
|
| 136 |
-
# Cross-check if radius also given
|
| 137 |
-
given_r = g.get("radius")
|
| 138 |
-
if given_r and "radius" in verified:
|
| 139 |
-
if abs(float(given_r) - verified["radius"]) > 1e-6:
|
| 140 |
-
schema.warnings.append(
|
| 141 |
-
f"CONTRADICTION: given radius={given_r} vs "
|
| 142 |
-
f"computed={verified['radius']:.4f}"
|
| 143 |
-
)
|
| 144 |
-
schema.verification_status = "contradiction"
|
| 145 |
-
return
|
| 146 |
-
|
| 147 |
-
schema.verification_status = "partial" if schema.null_fields() else "verified"
|
| 148 |
-
return
|
| 149 |
-
else:
|
| 150 |
-
schema.warnings.append(
|
| 151 |
-
f"diameter_lines parsing/solve failed: {method} โ falling back to chord"
|
| 152 |
-
)
|
| 153 |
-
|
| 154 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 155 |
-
# PRIORITY 2: Midpoint of diameter chord (FALLBACK)
|
| 156 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 157 |
-
points = {}
|
| 158 |
-
for k, v in g.items():
|
| 159 |
-
if v is None:
|
| 160 |
-
continue
|
| 161 |
-
p = _to_point(v)
|
| 162 |
-
if p:
|
| 163 |
-
points[k] = p
|
| 164 |
-
|
| 165 |
-
radius_raw = g.get("radius")
|
| 166 |
-
radius = _to_rational(radius_raw)
|
| 167 |
-
|
| 168 |
-
diameter_pairs: List[Tuple[str, str]] = []
|
| 169 |
-
has_diameter_constraint = False
|
| 170 |
-
for c in schema.constraints:
|
| 171 |
-
if "diameter" in c.lower():
|
| 172 |
-
has_diameter_constraint = True
|
| 173 |
-
labels = _re.findall(r"\b([A-Za-z]{1,2})\b", c)
|
| 174 |
-
matched = [lbl for lbl in labels if lbl in points]
|
| 175 |
-
if len(matched) >= 2:
|
| 176 |
-
diameter_pairs.append((matched[0], matched[1]))
|
| 177 |
-
|
| 178 |
-
# Auto-pick first 2 points if labels ambiguous
|
| 179 |
-
if not diameter_pairs and has_diameter_constraint and len(points) >= 2:
|
| 180 |
-
keys = list(points.keys())
|
| 181 |
-
diameter_pairs.append((keys[0], keys[1]))
|
| 182 |
-
logger.info(
|
| 183 |
-
"๐ [VERIFIER/circle/fallback] Using points %s, %s as diameter chord",
|
| 184 |
-
keys[0], keys[1],
|
| 185 |
-
)
|
| 186 |
-
|
| 187 |
-
if not diameter_pairs:
|
| 188 |
-
msg = "No diameter_lines and no named diameter chord โ insufficient data"
|
| 189 |
-
schema.warnings.append(msg)
|
| 190 |
-
schema.verification_status = "insufficient_data"
|
| 191 |
-
return
|
| 192 |
-
|
| 193 |
-
for p1_label, p2_label in diameter_pairs:
|
| 194 |
-
p1, p2 = points[p1_label], points[p2_label]
|
| 195 |
-
cx = (p1.x + p2.x) / 2
|
| 196 |
-
cy = (p1.y + p2.y) / 2
|
| 197 |
-
center = sp.Point2D(cx, cy)
|
| 198 |
-
computed_radius = p1.distance(p2) / 2
|
| 199 |
-
|
| 200 |
-
verified["center"] = (float(cx), float(cy))
|
| 201 |
-
verified["M"] = (float(cx), float(cy))
|
| 202 |
-
verified["radius"] = float(computed_radius)
|
| 203 |
-
verified["center_method"] = "chord_midpoint_fallback"
|
| 204 |
-
verified["verification_method"] = "chord_midpoint_fallback"
|
| 205 |
-
schema.verified.update(verified)
|
| 206 |
-
|
| 207 |
-
logger.info(
|
| 208 |
-
"๐ [VERIFIER/circle/fallback] midpoint(%s,%s) โ center=%s radius=%s [FALLBACK โ status=partial]",
|
| 209 |
-
p1_label, p2_label, center, computed_radius,
|
| 210 |
-
)
|
| 211 |
-
|
| 212 |
-
if radius is not None and abs(float(computed_radius) - float(radius)) > 1e-6:
|
| 213 |
-
msg = (
|
| 214 |
-
f"CONTRADICTION: given radius={radius_raw} but "
|
| 215 |
-
f"computed from chord {p1_label}{p2_label}={float(computed_radius):.4f}"
|
| 216 |
-
)
|
| 217 |
-
schema.warnings.append(msg)
|
| 218 |
-
logger.error("๐จ [VERIFIER/circle] %s", msg)
|
| 219 |
-
schema.verification_status = "contradiction"
|
| 220 |
-
return
|
| 221 |
-
|
| 222 |
-
for label, pt in points.items():
|
| 223 |
-
if label in (p1_label, p2_label):
|
| 224 |
-
continue
|
| 225 |
-
dist = float(pt.distance(center))
|
| 226 |
-
r = float(computed_radius)
|
| 227 |
-
if abs(dist - r) < 1e-6:
|
| 228 |
-
verified[f"{label}_on_circle"] = True
|
| 229 |
-
else:
|
| 230 |
-
schema.warnings.append(
|
| 231 |
-
f"Point {label} NOT on circle (dist={dist:.4f}, r={r:.4f})"
|
| 232 |
-
)
|
| 233 |
-
|
| 234 |
-
schema.verified.update(verified)
|
| 235 |
-
# Chord midpoint fallback is always partial โ center is unconfirmed without lines
|
| 236 |
-
if schema.verified.get("center_method") == "chord_midpoint_fallback":
|
| 237 |
-
schema.verification_status = "partial"
|
| 238 |
-
else:
|
| 239 |
-
schema.verification_status = "partial" if schema.null_fields() else "verified"
|
| 240 |
-
|
| 241 |
-
def _verify_linear(self, schema: MathProblemSchema, g: dict, verified: dict) -> None:
|
| 242 |
-
schema.warnings.append("Linear geometry verifier: basic โ extend as needed")
|
| 243 |
-
schema.verification_status = "partial"
|
| 244 |
-
|
| 245 |
-
# โโ ALGEBRA โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 246 |
-
def _verify_algebra(self, schema: MathProblemSchema) -> None:
|
| 247 |
-
g = schema.given
|
| 248 |
-
verified = schema.verified
|
| 249 |
-
equation_str = g.get("equation")
|
| 250 |
-
if equation_str is None:
|
| 251 |
-
schema.warnings.append("Algebra: 'equation' is null")
|
| 252 |
-
schema.verification_status = "insufficient_data"
|
| 253 |
-
return
|
| 254 |
-
try:
|
| 255 |
-
if "=" in str(equation_str):
|
| 256 |
-
lhs_s, rhs_s = str(equation_str).split("=", 1)
|
| 257 |
-
x = sp.Symbol("x")
|
| 258 |
-
expr = sp.sympify(lhs_s.strip()) - sp.sympify(rhs_s.strip())
|
| 259 |
-
else:
|
| 260 |
-
x = sp.Symbol("x")
|
| 261 |
-
expr = sp.sympify(str(equation_str))
|
| 262 |
-
solutions = sp.solve(expr, x)
|
| 263 |
-
verified["solutions"] = [str(s) for s in solutions]
|
| 264 |
-
verified["verification_method"] = "sympy_solve_substitution"
|
| 265 |
-
logger.info("๐ [VERIFIER/algebra] Solutions: %s (method=sympy_solve_substitution)", solutions)
|
| 266 |
-
given_solution = g.get("solution")
|
| 267 |
-
if given_solution is not None:
|
| 268 |
-
given_val = sp.Rational(given_solution)
|
| 269 |
-
if given_val not in solutions:
|
| 270 |
-
schema.warnings.append(
|
| 271 |
-
f"CONTRADICTION: given solution={given_solution} not in {solutions}"
|
| 272 |
-
)
|
| 273 |
-
schema.verification_status = "contradiction"
|
| 274 |
-
return
|
| 275 |
-
schema.verification_status = "partial" if schema.null_fields() else "verified"
|
| 276 |
-
except Exception as e:
|
| 277 |
-
schema.warnings.append(f"Algebra verifier error: {e}")
|
| 278 |
-
schema.verification_status = "partial"
|
| 279 |
-
|
| 280 |
-
# โโ PROBABILITY โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 281 |
-
def _verify_probability(self, schema: MathProblemSchema) -> None:
|
| 282 |
-
g = schema.given
|
| 283 |
-
verified = schema.verified
|
| 284 |
-
probs = g.get("probabilities")
|
| 285 |
-
if probs is None:
|
| 286 |
-
schema.warnings.append("Probability: 'probabilities' is null")
|
| 287 |
-
schema.verification_status = "insufficient_data"
|
| 288 |
-
return
|
| 289 |
-
try:
|
| 290 |
-
total = sum(sp.Rational(str(p)) for p in probs)
|
| 291 |
-
verified["sum"] = float(total)
|
| 292 |
-
verified["verification_method"] = "sympy_rational_sum"
|
| 293 |
-
logger.info("๐ [VERIFIER/probability] sum=%s (method=sympy_rational_sum)", float(total))
|
| 294 |
-
if total != 1:
|
| 295 |
-
schema.warnings.append(
|
| 296 |
-
f"CONTRADICTION: probabilities sum to {float(total):.4f}, not 1.0"
|
| 297 |
-
)
|
| 298 |
-
schema.verification_status = "contradiction"
|
| 299 |
-
else:
|
| 300 |
-
schema.verification_status = "verified"
|
| 301 |
-
except Exception as e:
|
| 302 |
-
schema.warnings.append(f"Probability verifier error: {e}")
|
| 303 |
-
schema.verification_status = "partial"
|
| 304 |
-
|
| 305 |
-
# โโ STATISTICS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 306 |
-
def _verify_statistics(self, schema: MathProblemSchema) -> None:
|
| 307 |
-
import numpy as np
|
| 308 |
-
g = schema.given
|
| 309 |
-
verified = schema.verified
|
| 310 |
-
data = g.get("data")
|
| 311 |
-
if data is None:
|
| 312 |
-
schema.warnings.append("Statistics: 'data' is null")
|
| 313 |
-
schema.verification_status = "insufficient_data"
|
| 314 |
-
return
|
| 315 |
-
try:
|
| 316 |
-
arr = np.array([float(x) for x in data])
|
| 317 |
-
verified["mean"] = float(np.mean(arr))
|
| 318 |
-
verified["std"] = float(np.std(arr, ddof=0))
|
| 319 |
-
verified["verification_method"] = "numpy_population_stats"
|
| 320 |
-
logger.info(
|
| 321 |
-
"๐ [VERIFIER/statistics] mean=%.4f std=%.4f (method=numpy_population_stats)",
|
| 322 |
-
verified["mean"], verified["std"],
|
| 323 |
-
)
|
| 324 |
-
for key in ("mean", "std"):
|
| 325 |
-
given_val = g.get(key)
|
| 326 |
-
if given_val is not None and abs(float(given_val) - verified[key]) > 1e-6:
|
| 327 |
-
schema.warnings.append(
|
| 328 |
-
f"CONTRADICTION: given {key}={given_val} vs computed={verified[key]:.6f}"
|
| 329 |
-
)
|
| 330 |
-
schema.verification_status = "contradiction"
|
| 331 |
-
return
|
| 332 |
-
schema.verification_status = "verified"
|
| 333 |
-
except Exception as e:
|
| 334 |
-
schema.warnings.append(f"Statistics verifier error: {e}")
|
| 335 |
-
schema.verification_status = "partial"
|
| 336 |
-
|
| 337 |
-
# โโ TRIGONOMETRY โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 338 |
-
def _verify_trigonometry(self, schema: MathProblemSchema) -> None:
|
| 339 |
-
g = schema.given
|
| 340 |
-
verified = schema.verified
|
| 341 |
-
identity = g.get("identity")
|
| 342 |
-
angle_deg = g.get("angle_deg")
|
| 343 |
-
if identity is None:
|
| 344 |
-
schema.warnings.append("Trigonometry: 'identity' is null")
|
| 345 |
-
schema.verification_status = "insufficient_data"
|
| 346 |
-
return
|
| 347 |
-
try:
|
| 348 |
-
theta = sp.Symbol("theta")
|
| 349 |
-
lhs_s, rhs_s = str(identity).split("=", 1)
|
| 350 |
-
lhs = sp.sympify(lhs_s.strip())
|
| 351 |
-
rhs = sp.sympify(rhs_s.strip())
|
| 352 |
-
diff = sp.simplify(lhs - rhs)
|
| 353 |
-
if diff == 0:
|
| 354 |
-
verified["identity_valid"] = True
|
| 355 |
-
verified["verification_method"] = "sympy_symbolic_simplification"
|
| 356 |
-
logger.info("โ
[VERIFIER/trig] Identity valid (method=sympy_symbolic_simplification)")
|
| 357 |
-
schema.verification_status = "verified"
|
| 358 |
-
elif 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 |
-
schema.verification_status = "partial" if abs(val) < 1e-9 else "contradiction"
|
| 363 |
-
else:
|
| 364 |
-
schema.warnings.append("Trig identity could not be simplified to zero")
|
| 365 |
-
schema.verification_status = "partial"
|
| 366 |
-
except Exception as e:
|
| 367 |
-
schema.warnings.append(f"Trigonometry verifier error: {e}")
|
| 368 |
-
schema.verification_status = "partial"
|
| 369 |
-
|
| 370 |
-
# โโ CALCULUS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 371 |
-
def _verify_calculus(self, schema: MathProblemSchema) -> None:
|
| 372 |
-
g = schema.given
|
| 373 |
-
verified = schema.verified
|
| 374 |
-
func_str = g.get("function")
|
| 375 |
-
point_raw = g.get("point")
|
| 376 |
-
if func_str is None:
|
| 377 |
-
schema.warnings.append("Calculus: 'function' is null")
|
| 378 |
-
schema.verification_status = "insufficient_data"
|
| 379 |
-
return
|
| 380 |
-
try:
|
| 381 |
-
x = sp.Symbol("x")
|
| 382 |
-
f = sp.sympify(str(func_str))
|
| 383 |
-
deriv = sp.diff(f, x)
|
| 384 |
-
verified["derivative"] = str(deriv)
|
| 385 |
-
verified["verification_method"] = "sympy_differentiation"
|
| 386 |
-
logger.info("๐ [VERIFIER/calculus] f'(x)=%s (method=sympy_differentiation)", deriv)
|
| 387 |
-
if point_raw is not None:
|
| 388 |
-
pt = _to_rational(point_raw)
|
| 389 |
-
val = float(deriv.subs(x, pt))
|
| 390 |
-
verified["derivative_at_point"] = val
|
| 391 |
-
given_dv = g.get("derivative_at_point")
|
| 392 |
-
if given_dv is not None and abs(float(given_dv) - val) > 1e-9:
|
| 393 |
-
schema.warnings.append(
|
| 394 |
-
f"CONTRADICTION: given f'({point_raw})={given_dv} vs computed={val:.6f}"
|
| 395 |
-
)
|
| 396 |
-
schema.verification_status = "contradiction"
|
| 397 |
-
return
|
| 398 |
-
schema.verification_status = "verified"
|
| 399 |
-
except Exception as e:
|
| 400 |
-
schema.warnings.append(f"Calculus verifier error: {e}")
|
| 401 |
-
schema.verification_status = "partial"
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
# โโ Module-level helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 405 |
-
def _find_center_from_diameter_lines(
|
| 406 |
-
diameter_lines: list,
|
| 407 |
-
schema: "MathProblemSchema",
|
| 408 |
-
) -> Tuple[Optional[sp.Point2D], str]:
|
| 409 |
-
"""
|
| 410 |
-
Solve the intersection of two diameter line equations.
|
| 411 |
-
Returns (sp.Point2D, description) or (None, reason).
|
| 412 |
-
"""
|
| 413 |
-
if len(diameter_lines) < 2:
|
| 414 |
-
return None, "need at least 2 diameter lines"
|
| 415 |
-
|
| 416 |
-
x, y = sp.symbols("x y")
|
| 417 |
-
|
| 418 |
-
def _parse_line(eq_str: str) -> Optional[sp.Eq]:
|
| 419 |
-
eq_str = str(eq_str).strip().replace(" ", "")
|
| 420 |
-
if "=" not in eq_str:
|
| 421 |
-
return None
|
| 422 |
-
lhs_s, rhs_s = eq_str.split("=", 1)
|
| 423 |
-
try:
|
| 424 |
-
return sp.Eq(sp.sympify(lhs_s), sp.sympify(rhs_s))
|
| 425 |
-
except Exception as e:
|
| 426 |
-
logger.error("โ [VERIFIER/diameters] Cannot parse '%s': %s", eq_str, e)
|
| 427 |
-
return None
|
| 428 |
-
|
| 429 |
-
eq1 = _parse_line(diameter_lines[0])
|
| 430 |
-
eq2 = _parse_line(diameter_lines[1])
|
| 431 |
-
|
| 432 |
-
if eq1 is None or eq2 is None:
|
| 433 |
-
return None, "line parsing failed"
|
| 434 |
-
|
| 435 |
-
logger.info(
|
| 436 |
-
"๐ [VERIFIER/diameters] Solving:\n L1=%s\n L2=%s", eq1, eq2
|
| 437 |
-
)
|
| 438 |
-
|
| 439 |
-
try:
|
| 440 |
-
sol = sp.solve([eq1, eq2], [x, y])
|
| 441 |
-
if not sol:
|
| 442 |
-
schema.warnings.append("Diameter lines are parallel โ no intersection")
|
| 443 |
-
return None, "parallel lines"
|
| 444 |
-
|
| 445 |
-
if isinstance(sol, dict):
|
| 446 |
-
cx, cy = sol[x], sol[y]
|
| 447 |
-
elif isinstance(sol, (list, tuple)) and sol:
|
| 448 |
-
first = sol[0]
|
| 449 |
-
cx, cy = first[0], first[1]
|
| 450 |
-
else:
|
| 451 |
-
return None, "unexpected solution format"
|
| 452 |
-
|
| 453 |
-
center = sp.Point2D(cx, cy)
|
| 454 |
-
logger.info("โ
[VERIFIER/diameters] Center = (%s, %s)", cx, cy)
|
| 455 |
-
return center, f"intersection of {diameter_lines[0]} โฉ {diameter_lines[1]}"
|
| 456 |
-
|
| 457 |
-
except Exception as e:
|
| 458 |
-
logger.error("โ [VERIFIER/diameters] Solve error: %s", e)
|
| 459 |
-
return None, f"solve error: {e}"
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
def _compute_radius_from_center(
|
| 463 |
-
g: dict, center: sp.Point2D, verified: dict, schema: "MathProblemSchema"
|
| 464 |
-
) -> None:
|
| 465 |
-
"""Compute radius as distance from center to the first known point."""
|
| 466 |
-
for k, v in g.items():
|
| 467 |
-
if k in ("diameter_lines", "radius"):
|
| 468 |
-
continue
|
| 469 |
-
if v is None:
|
| 470 |
-
continue
|
| 471 |
-
p = _to_point(v)
|
| 472 |
-
if p:
|
| 473 |
-
r = float(p.distance(center))
|
| 474 |
-
verified["radius"] = r
|
| 475 |
-
logger.info("๐ [VERIFIER/circle] radius = dist(%s, center) = %.4f", k, r)
|
| 476 |
-
return
|
| 477 |
-
# Fallback: use given radius
|
| 478 |
-
given_r = g.get("radius")
|
| 479 |
-
if given_r is not None:
|
| 480 |
-
verified["radius"] = float(given_r)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/variable_pinner.py
DELETED
|
@@ -1,113 +0,0 @@
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config.py
DELETED
|
@@ -1,63 +0,0 @@
|
|
| 1 |
-
# config.py - V1.1 (Central Config - Force Rebuild)
|
| 2 |
-
import os
|
| 3 |
-
from dotenv import load_dotenv
|
| 4 |
-
|
| 5 |
-
# Load environment variables from .env file
|
| 6 |
-
load_dotenv()
|
| 7 |
-
|
| 8 |
-
# Determine Environment
|
| 9 |
-
# V5.9.3: Detailed logging for Hugging Face environment detection
|
| 10 |
-
raw_env = os.getenv("ENV", "").lower()
|
| 11 |
-
space_id = os.getenv("SPACE_ID", "").lower()
|
| 12 |
-
|
| 13 |
-
print(f"๐ [DEBUG-CONFIG] raw_env: '{raw_env}', space_id: '{space_id}'")
|
| 14 |
-
|
| 15 |
-
if not raw_env:
|
| 16 |
-
# Check for keywords that indicate a dev space
|
| 17 |
-
if any(k in space_id for k in ["buddymath-dev", "-dev", "sandbox"]):
|
| 18 |
-
ENV = "development"
|
| 19 |
-
print(f"โจ [DEBUG-CONFIG] Auto-detected DEVELOPMENT mode (space_id match).")
|
| 20 |
-
else:
|
| 21 |
-
ENV = "production"
|
| 22 |
-
print(f"๐ก๏ธ [DEBUG-CONFIG] Defaulting to PRODUCTION mode.")
|
| 23 |
-
else:
|
| 24 |
-
ENV = raw_env
|
| 25 |
-
print(f"๐ [DEBUG-CONFIG] Using explicit ENV from settings: {ENV}")
|
| 26 |
-
|
| 27 |
-
IS_PRODUCTION = ENV == "production"
|
| 28 |
-
|
| 29 |
-
# V5.9.1: DEV Auth Bypass Token (Whitelisted only in non-prod environments)
|
| 30 |
-
DEV_BYPASS_TOKEN = "BUDDY-MATH-DEV-2026-BYPASS"
|
| 31 |
-
|
| 32 |
-
# Firebase Configuration
|
| 33 |
-
# V5.11.3: DEPRECATED local JSON files for security.
|
| 34 |
-
# Credentials must be provided via FIREBASE_CREDENTIALS environment variable.
|
| 35 |
-
if IS_PRODUCTION:
|
| 36 |
-
# PROD: bussymath
|
| 37 |
-
FIREBASE_CREDENTIALS_PATH = None
|
| 38 |
-
STORAGE_BUCKET = "bussymath.firebasestorage.app"
|
| 39 |
-
PROJECT_ID = "bussymath"
|
| 40 |
-
else:
|
| 41 |
-
# DEV: buddy-math-dev
|
| 42 |
-
FIREBASE_CREDENTIALS_PATH = None
|
| 43 |
-
STORAGE_BUCKET = "buddy-math-dev.firebasestorage.app"
|
| 44 |
-
PROJECT_ID = "buddy-math-dev"
|
| 45 |
-
|
| 46 |
-
# Server Configuration
|
| 47 |
-
HOST = "0.0.0.0"
|
| 48 |
-
PORT = 8000 if not IS_PRODUCTION else 7860 # HF Spaces/Cloud Run often use 7860 or PORT env
|
| 49 |
-
|
| 50 |
-
# V3.1.2: Adaptive Failure Mode - Confidence Thresholds
|
| 51 |
-
# V3.1.3: Recalibrated (Hard Floor 0.55)
|
| 52 |
-
# V4.2.11: Lowered for DEV to unblock geometry diagnostics
|
| 53 |
-
CONFIDENCE_THRESHOLD_HIGH = 0.75
|
| 54 |
-
CONFIDENCE_THRESHOLD_MEDIUM = 0.55 if IS_PRODUCTION else 0.01
|
| 55 |
-
|
| 56 |
-
print(f"[CONFIG] Loading {ENV.upper()} configuration.")
|
| 57 |
-
print(f"[CONFIG] Project: {PROJECT_ID}")
|
| 58 |
-
print(f"[CONFIG] Bucket: {STORAGE_BUCKET}")
|
| 59 |
-
|
| 60 |
-
# V3.1.3: Model Hardening - Restricted to gemini-2.0-flash ONLY
|
| 61 |
-
# To change model, update the environment variable 'GEMINI_MODEL'
|
| 62 |
-
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.0-flash")
|
| 63 |
-
print(f"[CONFIG] Active Model: {GEMINI_MODEL}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constraint_checks.py
DELETED
|
@@ -1,159 +0,0 @@
|
|
| 1 |
-
# constraint_checks.py - V1.0 (Geometric Constraint Validator)
|
| 2 |
-
# Deterministic algebraic checks using SymPy.
|
| 3 |
-
# Each function verifies a specific geometric relationship and logs
|
| 4 |
-
# the result to the GeometryAnchor (verified_facts or warnings).
|
| 5 |
-
|
| 6 |
-
import logging
|
| 7 |
-
from sympy import simplify, symbols, Eq, solve, Rational
|
| 8 |
-
|
| 9 |
-
from geometry_types import Point, Line, Segment, Circle, GeometryAnchor
|
| 10 |
-
|
| 11 |
-
logger = logging.getLogger(__name__)
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
def check_point_on_circle(p: Point, circle: Circle, anchor: GeometryAnchor):
|
| 15 |
-
"""
|
| 16 |
-
Verifies algebraically whether point p lies on the circle boundary.
|
| 17 |
-
Injects a verified_fact or a warning into the anchor.
|
| 18 |
-
"""
|
| 19 |
-
try:
|
| 20 |
-
if circle.contains(p):
|
| 21 |
-
anchor.add_fact(
|
| 22 |
-
f"โ ืื ืงืืื {p} ื ืืฆืืช ืขื ืืืขืื (ืืจืื {circle.center}, r={circle.radius})"
|
| 23 |
-
)
|
| 24 |
-
else:
|
| 25 |
-
lhs = (p.x - circle.center.x)**2 + (p.y - circle.center.y)**2
|
| 26 |
-
anchor.add_warning(
|
| 27 |
-
f"โ ืื ืงืืื {p} ืืื ื ืขื ืืืขืื โ "
|
| 28 |
-
f"ืืืฉืื: {simplify(lhs)} โ rยฒ={simplify(circle.radius**2)}. "
|
| 29 |
-
f"ืืืชืื ืฉื-OCR ืืืืฅ ืงืืืืจืืื ืืืช ืฉืืืืืช."
|
| 30 |
-
)
|
| 31 |
-
except Exception as e:
|
| 32 |
-
logger.error(f"[CONSTRAINT] check_point_on_circle failed: {e}")
|
| 33 |
-
anchor.add_warning(f"โ ๏ธ ืื ื ืืชื ืืืืช ืฉืื ืงืืื {p.name} ืขื ืืืขืื: {e}")
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def check_point_on_line(p: Point, line: Line, anchor: GeometryAnchor):
|
| 37 |
-
"""
|
| 38 |
-
Verifies algebraically whether point p lies on the line ax + by + c = 0.
|
| 39 |
-
"""
|
| 40 |
-
try:
|
| 41 |
-
if line.contains(p):
|
| 42 |
-
anchor.add_fact(
|
| 43 |
-
f"โ ืื ืงืืื {p} ื ืืฆืืช ืขื ืืืฉืจ {line}"
|
| 44 |
-
)
|
| 45 |
-
else:
|
| 46 |
-
val = simplify(line.a * p.x + line.b * p.y + line.c)
|
| 47 |
-
anchor.add_warning(
|
| 48 |
-
f"โ ืื ืงืืื {p} ืืื ื ืขื ืืืฉืจ {line} โ "
|
| 49 |
-
f"ืืฆืื ื ืืชื ืช {val} โ 0"
|
| 50 |
-
)
|
| 51 |
-
except Exception as e:
|
| 52 |
-
logger.error(f"[CONSTRAINT] check_point_on_line failed: {e}")
|
| 53 |
-
anchor.add_warning(f"โ ๏ธ ืื ื ืืชื ืืืืช ืฉืื ืงืืื {p.name} ืขื ืืืฉืจ: {e}")
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def check_is_diameter(segment: Segment, circle: Circle, anchor: GeometryAnchor):
|
| 57 |
-
"""
|
| 58 |
-
Checks if a segment is a diameter by verifying its midpoint equals the circle center.
|
| 59 |
-
This is the most critical check for preventing midpoint hallucination.
|
| 60 |
-
"""
|
| 61 |
-
try:
|
| 62 |
-
mid = segment.midpoint()
|
| 63 |
-
center = circle.center
|
| 64 |
-
if mid == center:
|
| 65 |
-
anchor.add_fact(
|
| 66 |
-
f"โ ืืงืืข {segment.p1.name}{segment.p2.name} ืืื ืงืืืจ โ "
|
| 67 |
-
f"ืืืฆืขื {mid} = ืืจืื ืืืขืื {center}"
|
| 68 |
-
)
|
| 69 |
-
else:
|
| 70 |
-
anchor.add_warning(
|
| 71 |
-
f"โ ืืงืืข {segment.p1.name}{segment.p2.name} ืืื ื ืงืืืจ โ "
|
| 72 |
-
f"ืืืฆืข ืืงืืข ืืื {mid}, ืื ืืจืื ืืืขืื ืืื {center}. "
|
| 73 |
-
f"โ ๏ธ ืื ืชื ืื ืฉืืจืื ืืืขืื ืืื ื ืงืืืช ืืืืฆืข ืฉื ืงืืข ืื!"
|
| 74 |
-
)
|
| 75 |
-
except Exception as e:
|
| 76 |
-
logger.error(f"[CONSTRAINT] check_is_diameter failed: {e}")
|
| 77 |
-
anchor.add_warning(f"โ ๏ธ ืื ื ืืชื ืืืืช ืืื ืืงืืข {segment} ืืื ืงืืืจ: {e}")
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def check_perpendicular_lines(slope1, slope2, anchor: GeometryAnchor, label1="ืืฉืจ 1", label2="ืืฉืจ 2"):
|
| 81 |
-
"""
|
| 82 |
-
Verifies that two lines (given by slopes) are perpendicular: m1 * m2 = -1.
|
| 83 |
-
"""
|
| 84 |
-
try:
|
| 85 |
-
product = simplify(slope1 * slope2)
|
| 86 |
-
if product == -1:
|
| 87 |
-
anchor.add_fact(
|
| 88 |
-
f"โ {label1} ื-{label2} ืืืื ืืื โ ืืืคืืช ืฉืืคืืขืื = {product} = -1"
|
| 89 |
-
)
|
| 90 |
-
else:
|
| 91 |
-
anchor.add_warning(
|
| 92 |
-
f"โ {label1} ื-{label2} ืืื ื ืืืื ืืื โ ืืืคืืช ืฉืืคืืขืื = {product} โ -1"
|
| 93 |
-
)
|
| 94 |
-
except Exception as e:
|
| 95 |
-
logger.error(f"[CONSTRAINT] check_perpendicular_lines failed: {e}")
|
| 96 |
-
anchor.add_warning(f"โ ๏ธ ืื ื ืืชื ืืืืช ืื ืืืืช: {e}")
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
def find_circle_center_from_diameters(
|
| 100 |
-
d1: Segment,
|
| 101 |
-
d2: Segment,
|
| 102 |
-
anchor: GeometryAnchor
|
| 103 |
-
) -> Point:
|
| 104 |
-
"""
|
| 105 |
-
Finds the circle center deterministically as the intersection of two diameter segments.
|
| 106 |
-
This eliminates any need for the LLM to 'guess' the center.
|
| 107 |
-
"""
|
| 108 |
-
try:
|
| 109 |
-
x, y = symbols('x y')
|
| 110 |
-
|
| 111 |
-
def line_eq_through(p1: Point, p2: Point):
|
| 112 |
-
if simplify(p2.x - p1.x) == 0:
|
| 113 |
-
# Vertical line
|
| 114 |
-
return Eq(x, p1.x)
|
| 115 |
-
slope = Rational(p2.y - p1.y, p2.x - p1.x)
|
| 116 |
-
return Eq(y, slope * (x - p1.x) + p1.y)
|
| 117 |
-
|
| 118 |
-
eq1 = line_eq_through(d1.p1, d1.p2)
|
| 119 |
-
eq2 = line_eq_through(d2.p1, d2.p2)
|
| 120 |
-
|
| 121 |
-
solution = solve([eq1, eq2], [x, y])
|
| 122 |
-
|
| 123 |
-
if not solution:
|
| 124 |
-
anchor.add_warning(
|
| 125 |
-
"[SOLVER] ืื ื ืืชื ืืืฉื ืืจืื ืืขืื ืืืืชืื ืฉื ื ืงืืจืื โ ืืืฉืจืื ืืงืืืืื?"
|
| 126 |
-
)
|
| 127 |
-
return None
|
| 128 |
-
|
| 129 |
-
cx = solution[x] if isinstance(solution, dict) else solution[0]
|
| 130 |
-
cy = solution[y] if isinstance(solution, dict) else solution[1]
|
| 131 |
-
center = Point(name="M", x=simplify(cx), y=simplify(cy))
|
| 132 |
-
anchor.add_fact(
|
| 133 |
-
f"โ ืืจืื ืืืขืื M ืืืฉื ืืืืชืื ืงืืจืื: M = {center}"
|
| 134 |
-
)
|
| 135 |
-
return center
|
| 136 |
-
|
| 137 |
-
except Exception as e:
|
| 138 |
-
logger.error(f"[CONSTRAINT] find_circle_center_from_diameters failed: {e}")
|
| 139 |
-
anchor.add_warning(f"โ ๏ธ ืืืฉืื ืืจืื ืืืขืื ื ืืฉื: {e}")
|
| 140 |
-
return None
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
def check_distance(p1: Point, p2: Point, expected_distance, anchor: GeometryAnchor, label=""):
|
| 144 |
-
"""
|
| 145 |
-
Verifies the distance between two points matches an expected value.
|
| 146 |
-
"""
|
| 147 |
-
try:
|
| 148 |
-
actual = simplify(p1.distance_to(p2))
|
| 149 |
-
expected = simplify(expected_distance)
|
| 150 |
-
desc = label or f"{p1.name}โ{p2.name}"
|
| 151 |
-
if simplify(actual - expected) == 0:
|
| 152 |
-
anchor.add_fact(f"โ ืืืจืืง {desc} = {actual} (ืชืืื ืื ืชืื)")
|
| 153 |
-
else:
|
| 154 |
-
anchor.add_warning(
|
| 155 |
-
f"โ ืืืจืืง {desc}: ืืืฉืื={actual} โ ื ืชืื={expected}"
|
| 156 |
-
)
|
| 157 |
-
except Exception as e:
|
| 158 |
-
logger.error(f"[CONSTRAINT] check_distance failed: {e}")
|
| 159 |
-
anchor.add_warning(f"โ ๏ธ ืืืืงืช ืืจืืง ื ืืฉืื: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cost_tracker.py
DELETED
|
@@ -1,93 +0,0 @@
|
|
| 1 |
-
PRICING = { "input": 0.10, "output": 0.40 }
|
| 2 |
-
|
| 3 |
-
class CostTracker:
|
| 4 |
-
def __init__(self):
|
| 5 |
-
self.input_tokens = 0
|
| 6 |
-
self.output_tokens = 0
|
| 7 |
-
|
| 8 |
-
def add(self, usage_metadata):
|
| 9 |
-
if usage_metadata:
|
| 10 |
-
self.input_tokens += getattr(usage_metadata, 'prompt_token_count', 0)
|
| 11 |
-
self.output_tokens += getattr(usage_metadata, 'candidates_token_count', 0)
|
| 12 |
-
|
| 13 |
-
def add_manual(self, inp, out):
|
| 14 |
-
self.input_tokens += inp
|
| 15 |
-
self.output_tokens += out
|
| 16 |
-
|
| 17 |
-
def get_cost(self):
|
| 18 |
-
return (self.input_tokens / 1e6 * PRICING["input"]) + (self.output_tokens / 1e6 * PRICING["output"])
|
| 19 |
-
|
| 20 |
-
def get_summary(self): # โ
ืืคืื ืงืฆืื ืฉืืืืชื ืืกืจื
|
| 21 |
-
return {
|
| 22 |
-
"input_tokens": self.input_tokens,
|
| 23 |
-
"output_tokens": self.output_tokens,
|
| 24 |
-
"cost_usd": self.get_cost()
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
def print_report(self):
|
| 28 |
-
print(f"๐ฐ COST: ${self.get_cost():.6f}")
|
| 29 |
-
|
| 30 |
-
# Static Logger
|
| 31 |
-
import os
|
| 32 |
-
import json
|
| 33 |
-
import time
|
| 34 |
-
import tempfile
|
| 35 |
-
|
| 36 |
-
# V260.9: Smarter logging path
|
| 37 |
-
def get_log_file_path():
|
| 38 |
-
base_dir = os.path.join(os.getcwd(), "logs")
|
| 39 |
-
# Try creating/accessing local logs dir
|
| 40 |
-
try:
|
| 41 |
-
if not os.path.exists(base_dir):
|
| 42 |
-
os.makedirs(base_dir)
|
| 43 |
-
# Test write permission
|
| 44 |
-
test_file = os.path.join(base_dir, ".test_write")
|
| 45 |
-
with open(test_file, "w") as f: f.write("ok")
|
| 46 |
-
os.remove(test_file)
|
| 47 |
-
return os.path.join(base_dir, "usage.jsonl")
|
| 48 |
-
except (PermissionError, OSError):
|
| 49 |
-
# Fallback to temp
|
| 50 |
-
return os.path.join(tempfile.gettempdir(), "buddy_math_usage.jsonl")
|
| 51 |
-
|
| 52 |
-
LOG_FILE = get_log_file_path()
|
| 53 |
-
LOG_DIR = os.path.dirname(LOG_FILE)
|
| 54 |
-
|
| 55 |
-
import contextvars
|
| 56 |
-
|
| 57 |
-
# Stores the total tokens used in the current solve request
|
| 58 |
-
current_request_tokens = contextvars.ContextVar('current_request_tokens', default=0)
|
| 59 |
-
|
| 60 |
-
def log_api_usage(usage_metadata, source="unknown"):
|
| 61 |
-
"""Log API usage to proper location and add to current ContextVar"""
|
| 62 |
-
if not usage_metadata:
|
| 63 |
-
return
|
| 64 |
-
|
| 65 |
-
try:
|
| 66 |
-
# LOG_DIR check is now redundant but safe
|
| 67 |
-
if not os.path.exists(LOG_DIR):
|
| 68 |
-
os.makedirs(LOG_DIR, exist_ok=True)
|
| 69 |
-
|
| 70 |
-
entry = {
|
| 71 |
-
"timestamp": time.time(),
|
| 72 |
-
"source": source,
|
| 73 |
-
"input_tokens": getattr(usage_metadata, 'prompt_token_count', 0),
|
| 74 |
-
"output_tokens": getattr(usage_metadata, 'candidates_token_count', 0),
|
| 75 |
-
"total_tokens": getattr(usage_metadata, 'total_token_count', 0)
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
| 79 |
-
f.write(json.dumps(entry) + "\n")
|
| 80 |
-
|
| 81 |
-
# Update the ContextVar for the current request
|
| 82 |
-
try:
|
| 83 |
-
current_request_tokens.set(current_request_tokens.get() + entry["total_tokens"])
|
| 84 |
-
except Exception:
|
| 85 |
-
pass
|
| 86 |
-
|
| 87 |
-
# Also print to stdout for immediate visibility in console logs
|
| 88 |
-
print(f"๐ฐ [USAGE] {source}: In={entry['input_tokens']}, Out={entry['output_tokens']}")
|
| 89 |
-
|
| 90 |
-
except Exception as e:
|
| 91 |
-
# Fallback to pure stdout if file logging completely fails
|
| 92 |
-
print(f"๐ฐ [USAGE-STDOUT only] {source}: In={getattr(usage_metadata, 'prompt_token_count', 0)}, Out={getattr(usage_metadata, 'candidates_token_count', 0)}")
|
| 93 |
-
print(f"โ ๏ธ [USAGE LOG ERROR] Failed to log to file {LOG_FILE}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
create_mock_user.py
DELETED
|
@@ -1,49 +0,0 @@
|
|
| 1 |
-
import firebase_admin
|
| 2 |
-
from firebase_admin import credentials, firestore
|
| 3 |
-
import json
|
| 4 |
-
import os
|
| 5 |
-
|
| 6 |
-
# Manual init for script
|
| 7 |
-
FIREBASE_CREDENTIALS_PATH = "serviceAccountKey.json"
|
| 8 |
-
STORAGE_BUCKET = "bussymath.firebasestorage.app"
|
| 9 |
-
|
| 10 |
-
if not firebase_admin._apps:
|
| 11 |
-
with open(FIREBASE_CREDENTIALS_PATH, "r", encoding="utf-8") as f:
|
| 12 |
-
cred_dict = json.load(f)
|
| 13 |
-
cred = credentials.Certificate(cred_dict)
|
| 14 |
-
firebase_admin.initialize_app(cred, {
|
| 15 |
-
'storageBucket': STORAGE_BUCKET
|
| 16 |
-
})
|
| 17 |
-
|
| 18 |
-
db = firestore.client()
|
| 19 |
-
|
| 20 |
-
def create_mock_user():
|
| 21 |
-
uid = "dev-bypass-user"
|
| 22 |
-
user_ref = db.collection('users').document(uid)
|
| 23 |
-
|
| 24 |
-
today = "2026-03-12" # Static for bypass
|
| 25 |
-
month = "2026-03"
|
| 26 |
-
|
| 27 |
-
user_data = {
|
| 28 |
-
'uid': uid,
|
| 29 |
-
'name': 'Dev Bypass User',
|
| 30 |
-
'status': 'approved',
|
| 31 |
-
'tier': 'student_premium',
|
| 32 |
-
'used_today': 0,
|
| 33 |
-
'used_tokens_this_month': 0,
|
| 34 |
-
'monthly_token_budget': 9999999,
|
| 35 |
-
'last_usage_month': month,
|
| 36 |
-
'quota': {
|
| 37 |
-
'limit': 9999,
|
| 38 |
-
'used_today': 0,
|
| 39 |
-
'last_reset_date': today,
|
| 40 |
-
},
|
| 41 |
-
'isAdmin': True,
|
| 42 |
-
'role': 'admin'
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
user_ref.set(user_data, merge=True)
|
| 46 |
-
print(f"โ
Created mock user: {uid}")
|
| 47 |
-
|
| 48 |
-
if __name__ == "__main__":
|
| 49 |
-
create_mock_user()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
curriculum_engine.py
DELETED
|
@@ -1,115 +0,0 @@
|
|
| 1 |
-
# curriculum_engine.py - V4.0 (Curriculum Oracle)
|
| 2 |
-
# Defines pedagogical boundaries according to the Israeli Ministry of Education (Mafmar).
|
| 3 |
-
|
| 4 |
-
import logging
|
| 5 |
-
|
| 6 |
-
logger = logging.getLogger(__name__)
|
| 7 |
-
|
| 8 |
-
CURRICULUM_RULES = {
|
| 9 |
-
# grade: { blacklist: [operators], whitelist: [operators], descriptions: {operator: hebrew_name} }
|
| 10 |
-
7: {
|
| 11 |
-
"blacklist": ["SYSTEM_OF_EQUATIONS", "QUADRATIC_EQUATION", "DERIVATIVE", "INTEGRAL", "TRIGONOMETRY"],
|
| 12 |
-
"whitelist": ["LINEAR_EQUATION", "FRACTIONS", "EXPONENTS_BASIC", "PERCENTAGES"],
|
| 13 |
-
"descriptions": {
|
| 14 |
-
"SYSTEM_OF_EQUATIONS": "ืืขืจืืช ืืฉืืืืืช ืืฉื ื ื ืขืืืื",
|
| 15 |
-
"QUADRATIC_EQUATION": "ืืฉืืืื ืจืืืืขืืช",
|
| 16 |
-
"DERIVATIVE": "ื ืืืจืืช",
|
| 17 |
-
"TRIGONOMETRY": "ืืจืืืื ืืืืจืื"
|
| 18 |
-
}
|
| 19 |
-
},
|
| 20 |
-
8: {
|
| 21 |
-
"blacklist": ["QUADRATIC_EQUATION", "DERIVATIVE", "INTEGRAL", "TRIG_Trig_ADVANCED"],
|
| 22 |
-
"whitelist": ["LINEAR_EQUATION", "SYSTEM_OF_EQUATIONS", "BASIC_GEOMETRY", "PYTHAGOREAN_THEOREM"],
|
| 23 |
-
"descriptions": {
|
| 24 |
-
"QUADRATIC_EQUATION": "ืืฉืืืื ืจืืืืขืืช",
|
| 25 |
-
"DERIVATIVE": "ื ืืืจืืช"
|
| 26 |
-
}
|
| 27 |
-
},
|
| 28 |
-
9: {
|
| 29 |
-
"blacklist": ["DERIVATIVE", "INTEGRAL", "LOGARITHM"],
|
| 30 |
-
"whitelist": ["QUADRATIC_EQUATION", "SYSTEM_OF_EQUATIONS", "SIMILAR_TRIANGLES", "CIRCLE_BASIC"],
|
| 31 |
-
"descriptions": {
|
| 32 |
-
"DERIVATIVE": "ื ืืืจืืช",
|
| 33 |
-
"LOGARITHM": "ืืืืจืืชืืื"
|
| 34 |
-
}
|
| 35 |
-
},
|
| 36 |
-
10: {
|
| 37 |
-
"blacklist": ["INTEGRAL_ADVANCED", "VECTORS"],
|
| 38 |
-
"whitelist": ["DERIVATIVE_POLYNOMIAL", "TRIGONOMETRY_BASIC", "CIRCLE_EQUATION_BASIC"],
|
| 39 |
-
"descriptions": {
|
| 40 |
-
"INTEGRAL_ADVANCED": "ืืื ืืืจืืื ืืืจืืืื",
|
| 41 |
-
"VECTORS": "ืืืงืืืจืื"
|
| 42 |
-
}
|
| 43 |
-
},
|
| 44 |
-
# V286.0: Grade 11 โ 4 ืื"ื focus
|
| 45 |
-
11: {
|
| 46 |
-
"blacklist": ["VECTORS_3D", "ADVANCED_INTEGRATION", "SOLID_GEOMETRY_ADVANCED"],
|
| 47 |
-
"whitelist": ["DERIVATIVE", "TRIGONOMETRY", "LOGARITHM", "SEQUENCES", "COMPLEX_NUMBERS_BASIC",
|
| 48 |
-
"FUNCTION_ANALYSIS", "PROBABILITY_BASIC", "CIRCLE_EQUATION"],
|
| 49 |
-
"descriptions": {
|
| 50 |
-
"VECTORS_3D": "ืืงืืืจืื ืืืจืื",
|
| 51 |
-
"ADVANCED_INTEGRATION": "ืืื ืืืจืฆืื ืืชืงืืืช"
|
| 52 |
-
}
|
| 53 |
-
},
|
| 54 |
-
# V286.0: Grade 12 / 5 ืื"ื โ full access
|
| 55 |
-
12: {
|
| 56 |
-
"blacklist": [],
|
| 57 |
-
"whitelist": ["DERIVATIVE", "INTEGRAL", "INTEGRAL_ADVANCED", "TRIGONOMETRY",
|
| 58 |
-
"TRIGONOMETRY_ADVANCED", "LOGARITHM", "SEQUENCES", "COMPLEX_NUMBERS",
|
| 59 |
-
"COMPLEX_ANALYSIS", "VECTORS", "VECTORS_3D", "FUNCTION_ANALYSIS",
|
| 60 |
-
"PROBABILITY", "COMBINATORICS", "SOLID_GEOMETRY", "LIMITS",
|
| 61 |
-
"OPTIMIZATION", "INDUCTION", "GEOMETRY_ANALYTIC", "CIRCLE_EQUATION"],
|
| 62 |
-
"descriptions": {}
|
| 63 |
-
}
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
def get_allowed_math_operators(grade_str: str, level: str = "4", topic: str = "GENERAL") -> dict:
|
| 67 |
-
"""
|
| 68 |
-
Returns the pedagogical boundary rules for a specific student profile.
|
| 69 |
-
|
| 70 |
-
Args:
|
| 71 |
-
grade_str: Student grade (e.g. "7", "ื", "ื\"ื")
|
| 72 |
-
level: Units level for high school (3, 4, 5)
|
| 73 |
-
topic: Current mathematical topic
|
| 74 |
-
|
| 75 |
-
Returns:
|
| 76 |
-
dict: { "allowed": [], "forbidden": [], "reason_map": {} }
|
| 77 |
-
"""
|
| 78 |
-
# 1. Normalize grade
|
| 79 |
-
from topic_taxonomy import _extract_grade_number
|
| 80 |
-
grade_num = _extract_grade_number(grade_str)
|
| 81 |
-
|
| 82 |
-
print(f"๐ [CURRICULUM] Fetching rules for Grade {grade_num}, Level {level} units")
|
| 83 |
-
|
| 84 |
-
rules = CURRICULUM_RULES.get(grade_num, {
|
| 85 |
-
"blacklist": [],
|
| 86 |
-
"whitelist": ["GENERAL"],
|
| 87 |
-
"descriptions": {}
|
| 88 |
-
})
|
| 89 |
-
|
| 90 |
-
# 2. Add Level-specific tweaks (Epic V4.1+)
|
| 91 |
-
if grade_num >= 10:
|
| 92 |
-
if level == "3":
|
| 93 |
-
rules["blacklist"].extend(["COMPLEX_ANALYSIS", "IMPLICIT_DERIVATIVE"])
|
| 94 |
-
elif level == "5":
|
| 95 |
-
rules["whitelist"].extend(["COMPLEX_ANALYSIS", "ADVANCED_CALCULUS"])
|
| 96 |
-
|
| 97 |
-
return {
|
| 98 |
-
"allowed": rules.get("whitelist", []),
|
| 99 |
-
"forbidden": rules.get("blacklist", []),
|
| 100 |
-
"reason_map": rules.get("descriptions", {})
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
def is_operator_allowed(operator: str, grade_str: str, level: str = "4") -> bool:
|
| 104 |
-
"""Helper to check a single operator."""
|
| 105 |
-
rules = get_allowed_math_operators(grade_str, level)
|
| 106 |
-
if operator in rules["forbidden"]:
|
| 107 |
-
return False
|
| 108 |
-
return True
|
| 109 |
-
|
| 110 |
-
if __name__ == "__main__":
|
| 111 |
-
# Test
|
| 112 |
-
print(get_allowed_math_operators("7"))
|
| 113 |
-
print(is_operator_allowed("DERIVATIVE", "7"))
|
| 114 |
-
print(is_operator_allowed("SYSTEM_OF_EQUATIONS", "7"))
|
| 115 |
-
print(is_operator_allowed("SYSTEM_OF_EQUATIONS", "8"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
curriculum_israel.py
DELETED
|
@@ -1,579 +0,0 @@
|
|
| 1 |
-
# curriculum_israel.py - ืชืืื ืืช ืืืืืืืื ืืืชืืืืงื ืืคื ืืฉืจื ืืืื ืื
|
| 2 |
-
# ====================================================================
|
| 3 |
-
# ืืืชืื ืืื ืืืชื ืืจืื (ื'-ื"ื) ืืคื ืชืืื ืืช ืืืืืืืื ืืืฉืจืืืืช
|
| 4 |
-
# ืื ืืชืืื ืืขืืจืืช!
|
| 5 |
-
|
| 6 |
-
"""
|
| 7 |
-
ืืื ื ืืขืจืืช ืืืื ืื ืืืฉืจืื - ืืชืืืืงื:
|
| 8 |
-
======================================
|
| 9 |
-
ืืืืืช ืืื ืืื (ื'-ื'): ืจืื ื' ืืฆืืืื ืื, ืจืื ื' ืจืืืื, ืจืื ื'
|
| 10 |
-
ืืืืื ืขืืืื ื (ื'-ื"ื): 3 ืื"ื, 4 ืื"ื, 5 ืื"ื
|
| 11 |
-
"""
|
| 12 |
-
|
| 13 |
-
# =============================================================================
|
| 14 |
-
# ืคืจืืคืืื ืืืชืืช - ืื ืืืืืื ืืื ืืืชื
|
| 15 |
-
# =============================================================================
|
| 16 |
-
|
| 17 |
-
GRADE_PROFILES = {
|
| 18 |
-
|
| 19 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 20 |
-
# ืืืชื ื'
|
| 21 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 22 |
-
"ื": {
|
| 23 |
-
"name": "ืืืชื ื'",
|
| 24 |
-
"age_range": "12-13",
|
| 25 |
-
"levels": {
|
| 26 |
-
"ื_ืืฆืืืื ืื": {
|
| 27 |
-
"name": "ืจืื ื' ืืฆืืืื ืื",
|
| 28 |
-
"topics": [
|
| 29 |
-
"ืืกืคืจืื ืฉืืืืืื ืืืืืืืื",
|
| 30 |
-
"ืืืงืืช ืืฉืืจืฉืื",
|
| 31 |
-
"ืืืืืืื ืืืืืจืืื",
|
| 32 |
-
"ืืฉืืืืืช ืืืขืื ืจืืฉืื ื",
|
| 33 |
-
"ืืืก ืืคืจืืคืืจืฆืื",
|
| 34 |
-
"ืืืืืื",
|
| 35 |
-
"ืืืืืืจืื: ืืืืืืช, ืืฉืืืฉืื, ืืจืืืขืื",
|
| 36 |
-
"ืฉืื ืืืืงืฃ",
|
| 37 |
-
"ื ืคื ืืฉืื ืคื ืื",
|
| 38 |
-
"ืกืืืืกืืืงื ืชืืืืจืืช",
|
| 39 |
-
],
|
| 40 |
-
"expected_level": "ืืชืงืื - ืฉืืืืช ืืืชืืจืืช, ืืฉืืื ืืชืืืืช",
|
| 41 |
-
"explanation_style": "ืืกืืจืื ืืขืืืงืื ืขื ืืจืืืืช",
|
| 42 |
-
},
|
| 43 |
-
"ื_ืจืืืื": {
|
| 44 |
-
"name": "ืจืื ื' ืจืืืื",
|
| 45 |
-
"topics": [
|
| 46 |
-
"ืืกืคืจืื ืฉืืืืืื ืืืืืืืื",
|
| 47 |
-
"ืืืงืืช ืืกืืกืืืช",
|
| 48 |
-
"ืืืืืืื ืืืืืจืืื ืคืฉืืืื",
|
| 49 |
-
"ืืฉืืืืืช ืืืขืื ืจืืฉืื ื",
|
| 50 |
-
"ืืืก ืืคืจืืคืืจืฆืื",
|
| 51 |
-
"ืืืืืื",
|
| 52 |
-
"ืืืืืืจืื ืืกืืกืืช",
|
| 53 |
-
"ืฉืื ืืืืงืฃ",
|
| 54 |
-
],
|
| 55 |
-
"expected_level": "ืกืื ืืจืื - ืื ืืืช ืืกืืก ืืืง",
|
| 56 |
-
"explanation_style": "ืืกืืจืื ืืจืืจืื ืขื ืืืืืืืช ืจืืืช",
|
| 57 |
-
},
|
| 58 |
-
"ื": {
|
| 59 |
-
"name": "ืจืื ื'",
|
| 60 |
-
"topics": [
|
| 61 |
-
"ืืกืคืจืื ืฉืืืื",
|
| 62 |
-
"ืคืขืืืืช ืืฉืืื ืืกืืกืืืช",
|
| 63 |
-
"ืฉืืจืื ืืืกืคืจืื ืขืฉืจืื ืืื",
|
| 64 |
-
"ืืืืืื ืคืฉืืืื",
|
| 65 |
-
"ืืืืืืจืื ืืกืืกืืช",
|
| 66 |
-
"ืืืืื",
|
| 67 |
-
],
|
| 68 |
-
"expected_level": "ืืกืืกื - ืืืืืง ืืกืืืืช",
|
| 69 |
-
"explanation_style": "ืืกืืจืื ืคืฉืืืื ืืืื, ืฆืขื ืืืจ ืฆืขื, ืขื ืืจืื ืขืืืื",
|
| 70 |
-
},
|
| 71 |
-
},
|
| 72 |
-
},
|
| 73 |
-
|
| 74 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 75 |
-
# ืืืชื ื'
|
| 76 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 77 |
-
"ื": {
|
| 78 |
-
"name": "ืืืชื ื'",
|
| 79 |
-
"age_range": "13-14",
|
| 80 |
-
"levels": {
|
| 81 |
-
"ื_ืืฆืืืื ืื": {
|
| 82 |
-
"name": "ืจืื ื' ืืฆืืืื ืื",
|
| 83 |
-
"topics": [
|
| 84 |
-
"ืคืื ืงืฆืื ืืื ืืจืืช",
|
| 85 |
-
"ืืขืจืืช ืืฉืืืืืช",
|
| 86 |
-
"ืื-ืฉืืืืื ืืช",
|
| 87 |
-
"ืืฉืืืฉืื: ืืคืืคื ืืืืืื",
|
| 88 |
-
"ืืฉืคื ืคืืชืืืจืก",
|
| 89 |
-
"ืืขืื",
|
| 90 |
-
"ืืกืชืืจืืช",
|
| 91 |
-
"ืกืืืืกืืืงื",
|
| 92 |
-
],
|
| 93 |
-
"expected_level": "ืืชืงืื - ืืื ื ืืชืืืื ืืจืื ืืืืื",
|
| 94 |
-
"explanation_style": "ืืกืืจืื ืืขืืืงืื ืขื ืงืืฉืืจืื ืืื ื ืืฉืืื",
|
| 95 |
-
},
|
| 96 |
-
"ื_ืจืืืื": {
|
| 97 |
-
"name": "ืจืื ื' ืจืืืื",
|
| 98 |
-
"topics": [
|
| 99 |
-
"ืคืื ืงืฆืื ืืื ืืจืืช",
|
| 100 |
-
"ืืขืจืืช ืืฉืืืืืช ืคืฉืืื",
|
| 101 |
-
"ืื-ืฉืืืืื ืืช ืคืฉืืืื",
|
| 102 |
-
"ืืฉืืืฉืื ืืืคืืคื",
|
| 103 |
-
"ืืฉืคื ืคืืชืืืจืก",
|
| 104 |
-
"ืืกืชืืจืืช ืืกืืกืืช",
|
| 105 |
-
],
|
| 106 |
-
"expected_level": "ืกืื ืืจืื",
|
| 107 |
-
"explanation_style": "ืืกืืจืื ืืจืืจืื ืืืกืืืจืื",
|
| 108 |
-
},
|
| 109 |
-
"ื": {
|
| 110 |
-
"name": "ืจืื ื'",
|
| 111 |
-
"topics": [
|
| 112 |
-
"ืืฉืืืืืช ืคืฉืืืืช",
|
| 113 |
-
"ืืจืคืื ืืกืืกืืื",
|
| 114 |
-
"ืืืืืืจืื: ืืฉืืืฉืื ืืืจืืืขืื",
|
| 115 |
-
"ืืืืื ืืฉืืืื",
|
| 116 |
-
"ืืกืชืืจืืช ืคืฉืืื",
|
| 117 |
-
],
|
| 118 |
-
"expected_level": "ืืกืืกื",
|
| 119 |
-
"explanation_style": "ืืกืืจืื ืคืฉืืืื ืขื ืืืืืืืช ืืืื ืืืืืืื",
|
| 120 |
-
},
|
| 121 |
-
},
|
| 122 |
-
},
|
| 123 |
-
|
| 124 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 125 |
-
# ืืืชื ื'
|
| 126 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 127 |
-
"ื": {
|
| 128 |
-
"name": "ืืืชื ื'",
|
| 129 |
-
"age_range": "14-15",
|
| 130 |
-
"levels": {
|
| 131 |
-
"ื_ืืฆืืืื ืื": {
|
| 132 |
-
"name": "ืจืื ื' ืืฆืืืื ืื",
|
| 133 |
-
"topics": [
|
| 134 |
-
"ืคืื ืงืฆืื ืจืืืืขืืช",
|
| 135 |
-
"ืืฉืืืื ืจืืืืขืืช ืื ืืกืืช ืืฉืืจืฉืื",
|
| 136 |
-
"ืื-ืฉืืืืื ืจืืืืขื",
|
| 137 |
-
"ืืืืื ืืฉืืืฉืื ืืชืงืื",
|
| 138 |
-
"ืืฉืคื ืชืืืก",
|
| 139 |
-
"ืืจืืืื ืืืืจืื ืืืฉืืืฉ ืืฉืจ-ืืืืืช",
|
| 140 |
-
"ืืขืื: ืืืืืืช ืืืงืคืืืช ืืืจืืืืืช",
|
| 141 |
-
"ืืกืชืืจืืช ืืชืงืืืช",
|
| 142 |
-
],
|
| 143 |
-
"expected_level": "ืืชืงืื - ืืื ื ื-5 ืื\"ื",
|
| 144 |
-
"explanation_style": "ืืกืืจืื ืืขืืืงืื ืขื ืืืืืืช",
|
| 145 |
-
},
|
| 146 |
-
"ื_ืจืืืื": {
|
| 147 |
-
"name": "ืจืื ื' ืจืืืื",
|
| 148 |
-
"topics": [
|
| 149 |
-
"ืคืื ืงืฆืื ืจืืืืขืืช",
|
| 150 |
-
"ืืฉืืืื ืจืืืืขืืช",
|
| 151 |
-
"ืืืืื ืืฉืืืฉืื",
|
| 152 |
-
"ืืจืืืื ืืืืจืื ืืกืืกืืช",
|
| 153 |
-
"ืืขืื",
|
| 154 |
-
],
|
| 155 |
-
"expected_level": "ืกืื ืืจืื - ืืื ื ื-4 ืื\"ื",
|
| 156 |
-
"explanation_style": "ืืกืืจืื ืืกืืืจืื ืขื ืชืจืืื",
|
| 157 |
-
},
|
| 158 |
-
"ื": {
|
| 159 |
-
"name": "ืจืื ื'",
|
| 160 |
-
"topics": [
|
| 161 |
-
"ืคืื ืงืฆืื ืืื ืืจืืช",
|
| 162 |
-
"ืืฉืืืืืช ืคืฉืืืืช",
|
| 163 |
-
"ืืืืืืจืื ืืกืืกืืช",
|
| 164 |
-
"ืืกืชืืจืืช ืคืฉืืื",
|
| 165 |
-
],
|
| 166 |
-
"expected_level": "ืืกืืกื - ืืื ื ื-3 ืื\"ื",
|
| 167 |
-
"explanation_style": "ืืกืืจืื ืคืฉืืืื ืืืขืืืืื",
|
| 168 |
-
},
|
| 169 |
-
},
|
| 170 |
-
},
|
| 171 |
-
|
| 172 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 173 |
-
# ืืืชื ื' - ืชืืืืช ืืืืืื ืืืจืืช
|
| 174 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 175 |
-
"ื": {
|
| 176 |
-
"name": "ืืืชื ื'",
|
| 177 |
-
"age_range": "15-16",
|
| 178 |
-
"levels": {
|
| 179 |
-
"3_ืืืืืืช": {
|
| 180 |
-
"name": "3 ืืืืืืช ืืืืื",
|
| 181 |
-
"topics": [
|
| 182 |
-
"ืืืืืจื: ืืฉืืืืืช ืืื-ืฉืืืืื ืืช",
|
| 183 |
-
"ืคืื ืงืฆืืืช: ืืื ืืจืืช ืืจืืืืขืืช",
|
| 184 |
-
"ืืืืืืจืื ืื ืืืืืช ืืกืืกืืช",
|
| 185 |
-
"ืืจืืืื ืืืืจืื ืืืฉืืืฉ",
|
| 186 |
-
"ืกืืืืกืืืงื ืืืกืชืืจืืช",
|
| 187 |
-
],
|
| 188 |
-
"bagrut_format": True,
|
| 189 |
-
"expected_level": "ืืืจืืช 3 ืื\"ื - ืืืฉ ืขื ืืืฉืืืื",
|
| 190 |
-
"explanation_style": "ืืกืืจืื ืืจืืจืื ืืคืืจืื ืืืจืืช, ืืืฉ ืขื ืฉืืืืช ืืืืืืืืช",
|
| 191 |
-
},
|
| 192 |
-
"4_ืืืืืืช": {
|
| 193 |
-
"name": "4 ืืืืืืช ืืืืื",
|
| 194 |
-
"topics": [
|
| 195 |
-
"ืืืืืจื ืืชืงื๏ฟฝ๏ฟฝืช",
|
| 196 |
-
"ืคืื ืงืฆืืืช: ืจืืืืขืืช, ืฉืืจืฉ, ืขืจื ืืืืื",
|
| 197 |
-
"ืืืืืืจืื ืื ืืืืืช",
|
| 198 |
-
"ืืจืืืื ืืืืจืื: ืืฉืคืื ืกืื ืืกืื ืืงืืกืื ืืกืื",
|
| 199 |
-
"ืกืืจืืช ืืฉืืื ืืืช ืืื ืืกืืืช",
|
| 200 |
-
"ืืกืชืืจืืช",
|
| 201 |
-
],
|
| 202 |
-
"bagrut_format": True,
|
| 203 |
-
"expected_level": "ืืืจืืช 4 ืื\"ื - ืจืื ืืื ืื ืืช-ืืืืื",
|
| 204 |
-
"explanation_style": "ืืกืืจืื ืืคืืจืืื ืืคืืจืื ืืืจืืช ืขื ื ืืืืงืื",
|
| 205 |
-
},
|
| 206 |
-
"5_ืืืืืืช": {
|
| 207 |
-
"name": "5 ืืืืืืช ืืืืื",
|
| 208 |
-
"topics": [
|
| 209 |
-
"ืืืืืจื ืืชืงืืืช",
|
| 210 |
-
"ืคืื ืงืฆืืืช ืืืจืืืืช",
|
| 211 |
-
"ืืืืืืจืื ืื ืืืืืช ืืชืงืืืช",
|
| 212 |
-
"ืืจืืืื ืืืืจืื ืืชืงืืืช",
|
| 213 |
-
"ืกืืจืืช ืืืืืืืช",
|
| 214 |
-
"ืืืื ืืืฉืืื ืืืคืจื ืฆืืืื",
|
| 215 |
-
],
|
| 216 |
-
"bagrut_format": True,
|
| 217 |
-
"expected_level": "ืืืจืืช 5 ืื\"ื - ืจืื ืืืืื",
|
| 218 |
-
"explanation_style": "ืืกืืจืื ืืขืืืงืื ืขื ืืืืืืช ืื ืืืืงืื ืืืืื",
|
| 219 |
-
},
|
| 220 |
-
},
|
| 221 |
-
},
|
| 222 |
-
|
| 223 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 224 |
-
# ืืืชื ื"ื
|
| 225 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 226 |
-
"ืื": {
|
| 227 |
-
"name": "ืืืชื ื\"ื",
|
| 228 |
-
"age_range": "16-17",
|
| 229 |
-
"levels": {
|
| 230 |
-
"3_ืืืืืืช": {
|
| 231 |
-
"name": "3 ืืืืืืช ืืืืื",
|
| 232 |
-
"topics": [
|
| 233 |
-
"ืืืจื ืืืขืืงื ืืืืืืจื",
|
| 234 |
-
"ืคืื ืงืฆืืืช ืืืืฉืืืื",
|
| 235 |
-
"ืกืืืืกืืืงื ืืชืงืืืช",
|
| 236 |
-
"ืืกืชืืจืืช",
|
| 237 |
-
"ืืืืืืจืื ืืืืฉืืจ",
|
| 238 |
-
],
|
| 239 |
-
"bagrut_format": True,
|
| 240 |
-
"expected_level": "ืืื ื ืืืืจืืช 3 ืื\"ื",
|
| 241 |
-
"explanation_style": "ืชืจืืื ืืืจืืืืช ืขื ืืกืืจืื",
|
| 242 |
-
},
|
| 243 |
-
"4_ืืืืืืช": {
|
| 244 |
-
"name": "4 ืืืืืืช ืืืืื",
|
| 245 |
-
"topics": [
|
| 246 |
-
"ืืื\"ื: ื ืืืจืืช",
|
| 247 |
-
"ืืงืืจืช ืคืื ืงืฆืืืช ืคืืืื ืืืืืืืืช",
|
| 248 |
-
"ืืืืืืจืื ืื ืืืืืช: ืืฉืจ ืืืขืื",
|
| 249 |
-
"ืืจืืืื ืืืืจืื ืืชืงืืืช",
|
| 250 |
-
"ืกืืจืืช",
|
| 251 |
-
"ืืกืชืืจืืช ืืืชื ืืช",
|
| 252 |
-
],
|
| 253 |
-
"bagrut_format": True,
|
| 254 |
-
"expected_level": "ืืืจืืช 4 ืื\"ื",
|
| 255 |
-
"explanation_style": "ืคืืจืื ืืืจืืช ืืื ืขื ืื ืื ืืืืงืื",
|
| 256 |
-
},
|
| 257 |
-
"5_ืืืืืืช": {
|
| 258 |
-
"name": "5 ืืืืืืช ืืืืื",
|
| 259 |
-
"topics": [
|
| 260 |
-
"ืืื\"ื: ื ืืืจืืช ืืืื ืืืจืืื",
|
| 261 |
-
"ืืงืืจืช ืคืื ืงืฆืืืช ืืืจืืืืช",
|
| 262 |
-
"ืืืืืืจืื ืื ืืืืืช ืืชืงืืืช",
|
| 263 |
-
"ืืกืคืจืื ืืจืืืืื",
|
| 264 |
-
"ืืกืชืืจืืช ืืชืงืืืช",
|
| 265 |
-
"ืืื ืืืงืฆืื",
|
| 266 |
-
],
|
| 267 |
-
"bagrut_format": True,
|
| 268 |
-
"expected_level": "ืืืจืืช 5 ืื\"ื - ืจืื ืืืืื",
|
| 269 |
-
"explanation_style": "ืืืืืืช ืืืืืช, ื ืืืืงืื ืคืืจืืืืื",
|
| 270 |
-
},
|
| 271 |
-
},
|
| 272 |
-
},
|
| 273 |
-
|
| 274 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 275 |
-
# ืืืชื ื"ื
|
| 276 |
-
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 277 |
-
"ืื": {
|
| 278 |
-
"name": "ืืืชื ื\"ื",
|
| 279 |
-
"age_range": "17-18",
|
| 280 |
-
"levels": {
|
| 281 |
-
"3_ืืืืืืช": {
|
| 282 |
-
"name": "3 ืืืืืืช ืืืืื",
|
| 283 |
-
"topics": [
|
| 284 |
-
"ืืืจื ืืงืืคื ืืืืจืืช",
|
| 285 |
-
"ืคืชืจืื ืืืจืืืืช ืงืืืืืช",
|
| 286 |
-
"ืื ื ืืฉืื 3 ืื\"ื",
|
| 287 |
-
],
|
| 288 |
-
"bagrut_format": True,
|
| 289 |
-
"expected_level": "ืืื ื ืืื ืื ืกืืืืช ืืืืจืืช",
|
| 290 |
-
"explanation_style": "ืคืชืจืื ืืืจืืืืช ืขื ืืืคืื ืืื๏ฟฝ๏ฟฝื",
|
| 291 |
-
},
|
| 292 |
-
"4_ืืืืืืช": {
|
| 293 |
-
"name": "4 ืืืืืืช ืืืืื",
|
| 294 |
-
"topics": [
|
| 295 |
-
"ืืืจื ืืงืืคื ืืืืจืืช",
|
| 296 |
-
"ืืื\"ื: ืืื ืืืจืืื ืืฉืืืื",
|
| 297 |
-
"ืืืืืืจืื ืื ืืืืืช ืืชืงืืืช",
|
| 298 |
-
"ืคืชืจืื ืืืจืืืืช",
|
| 299 |
-
],
|
| 300 |
-
"bagrut_format": True,
|
| 301 |
-
"expected_level": "ืืื ื ืืื ืื ืกืืืืช ืืืืจืืช 4 ืื\"ื",
|
| 302 |
-
"explanation_style": "ืคืชืจืื ืืืื ืื ืขื ืืกืืจืืืืืช",
|
| 303 |
-
},
|
| 304 |
-
"5_ืืืืืืช": {
|
| 305 |
-
"name": "5 ืืืืืืช ืืืืื",
|
| 306 |
-
"topics": [
|
| 307 |
-
"ืืื\"ื ืืชืงืื",
|
| 308 |
-
"ืืื ืืืจืืื ืืืืฉืืืื",
|
| 309 |
-
"ืืฉืืืืืช ืืืคืจื ืฆืืืืืืช ืคืฉืืืืช",
|
| 310 |
-
"ืืืืืืจืื ืื ืืืืืช: ืืืืคืกื ืืืืคืจืืืื",
|
| 311 |
-
"ืืกืคืจืื ืืจืืืืื ืืชืงืื",
|
| 312 |
-
"ืืงืืืจืื",
|
| 313 |
-
"ืืื ื ืืืืจืืช",
|
| 314 |
-
],
|
| 315 |
-
"bagrut_format": True,
|
| 316 |
-
"expected_level": "ืืื ื ืืื ืื ืกืืืืช ืืืืจืืช 5 ืื\"ื",
|
| 317 |
-
"explanation_style": "ืคืชืจืื ืืื ืขื ืื ืืืืืืืช ืืื ืืืืงืื",
|
| 318 |
-
},
|
| 319 |
-
},
|
| 320 |
-
},
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
# =============================================================================
|
| 325 |
-
# ืืืื ืืืจืืช ืืคื ื ืืฉื (ืืชืื ืืกืื ืืฉืจื ืืืื ืื)
|
| 326 |
-
# =============================================================================
|
| 327 |
-
|
| 328 |
-
BAGRUT_RULES = {
|
| 329 |
-
"ืืืืืืจืื": {
|
| 330 |
-
"ืืืื": [
|
| 331 |
-
"ืื ืืง ืืืืืจืืช ืืืฉืคืืื ืืืืืืจืืื ืืืื",
|
| 332 |
-
"ืืฆืืื ืืช ืืืฉืืืฉ/ืืจืืืข ืฉืืืื ืืชืืืืกืื",
|
| 333 |
-
"ืืืืืืจ ืืืืืช ื-3 ืืืชืืืช ืื ืืืช ืืืช + ืฉื ืืืฉืืืฉ",
|
| 334 |
-
],
|
| 335 |
-
"ืื_ื ืืจืฉ": [
|
| 336 |
-
"'ืื ืืืื ืฉืืื ืืขืฆืื'",
|
| 337 |
-
"'ืืื ืืืืืืจ'",
|
| 338 |
-
"'ืืื ืืืืกืืจ'",
|
| 339 |
-
"ืืจืฉืื ืฉืงืืขืื ืืงืืืืื ืื ืืชืื ื ืฉืืฉืจืื ืืงืืืืื",
|
| 340 |
-
],
|
| 341 |
-
"ื ืืืืงืื_ื ืคืืฆืื": [
|
| 342 |
-
"ื ืชืื",
|
| 343 |
-
"ืืืืืืช ืืชืืืคืืช ืืื ืืฉืจืื ืืงืืืืื",
|
| 344 |
-
"ืืืืืืช ืื-ืฆืืืืืช ืืฉืืืืืช ื-180ยฐ",
|
| 345 |
-
"ืืืืืืช ืงืืืงืืืืืช ืฉืืืช",
|
| 346 |
-
"ืกืืื ืืืืืืช ืืืฉืืืฉ = 180ยฐ",
|
| 347 |
-
"ืืฉืคื ืคืืชืืืจืก",
|
| 348 |
-
"ืืฉืคื ืชืืืก",
|
| 349 |
-
"ืืคืืคืช ืืฉืืืฉืื (ืฆ.ื.ืฆ / ื.ืฆ.ื / ืฆ.ืฆ.ืฆ)",
|
| 350 |
-
"ืืืืื ืืฉืืืฉืื (ื.ื / ืฆ.ื.ืฆ / ืฆ.ืฆ.ืฆ)",
|
| 351 |
-
],
|
| 352 |
-
},
|
| 353 |
-
|
| 354 |
-
"ืืืื": {
|
| 355 |
-
"ืืืื": [
|
| 356 |
-
"ืชืืื ืืืืจื: ืืืชืื ืื-ืฉืืืืื ื ืืื (ืืืง โ ืืืฉ)",
|
| 357 |
-
"ืืืื ืืืจื: ืืืชืื ืคืื ืงืฆืื ืงืืืื + ืืฆืืช ืืืืืืช",
|
| 358 |
-
"ืืืชืื dx ืืกืืืจืืื ืืืงืื ืื ืืื",
|
| 359 |
-
],
|
| 360 |
-
"ืื_ื ืืจืฉ": [
|
| 361 |
-
"ืืคืจื ืืืฉืืื ืืืืืืช ืืืกืืืคืืืืืช",
|
| 362 |
-
"ืืคืจื ืืฆืืืช ืืงืืืขืช ืกืื ืงืืฆืื",
|
| 363 |
-
],
|
| 364 |
-
"ืืขืืืืช_ืงืจืืืืืช": [
|
| 365 |
-
"ืชืืื ืืืืจื ืขื ืืืืื ืื-ืฉืืืืื ืฉืืื = 0 ื ืงืืืืช",
|
| 366 |
-
"ืืื ืืืจื ืืื ืคืื ืงืฆืื ืงืืืื = 0 ื ืงืืืืช",
|
| 367 |
-
],
|
| 368 |
-
},
|
| 369 |
-
|
| 370 |
-
"ืืกืชืืจืืช": {
|
| 371 |
-
"ืืืื": [
|
| 372 |
-
"ืืืืืืจ ืื ืืืืจืข (A, B) ืืืืืื",
|
| 373 |
-
"ืืืกืชืืจืืช ืืืชื ืืช: ืืจืฉืื P(B|A) ืืื ืืืฆืื ืื ืืกืื",
|
| 374 |
-
"ืืืจืืืช ืื ืืืฉืื",
|
| 375 |
-
],
|
| 376 |
-
},
|
| 377 |
-
|
| 378 |
-
"ืืืืืจื": {
|
| 379 |
-
"ืืืื": [
|
| 380 |
-
"ืืืจืืืช ืืจื ืคืชืจืื ืืืื",
|
| 381 |
-
"ืชืฉืืื ืกืืคืืช ืืื ืืจื = ืื ื ืืืง",
|
| 382 |
-
"ืืืฉืืืื ืจืืืืขืืช: ืืืจืืืช ื ืืกืื ืื ืคืืจืืง",
|
| 383 |
-
],
|
| 384 |
-
},
|
| 385 |
-
|
| 386 |
-
"ืืื ืืืงืฆืื": {
|
| 387 |
-
"ืืืื": [
|
| 388 |
-
"ืืฉืคืื ืงืืฉืืจ: 'ื ืืืืง ืขืืืจ n=1', 'ื ื ืื ืขืืืจ n=k', 'ื ืืืื ืขืืืจ n=k+1'",
|
| 389 |
-
"ืืจืฉืื ื ืืื ืื ืฆืจืื ืืืืืื",
|
| 390 |
-
],
|
| 391 |
-
"ืืขืืืืช_ืงืจืืืืืช": [
|
| 392 |
-
"'ื ื ืื ืืื n ืืืขื' = ืืืจืื ืฉื 20%",
|
| 393 |
-
"ืจืืฉืื ืฉืืื ืฉื ืื ืฉืฆืจืื ืืืืืื = ืื ืืืืงืื ืืืฉื",
|
| 394 |
-
],
|
| 395 |
-
},
|
| 396 |
-
|
| 397 |
-
"ืืจืืืื ืืืืจืื": {
|
| 398 |
-
"4_ืืืืืืช": [
|
| 399 |
-
"ืืคืฉื sin(180ยฐ-x) ืืืืืื",
|
| 400 |
-
],
|
| 401 |
-
"5_ืืืืืืช": [
|
| 402 |
-
"ืืขืืื ืืจืืืื ืื ืืืื (ืืขืืืช = 0 ื ืงืืืืช)",
|
| 403 |
-
],
|
| 404 |
-
},
|
| 405 |
-
|
| 406 |
-
"ืกืืจืืช": {
|
| 407 |
-
"ืืืื": [
|
| 408 |
-
"ืืจืฉืื ืืช ืกืื ืืกืืจื (ืืฉืืื ืืช/ืื ืืกืืช) ืืืช ืื ืืกืื ืืืืืืช",
|
| 409 |
-
"ืืกืืื ืืืงื: ืืจืฉืื ืืช ืื ืืกืื ืืืืฆืื",
|
| 410 |
-
"ืืืืืืช ื ืืกืื: ืืืฉืชืืฉ ืืืื ืืืงืฆืื ืืืื",
|
| 411 |
-
],
|
| 412 |
-
},
|
| 413 |
-
|
| 414 |
-
"ืืกืคืจืื_ืืจืืืืื": {
|
| 415 |
-
"ืืืื": [
|
| 416 |
-
"ืืจืฉืื z ืืฆืืจื ืืืืืจืืช (a+bi) ืืื ืืฆืืจื ืืจืืืื ืืืืจืืช ืื ื ืืจืฉ",
|
| 417 |
-
"ืืืจืืืช ืืืฉืื ืืืืืืืก ืืืจืืืื ื",
|
| 418 |
-
"ืืฉืืจืฉืื: ืืืฉืชืืฉ ืื ืืกืืช ืื-ืืืืืจ ืืืจืฉืื ืืช ืื ืืฉืืจืฉืื",
|
| 419 |
-
],
|
| 420 |
-
},
|
| 421 |
-
|
| 422 |
-
"ืืงืืืจืื": {
|
| 423 |
-
"ืืืื": [
|
| 424 |
-
"ืืจืฉืื ืืงืืืจ ืืฆืื ืืืกืืจ",
|
| 425 |
-
"ืืืืคืื ืกืงืืจืืช: ืืืจืืืช ืืืฉืื ืืฉืชื ืืจืืื (ืงืืืืจืืื ืืืช ืืืืืืช)",
|
| 426 |
-
"ืืืืืื ื ืืฆืืืช ืืจื ืืืคืื ืกืงืืจืืช = 0",
|
| 427 |
-
],
|
| 428 |
-
},
|
| 429 |
-
|
| 430 |
-
"ืกืืืืกืืืงื": {
|
| 431 |
-
"ืืืื": [
|
| 432 |
-
"ืืจืฉืื ืืช ื ืืกืืช ืืืืืฆืข/ืฉืื ืืช/ืกืืืืช ืชืงื",
|
| 433 |
-
"ืืืชืคืืืืช ื ืืจืืืืช: ืืฆืืื Z-score ืืืืฉืชืืฉ ืืืืืืืช",
|
| 434 |
-
"ืืืชืคืืืืช ืืื ืืืืช: ืืจืฉืื n, p, ืืืืฆืื ืื ืืกืื",
|
| 435 |
-
],
|
| 436 |
-
},
|
| 437 |
-
}
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
# =============================================================================
|
| 441 |
-
# ืกืื ืื ืืช ืืกืืจ ืืคื ืืื
|
| 442 |
-
# =============================================================================
|
| 443 |
-
|
| 444 |
-
EXPLANATION_STYLES = {
|
| 445 |
-
"ืืืืืช_ืืื ืืื": {
|
| 446 |
-
"ืฉืคื": "ืคืฉืืื ืื ืืืฉื",
|
| 447 |
-
"ืืืืืืืช": "ืืืื ืืืืืืื",
|
| 448 |
-
"ืขืืืื": "ืืจืื ืืืืืงืื ืืืืืืื",
|
| 449 |
-
"ืงืฆื": "ืืืื ืืืคืืจื",
|
| 450 |
-
"ืืืืื'ืื": "ืื, ืืืืืช ืืชืื ื",
|
| 451 |
-
},
|
| 452 |
-
"ืืืืื_ืขืืืื ื_3_ืืืืืืช": {
|
| 453 |
-
"ืฉืคื": "ืืจืืจื ืขื ืืื ืืื ืืชืืืืื ืืกืืกืืื",
|
| 454 |
-
"ืืืืืืืช": "ืืืฉืืืืืช",
|
| 455 |
-
"ืขืืืื": "ืืชืื",
|
| 456 |
-
"ืงืฆื": "ืจืืื",
|
| 457 |
-
"ืคืืจืื": "ืืืจืืช",
|
| 458 |
-
},
|
| 459 |
-
"ืืืืื_ืขืืืื ื_4_ืืืืืืช": {
|
| 460 |
-
"ืฉืคื": "ืืชืืืืช ืขื ืืกืืจืื",
|
| 461 |
-
"ืืืืืืืช": "ืืชืืืืืช",
|
| 462 |
-
"ืขืืืื": "ืืืืงื",
|
| 463 |
-
"ืงืฆื": "ืจืืื-ืืืืจ",
|
| 464 |
-
"ืคืืจืื": "ืืืจืืช ืืื",
|
| 465 |
-
},
|
| 466 |
-
"ืืืืื_ืขืืืื ื_5_ืืืืืืช": {
|
| 467 |
-
"ืฉืคื": "ืืชืืืืช ืคืืจืืืืช",
|
| 468 |
-
"ืืืืืืืช": "ืืืืืืช ืื ืืืืงืื",
|
| 469 |
-
"ืขืืืื": "ืืื ืืืื",
|
| 470 |
-
"ืงืฆื": "ืืืืจ",
|
| 471 |
-
"ืคืืจืื": "ืืืจืืช ืืื ืขื ืืืืืืช",
|
| 472 |
-
},
|
| 473 |
-
}
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
# =============================================================================
|
| 477 |
-
# ืคืื ืงืฆืืืช ืขืืจ
|
| 478 |
-
# =============================================================================
|
| 479 |
-
|
| 480 |
-
def get_grade_profile(grade: str, level: str = None) -> dict:
|
| 481 |
-
"""ืืืืืจ ืคืจืืคืื ืืืชื ืืจืื"""
|
| 482 |
-
|
| 483 |
-
# ื ืจืืื ืฉื ืืืืชื
|
| 484 |
-
grade_map = {
|
| 485 |
-
"ื'": "ื", "ื": "ื", "7": "ื",
|
| 486 |
-
"ื'": "ื", "ื": "ื", "8": "ื",
|
| 487 |
-
"ื'": "ื", "ื": "ื", "9": "ื",
|
| 488 |
-
"ื'": "ื", "ื": "ื", "10": "ื",
|
| 489 |
-
"ื\"ื": "ืื", "ืื": "ืื", "11": "ืื",
|
| 490 |
-
"ื\"ื": "ืื", "ืื": "ืื", "12": "ืื",
|
| 491 |
-
}
|
| 492 |
-
|
| 493 |
-
grade_key = grade_map.get(grade, "ื")
|
| 494 |
-
profile = GRADE_PROFILES.get(grade_key, GRADE_PROFILES["ื"])
|
| 495 |
-
|
| 496 |
-
# ื ืจืืื ืจืื
|
| 497 |
-
if level:
|
| 498 |
-
level_map = {
|
| 499 |
-
"3 ืื\"ื": "3_ืืืืืืช", "3 ืืืืืืช": "3_ืืืืืืช", "3": "3_ืืืืืืช",
|
| 500 |
-
"4 ืื\"ื": "4_ืืืืืืช", "4 ืืืืืืช": "4_ืืืืืืช", "4": "4_ืืืืืืช",
|
| 501 |
-
"5 ืื\"ื": "5_ืืืืืืช", "5 ืืืืืืช": "5_ืืืืืืช", "5": "5_ืืืืืืช",
|
| 502 |
-
"ืจืื ื' ืืฆืืืื ืื": "ื_ืืฆืืืื ืื", "ืืฆืืืื ืื": "ื_ืืฆืืืื ืื",
|
| 503 |
-
"ืจืื ื' ืจืืืื": "ื_ืจืืืื", "ืจืืืื": "ื_ืจืืืื",
|
| 504 |
-
"ืจืื ื'": "ื", "ื": "ื",
|
| 505 |
-
}
|
| 506 |
-
level_key = level_map.get(level, list(profile["levels"].keys())[0])
|
| 507 |
-
level_profile = profile["levels"].get(level_key, list(profile["levels"].values())[0])
|
| 508 |
-
else:
|
| 509 |
-
level_profile = list(profile["levels"].values())[0]
|
| 510 |
-
|
| 511 |
-
return {
|
| 512 |
-
"grade": profile,
|
| 513 |
-
"level": level_profile,
|
| 514 |
-
}
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
def get_explanation_style(grade: str, level: str = None) -> dict:
|
| 518 |
-
"""ืืืืืจ ืกืื ืื ืืกืืจ ืืืชืื"""
|
| 519 |
-
|
| 520 |
-
profile = get_grade_profile(grade, level)
|
| 521 |
-
|
| 522 |
-
# ืืืืื ืื ืืืืืช ืืื ืืื ืื ืขืืืื ื
|
| 523 |
-
if grade in ["ื", "ื'", "ื", "ื'", "ื", "ื'", "7", "8", "9"]:
|
| 524 |
-
return EXPLANATION_STYLES["ืืืืืช_ืืื ืืื"]
|
| 525 |
-
|
| 526 |
-
# ืืืืื ืขืืืื ื - ืืคื ืืืืืืช
|
| 527 |
-
level_name = profile["level"].get("name", "")
|
| 528 |
-
if "3 ืืืืืืช" in level_name:
|
| 529 |
-
return EXPLANATION_STYLES["ืืืืื_ืขืืืื ื_3_ืืืืืืช"]
|
| 530 |
-
elif "4 ืืืืืืช" in level_name:
|
| 531 |
-
return EXPLANATION_STYLES["ืืืืื_ืขืืืื ื_4_ืืืืืืช"]
|
| 532 |
-
else:
|
| 533 |
-
return EXPLANATION_STYLES["ืืืืื_ืขืืืื ื_5_ืืืืืืช"]
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
def get_bagrut_rules(topic: str) -> dict:
|
| 537 |
-
"""ืืืืืจ ืืืื ืืืจืืช ืื ืืฉื ืืกืืื"""
|
| 538 |
-
|
| 539 |
-
topic_map = {
|
| 540 |
-
"ืืืืืืจืื": "ืืืืืืจืื",
|
| 541 |
-
"geometry": "ืืืืืืจืื",
|
| 542 |
-
"ืืืื": "ืืืื",
|
| 543 |
-
"calculus": "ืืืื",
|
| 544 |
-
"ืืงืืจื": "ืืืื",
|
| 545 |
-
"ื ืืืจืช": "ืืืื",
|
| 546 |
-
"ืืื ืืืจื": "ืืืื",
|
| 547 |
-
"ืืกืชืืจืืช": "ืืกืชืืจืืช",
|
| 548 |
-
"probability": "ืืกืชืืจืืช",
|
| 549 |
-
"ืืืืืจื": "ืืืืืจื",
|
| 550 |
-
"algebra": "ืืืืืจื",
|
| 551 |
-
"ืืฉืืืื": "ืืืืืจื",
|
| 552 |
-
"ืืื ืืืงืฆืื": "ืืื ืืืงืฆืื",
|
| 553 |
-
"induction": "ืืื ืืืงืฆืื",
|
| 554 |
-
"ืืจืืืื ืืืืจืื": "ืืจืืืื ืืืืจืื",
|
| 555 |
-
"trigonometry": "ืืจืืืื ืืืืจืื",
|
| 556 |
-
"ืกืืจื": "ืกืืจืืช",
|
| 557 |
-
"ืกืืจืืช": "ืกืืจืืช",
|
| 558 |
-
"sequence": "ืกืืจืืช",
|
| 559 |
-
"ืืจืืื": "ืืกืคืจืื_ืืจืืืืื",
|
| 560 |
-
"complex": "ืืกืคืจืื_ืืจืืืืื",
|
| 561 |
-
"ืืงืืืจ": "ืืงืืืจืื",
|
| 562 |
-
"vector": "ืืงืืืจืื",
|
| 563 |
-
"ืกืืืืกืืืงื": "ืกืืืืกืืืงื",
|
| 564 |
-
"statistics": "ืกืืืืกืืืงื",
|
| 565 |
-
"ืืืืฆืข": "ืกืืืืกืืืงื",
|
| 566 |
-
"ืืชืคืืืืช": "ืกืืืืกืืืงื",
|
| 567 |
-
}
|
| 568 |
-
|
| 569 |
-
for key, value in topic_map.items():
|
| 570 |
-
if key in topic.lower():
|
| 571 |
-
return BAGRUT_RULES.get(value, {})
|
| 572 |
-
|
| 573 |
-
return {}
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
def is_bagrut_level(grade: str, level: str = None) -> bool:
|
| 577 |
-
"""ืืืืง ืื ืื ืจืืช ืืืจืืช"""
|
| 578 |
-
profile = get_grade_profile(grade, level)
|
| 579 |
-
return profile["level"].get("bagrut_format", False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
debug_llm_payload.py
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
import asyncio
|
| 2 |
-
import os
|
| 3 |
-
import sys
|
| 4 |
-
|
| 5 |
-
# Set encoding for Windows console (just in case)
|
| 6 |
-
if sys.platform == 'win32':
|
| 7 |
-
sys.stdout.reconfigure(encoding='utf-8')
|
| 8 |
-
sys.stderr.reconfigure(encoding='utf-8')
|
| 9 |
-
|
| 10 |
-
from orchestrator import BuddyOrchestrator
|
| 11 |
-
from domain.processing_strategy import ProcessingStrategy
|
| 12 |
-
from smart_solver import SmartSolver
|
| 13 |
-
|
| 14 |
-
async def debug_orchestrator():
|
| 15 |
-
orchestrator = BuddyOrchestrator()
|
| 16 |
-
problem_text = "ืคืชืืจ ืืช ืืืฉืืืื ืืืจืืืื ืืืืจืืช: sin x = 0.5"
|
| 17 |
-
grade = "10"
|
| 18 |
-
student_name = "ืืื"
|
| 19 |
-
|
| 20 |
-
print("๐ Starting Smart Solver Trace...")
|
| 21 |
-
try:
|
| 22 |
-
data_anchor = {"function_equations": ["sin x = 0.5"]}
|
| 23 |
-
result = await orchestrator.smart_solve(
|
| 24 |
-
problem_text=problem_text,
|
| 25 |
-
data_anchor=data_anchor,
|
| 26 |
-
grade=grade,
|
| 27 |
-
category="ALGEBRA",
|
| 28 |
-
processing_strategy=ProcessingStrategy.STRICT_SYMBOLIC
|
| 29 |
-
)
|
| 30 |
-
|
| 31 |
-
import json
|
| 32 |
-
with open("debug_output.json", "w", encoding="utf-8") as f:
|
| 33 |
-
json.dump(result, f, indent=2, ensure_ascii=False)
|
| 34 |
-
print("โ
Saved to debug_output.json")
|
| 35 |
-
|
| 36 |
-
except Exception as e:
|
| 37 |
-
print(f"โ Error during execution: {e}")
|
| 38 |
-
import traceback
|
| 39 |
-
traceback.print_exc()
|
| 40 |
-
|
| 41 |
-
if __name__ == "__main__":
|
| 42 |
-
asyncio.run(debug_orchestrator())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deploy_dev.ps1
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
$PROJECT_ID = "buddy-math-dev"
|
| 2 |
-
$SERVICE_NAME = "buddy-math-server"
|
| 3 |
-
$REGION = "europe-west3"
|
| 4 |
-
$IMAGE_NAME = "gcr.io/$PROJECT_ID/$SERVICE_NAME"
|
| 5 |
-
|
| 6 |
-
Write-Host "--- Starting Deployment for BuddyMath Server (DEV) ---" -ForegroundColor Cyan
|
| 7 |
-
|
| 8 |
-
# 1. Check for gcloud
|
| 9 |
-
if (!(Get-Command gcloud -ErrorAction SilentlyContinue)) {
|
| 10 |
-
Write-Error "โ gcloud CLI not found."
|
| 11 |
-
exit
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
# 2. Set Project
|
| 15 |
-
gcloud config set project $PROJECT_ID
|
| 16 |
-
|
| 17 |
-
# 3. Build Image
|
| 18 |
-
Write-Host "Building Image..."
|
| 19 |
-
gcloud builds submit --tag $IMAGE_NAME .
|
| 20 |
-
|
| 21 |
-
# 4. Deploy to Cloud Run (Including Secrets & 60s Timeout)
|
| 22 |
-
Write-Host "Deploying to Cloud Run..."
|
| 23 |
-
gcloud run deploy $SERVICE_NAME --image $IMAGE_NAME --platform managed --region $REGION --allow-unauthenticated --set-env-vars "ENV=development,OCR_STRIP_MODE=development" --set-secrets "GOOGLE_API_KEY=GOOGLE_API_KEY:latest,FIREBASE_CREDENTIALS=FIREBASE_CREDENTIALS:latest" --timeout 60
|
| 24 |
-
|
| 25 |
-
Write-Host "Deployment Complete!" -ForegroundColor Green
|
| 26 |
-
|
| 27 |
-
# 5. Storage Lifecycle Rule
|
| 28 |
-
Write-Host "Applying Storage Lifecycle Rule..." -ForegroundColor Yellow
|
| 29 |
-
gcloud storage buckets update gs://buddy-math-dev.firebasestorage.app --lifecycle-file=gcs_lifecycle_rule.json
|
| 30 |
-
|
| 31 |
-
Write-Host "DONE: Everything is up and running!" -ForegroundColor Green
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deploy_dev.sh
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
# deploy_dev.sh - Tutor Mode Deployment Script (DEV)
|
| 3 |
-
# V3.18.0 - Includes Phase C Environment Variables
|
| 4 |
-
|
| 5 |
-
PROJECT_ID="buddy-math-dev"
|
| 6 |
-
SERVICE_NAME="buddy-math-server"
|
| 7 |
-
REGION="europe-west3" # Change if needed
|
| 8 |
-
IMAGE_NAME="gcr.io/$PROJECT_ID/$SERVICE_NAME"
|
| 9 |
-
|
| 10 |
-
echo "๐ Starting Deployment for BuddyMath Server (DEV)..."
|
| 11 |
-
|
| 12 |
-
# 1. Check for gcloud
|
| 13 |
-
if ! command -v gcloud &> /dev/null
|
| 14 |
-
then
|
| 15 |
-
echo "โ gcloud CLI not found. Please install it: https://cloud.google.com/sdk/docs/install"
|
| 16 |
-
exit
|
| 17 |
-
fi
|
| 18 |
-
|
| 19 |
-
# 2. Set Project
|
| 20 |
-
echo "๐ Setting GCP Project to $PROJECT_ID..."
|
| 21 |
-
gcloud config set project $PROJECT_ID
|
| 22 |
-
|
| 23 |
-
# 3. Build Image using Cloud Build
|
| 24 |
-
echo "๐๏ธ Submitting build to Cloud Build..."
|
| 25 |
-
gcloud builds submit --tag $IMAGE_NAME .
|
| 26 |
-
|
| 27 |
-
# 4. Deploy to Cloud Run
|
| 28 |
-
echo "โ๏ธ Deploying to Cloud Run..."
|
| 29 |
-
gcloud run deploy $SERVICE_NAME \
|
| 30 |
-
--image $IMAGE_NAME \
|
| 31 |
-
--platform managed \
|
| 32 |
-
--region $REGION \
|
| 33 |
-
--allow-unauthenticated \
|
| 34 |
-
--set-env-vars "ENV=development,OCR_STRIP_MODE=development"
|
| 35 |
-
|
| 36 |
-
echo "โ
Deployment Complete!"
|
| 37 |
-
|
| 38 |
-
# 5. Reminder for Storage Lifecycle
|
| 39 |
-
echo -e "\n๐ [IMPORTANT] Don't forget to apply the Storage Lifecycle Rule if you haven't yet:"
|
| 40 |
-
echo "gsutil lifecycle set gcs_lifecycle_rule.json gs://buddy-math-dev.firebasestorage.app"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deploy_hf
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Subproject commit ebcf3058d7ba593763ac0ff97ea695b1d67955ec
|
|
|
|
|
|
docs/legacy_prompts_backup.txt
DELETED
|
@@ -1,937 +0,0 @@
|
|
| 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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/curriculum_classifier.py
DELETED
|
@@ -1,94 +0,0 @@
|
|
| 1 |
-
# domain/curriculum_classifier.py
|
| 2 |
-
import sympy as sp
|
| 3 |
-
import logging
|
| 4 |
-
|
| 5 |
-
logger = logging.getLogger(__name__)
|
| 6 |
-
|
| 7 |
-
class CurriculumClassifier:
|
| 8 |
-
"""
|
| 9 |
-
V6.1 Hybrid Architecture - Phase 1: Router Layer
|
| 10 |
-
Evaluates math complexity and injects grade-specific pedagogical prompts.
|
| 11 |
-
"""
|
| 12 |
-
|
| 13 |
-
@staticmethod
|
| 14 |
-
def estimate_complexity(math_string: str) -> int:
|
| 15 |
-
"""
|
| 16 |
-
Calculates a complexity score (1-10) based on AST features.
|
| 17 |
-
"""
|
| 18 |
-
score = 1
|
| 19 |
-
try:
|
| 20 |
-
# Parse multiple equations if necessary
|
| 21 |
-
expressions = []
|
| 22 |
-
|
| 23 |
-
# Helper to quickly strip common LaTeX that crashes sympify
|
| 24 |
-
import re
|
| 25 |
-
cleaned_str = re.sub(r'\\[a-zA-Z]+', '', str(math_string))
|
| 26 |
-
cleaned_str = cleaned_str.replace('{', '(').replace('}', ')').replace('$', '')
|
| 27 |
-
|
| 28 |
-
for part in cleaned_str.split(','):
|
| 29 |
-
expressions.append(sp.sympify(part.replace('=', '-'), evaluate=False))
|
| 30 |
-
|
| 31 |
-
for expr in expressions:
|
| 32 |
-
# Number of variables
|
| 33 |
-
vars_count = len(expr.free_symbols)
|
| 34 |
-
if vars_count > 1:
|
| 35 |
-
score += vars_count
|
| 36 |
-
|
| 37 |
-
# Number of operations / tree depth mapping (approx)
|
| 38 |
-
ops_count = expr.count_ops()
|
| 39 |
-
score += ops_count // 3
|
| 40 |
-
|
| 41 |
-
# Advanced functions (Trigonometry, Logarithms)
|
| 42 |
-
if expr.has(sp.sin, sp.cos, sp.tan, sp.log, sp.exp):
|
| 43 |
-
score += 3
|
| 44 |
-
|
| 45 |
-
# High degree polynomials
|
| 46 |
-
if expr.is_polynomial():
|
| 47 |
-
degrees = sp.degree_list(expr)
|
| 48 |
-
max_deg = max(degrees) if degrees else 0
|
| 49 |
-
if max_deg > 2:
|
| 50 |
-
score += 2
|
| 51 |
-
|
| 52 |
-
except Exception as e:
|
| 53 |
-
# Fallback heuristic if SymPy parse fails entirely
|
| 54 |
-
raw_len = len(str(math_string))
|
| 55 |
-
fallback_score = 3 + (raw_len // 15)
|
| 56 |
-
logger.warning(f"Complexity estimation failed, falling back to len heuristic ({fallback_score}): {e}")
|
| 57 |
-
score = fallback_score
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
final_score = min(max(int(score), 1), 10)
|
| 61 |
-
logger.info(f"[ROUTER] Intended Math Complexity Score: {final_score}/10")
|
| 62 |
-
return final_score
|
| 63 |
-
|
| 64 |
-
@staticmethod
|
| 65 |
-
def get_prompt_specialization(grade: str, intent_category: str) -> str:
|
| 66 |
-
"""
|
| 67 |
-
Returns pedagogical prompt constraints based on the student's grade.
|
| 68 |
-
"""
|
| 69 |
-
grade_num = 0
|
| 70 |
-
import re
|
| 71 |
-
match = re.search(r'\d+', str(grade))
|
| 72 |
-
if match:
|
| 73 |
-
grade_num = int(match.group())
|
| 74 |
-
|
| 75 |
-
constraints = []
|
| 76 |
-
|
| 77 |
-
# Base setup
|
| 78 |
-
constraints.append("ืืชื ืืืจื ืคืจืื ืืืชืืืืงื.")
|
| 79 |
-
|
| 80 |
-
# Grade specific logic
|
| 81 |
-
if grade_num <= 7:
|
| 82 |
-
constraints.append("ืืกืืจ ืืืื ืืื ืคืฉืืืื ืืืื. ืื ืชืฉืชืืฉ ืืืืืื ืืื 'ื ืืืื' ืื 'ื ืคืฉื', ืืื 'ื ืฉืืืจ ืืช ืื ืขืื ืืื' ืื 'ื ืืฉื ืืช ืืืกืคืจืื'.")
|
| 83 |
-
elif grade_num <= 9:
|
| 84 |
-
constraints.append("ืืฉืชืืฉ ืืืื ืื ืืืืืจื ืชืงื ืืื ืืื 'ืืขืืจืช ืืืคืื', 'ืืื ืืก ืืืืจืื ืืืืื', ื'ืืืืื ืืฉืชื ื'. ืฉืืืจ ืขื ืฉืคื ืืขืืืืช.")
|
| 85 |
-
elif grade_num <= 12:
|
| 86 |
-
constraints.append("ืื ื ืื ืืชืืืื ืืืืจ ืืืฉืื ืืกืื ืืืืืืจื. ืืชืืงื ืืืืืืื ืืขืืืง ืฉื ืืชืจืืื ืืืืืงืื ืืืชืืืืื (ืืืฉื ืืืงื ืืืงืืช, ืืืืืืช). ืืืื ืจืฉืื ืืืืืืง.")
|
| 87 |
-
if "TRIGONOMETRY" in intent_category.upper():
|
| 88 |
-
constraints.append("ืืืฉืืืืืช ืืจืืืื ืืืืจืืืช, ืืืืจ ืชืืื ืืช ืฉืืงืืื ืืืืืืจืืืช ืื ืืขืื ืืืืืื.")
|
| 89 |
-
else:
|
| 90 |
-
constraints.append("ืืกืืจ ืืืืืจืืช ืืืฆืืจื ืืฉืืจื.")
|
| 91 |
-
|
| 92 |
-
specialization = " ".join(constraints)
|
| 93 |
-
logger.info(f"[ROUTER] Prompt specialization applied for Grade {grade} ({intent_category})")
|
| 94 |
-
return specialization
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/math_normalizer.py
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
# domain/math_normalizer.py
|
| 2 |
-
|
| 3 |
-
import re
|
| 4 |
-
import logging
|
| 5 |
-
|
| 6 |
-
logger = logging.getLogger(__name__)
|
| 7 |
-
|
| 8 |
-
class MathCanonicalizer:
|
| 9 |
-
@staticmethod
|
| 10 |
-
def preprocess_ocr_string(math_string: str) -> str:
|
| 11 |
-
"""
|
| 12 |
-
V6 Polish (P0): Aggressively cleans raw OCR math strings before SymPy parsing.
|
| 13 |
-
- Adds explicit multiplication.
|
| 14 |
-
- Wraps trigonometric functions in parentheses (e.g., sin x -> sin(x)).
|
| 15 |
-
- Normalizes basic artifacts.
|
| 16 |
-
"""
|
| 17 |
-
if not math_string:
|
| 18 |
-
return ""
|
| 19 |
-
|
| 20 |
-
clean_str = str(math_string).strip()
|
| 21 |
-
|
| 22 |
-
# 1. Standardize Whitespace
|
| 23 |
-
clean_str = re.sub(r'\s+', ' ', clean_str)
|
| 24 |
-
|
| 25 |
-
# 2. Add implicit multiplication between numbers and variables (e.g., 2x -> 2*x)
|
| 26 |
-
# Note: SymPy handles some of this, but we do it explicitly to be safe
|
| 27 |
-
clean_str = re.sub(r'(\d)([a-zA-Z])', r'\1*\2', clean_str)
|
| 28 |
-
|
| 29 |
-
# 3. Trigonometric Functions Parentheses Guard
|
| 30 |
-
# Matches sin, cos, tan, cot, arcsin, arccos, arctan, etc. followed by space and a variable/number
|
| 31 |
-
# Example: 'sin x' -> 'sin(x)', 'cos 2x' -> 'cos(2*x)', 'tan ^2x' is trickier but we handle the basics.
|
| 32 |
-
trig_funcs = r'(sin|cos|tan|cot|sec|csc|arcsin|arccos|arctan)'
|
| 33 |
-
|
| 34 |
-
# Match 'sin x' or 'sin 2*x' but not 'sin(x)'
|
| 35 |
-
# This regex looks for trig function, optional space, and then something that is not a parenthesis
|
| 36 |
-
clean_str = re.sub(fr'{trig_funcs}\s+([a-zA-Z0-9_*]+)(?!\()', r'\1(\2)', clean_str)
|
| 37 |
-
|
| 38 |
-
return clean_str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/math_validator.py
DELETED
|
@@ -1,316 +0,0 @@
|
|
| 1 |
-
# domain/math_validator.py โ V1.1 (STABLE REGEX POLYGRAPH)
|
| 2 |
-
import re
|
| 3 |
-
import logging
|
| 4 |
-
import re
|
| 5 |
-
import logging
|
| 6 |
-
import time
|
| 7 |
-
import asyncio
|
| 8 |
-
from typing import Tuple, List, Optional, Any
|
| 9 |
-
import sympy
|
| 10 |
-
from sympy.parsing.sympy_parser import parse_expr
|
| 11 |
-
from utils.math_utils import aggressive_sympy_sanitizer
|
| 12 |
-
|
| 13 |
-
logger = logging.getLogger(__name__)
|
| 14 |
-
|
| 15 |
-
# Expressions that are structurally unparseable but pedagogically harmless
|
| 16 |
-
_PIPE_SEPARATED_RESULT = re.compile(r'\|')
|
| 17 |
-
_HEBREW_ONLY = re.compile(r'^[\u0590-\u05FF\s]+$')
|
| 18 |
-
|
| 19 |
-
# LaTeX commands that SymPy cannot parse โ strip layout but KEEP math functions
|
| 20 |
-
_LATEX_STRIP = re.compile(
|
| 21 |
-
r'\\(?:left|right|div|pm|mp|leq|geq|neq'
|
| 22 |
-
r'|approx|infty|text|mathrm|mathbf|boxed|underbrace|overbrace|hat|bar|vec|dot|overline|underline)\b'
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
-
# V307.0: Functions that often lack parentheses in LLM output (e.g. lnx)
|
| 26 |
-
_MATH_FUNC_PARENS = ['ln', 'sin', 'cos', 'tan', 'sqrt', 'log', 'exp', 'Abs']
|
| 27 |
-
|
| 28 |
-
def _latex_to_sympy_str(latex_str: str) -> str:
|
| 29 |
-
"""
|
| 30 |
-
Best-effort LaTeX โ SymPy-parseable string.
|
| 31 |
-
V310.0: Aggressive Hebrew stripping and malformed notation cleanup.
|
| 32 |
-
"""
|
| 33 |
-
if latex_str is None:
|
| 34 |
-
return ""
|
| 35 |
-
s = str(latex_str).strip()
|
| 36 |
-
|
| 37 |
-
# 0. V310.0: Strip Hebrew characters and BOM/Zero-width chars immediately
|
| 38 |
-
s = re.sub(r'[\u0590-\u05FF\u200B-\u200D\uFEFF]', ' ', s)
|
| 39 |
-
|
| 40 |
-
# 1. Handle \frac{a}{b} โ (a)/(b)
|
| 41 |
-
loop_counter = 0
|
| 42 |
-
max_loops = 15
|
| 43 |
-
while r'\frac' in s and loop_counter < max_loops:
|
| 44 |
-
old_s = s
|
| 45 |
-
s = re.sub(r'\\frac\s*\{([^{}]*)\}\s*\{([^{}]*)\}', r'(\1)/(\2)', s)
|
| 46 |
-
if old_s == s:
|
| 47 |
-
s = s.replace(r'\frac', '(frac_err)')
|
| 48 |
-
break
|
| 49 |
-
loop_counter += 1
|
| 50 |
-
|
| 51 |
-
# 2. Convert LaTeX functions to plain words (e.g. \ln -> ln)
|
| 52 |
-
s = s.replace(r'\ln', ' ln ').replace(r'\sin', ' sin ').replace(r'\cos', ' cos ')
|
| 53 |
-
s = s.replace(r'\tan', ' tan ').replace(r'\sqrt', ' sqrt ').replace(r'\log', ' log ')
|
| 54 |
-
s = s.replace(r'\exp', ' exp ').replace(r'\pi', ' pi ').replace(r'\theta', ' theta ')
|
| 55 |
-
|
| 56 |
-
# --- ๐ BEGIN HOTFIX V9.0.3: LATEX MULTIPLICATION ---
|
| 57 |
-
s = s.replace(r'\cdot', '*').replace(r'\times', '*')
|
| 58 |
-
# --- END HOTFIX ---
|
| 59 |
-
|
| 60 |
-
# 3. Remove remaining purely structural LaTeX commands
|
| 61 |
-
s = _LATEX_STRIP.sub(' ', s)
|
| 62 |
-
|
| 63 |
-
# 4. Remove LaTeX delimiters/wrappers
|
| 64 |
-
s = s.replace('{', '(').replace('}', ')').replace('$', '')
|
| 65 |
-
s = s.replace(r'\left', '').replace(r'\right', '')
|
| 66 |
-
|
| 67 |
-
# 5. V307.0: Fix implicit function arguments (e.g. lnx -> ln(x))
|
| 68 |
-
for func in _MATH_FUNC_PARENS:
|
| 69 |
-
pattern = r'\b' + func + r'\b\s*([^( \t\n\r\f\v,]+)'
|
| 70 |
-
s = re.sub(pattern, func + r'(\1)', s)
|
| 71 |
-
|
| 72 |
-
# 6. Handle absolute value pipes |x| -> Abs(x)
|
| 73 |
-
loop_counter = 0
|
| 74 |
-
while '|' in s and s.count('|') >= 2 and loop_counter < 10:
|
| 75 |
-
s = re.sub(r'\|([^|]+)\|', r'Abs(\1)', s)
|
| 76 |
-
loop_counter += 1
|
| 77 |
-
|
| 78 |
-
# 7. Implicit multiplication: 2x โ 2*x (only if not inside a word)
|
| 79 |
-
s = re.sub(r'(\d)([a-zA-Z(])', r'\1*\2', s)
|
| 80 |
-
|
| 81 |
-
# 8. V280.0: Equals sign handling is now moved to _check_segment
|
| 82 |
-
# for more robust parsing of equations.
|
| 83 |
-
|
| 84 |
-
# 9. Final cleanup: Remove illegal SymPy chars like ', ", ?, !
|
| 85 |
-
s = re.sub(r'[?!\'"]', '', s)
|
| 86 |
-
s = re.sub(r'\s+', ' ', s)
|
| 87 |
-
return s.strip()
|
| 88 |
-
|
| 89 |
-
def _is_plaintext(expr_str: str) -> bool:
|
| 90 |
-
if _HEBREW_ONLY.match(expr_str):
|
| 91 |
-
return True
|
| 92 |
-
if _PIPE_SEPARATED_RESULT.search(expr_str) and not any(c in expr_str for c in ['+', '-', '*', '/', '^', '=']):
|
| 93 |
-
return True
|
| 94 |
-
return False
|
| 95 |
-
|
| 96 |
-
class MathPolygraph:
|
| 97 |
-
TIMEOUT_SECONDS = 3
|
| 98 |
-
|
| 99 |
-
@staticmethod
|
| 100 |
-
async def _validate_single(text: str, step_id) -> Tuple[bool, str]:
|
| 101 |
-
"""
|
| 102 |
-
V280.0 REDESIGN:
|
| 103 |
-
1. No Blind Stripping: Extracts $...$ or $$...$$ using re.finditer with DOTALL.
|
| 104 |
-
2. Security: Uses parse_expr(evaluate=False).
|
| 105 |
-
3. Equations: Splits by '=' and validates parts to bypass SymPy's '=' limitation.
|
| 106 |
-
4. Multi-Equal: Handles x=y=5 without crashing.
|
| 107 |
-
5. Empty Guard: Skips $$$$.
|
| 108 |
-
"""
|
| 109 |
-
if not text or not text.strip():
|
| 110 |
-
return True, ""
|
| 111 |
-
|
| 112 |
-
# regex: find both $$display$$ and $inline$ blocks. DOTALL allows multi-line display math.
|
| 113 |
-
# Group 1 = display math, Group 2 = inline math
|
| 114 |
-
math_pattern = re.compile(r'\$\$(.*?)\$\$|\$(.*?)\$', re.DOTALL)
|
| 115 |
-
matches = list(re.finditer(math_pattern, text))
|
| 116 |
-
|
| 117 |
-
if not matches:
|
| 118 |
-
# V280.0 Rule: If no delimiters are found, treat the whole string as plain text
|
| 119 |
-
# or try to parse if it looks like math (existing behavior for backward compatibility)
|
| 120 |
-
if _is_plaintext(text):
|
| 121 |
-
return True, ""
|
| 122 |
-
return await MathPolygraph._check_segment(text, step_id)
|
| 123 |
-
|
| 124 |
-
for match in matches:
|
| 125 |
-
# Group 1 (Display) or Group 2 (Inline)
|
| 126 |
-
content = (match.group(1) or match.group(2) or "").strip()
|
| 127 |
-
|
| 128 |
-
# 5. Empty String Guard
|
| 129 |
-
if not content:
|
| 130 |
-
continue
|
| 131 |
-
|
| 132 |
-
# V280.0 Fix: Multi-line display math might contain multiple equations.
|
| 133 |
-
# Split by newline before validating segments.
|
| 134 |
-
sub_segments = [s.strip() for s in content.split('\n') if s.strip()]
|
| 135 |
-
for sub in sub_segments:
|
| 136 |
-
ok, reason = await MathPolygraph._check_segment(sub, step_id)
|
| 137 |
-
if not ok:
|
| 138 |
-
return False, reason
|
| 139 |
-
|
| 140 |
-
return True, ""
|
| 141 |
-
|
| 142 |
-
@staticmethod
|
| 143 |
-
async def _run_safe_math_op(func, *args, timeout_sec: float = 0.3) -> Any:
|
| 144 |
-
"""
|
| 145 |
-
V9.0.5: Executes a CPU-bound SymPy operation in a separate thread with a strict timeout.
|
| 146 |
-
Returns the result of the function, or raises TimeoutError/Exception.
|
| 147 |
-
"""
|
| 148 |
-
try:
|
| 149 |
-
return await asyncio.wait_for(
|
| 150 |
-
asyncio.to_thread(func, *args),
|
| 151 |
-
timeout=timeout_sec
|
| 152 |
-
)
|
| 153 |
-
except asyncio.TimeoutError:
|
| 154 |
-
raise asyncio.TimeoutError("SymPy operation timed out")
|
| 155 |
-
|
| 156 |
-
@staticmethod
|
| 157 |
-
async def _check_segment(raw_segment: str, step_id) -> Tuple[bool, str]:
|
| 158 |
-
"""Internal helper to validate a single extracted math segment."""
|
| 159 |
-
# V9.0.1: Use Aggressive Sanitizer to handle Hebrew, Arrows, and Comma-splitting
|
| 160 |
-
sanitized_parts = aggressive_sympy_sanitizer(raw_segment)
|
| 161 |
-
|
| 162 |
-
if not sanitized_parts:
|
| 163 |
-
return True, ""
|
| 164 |
-
|
| 165 |
-
for part in sanitized_parts:
|
| 166 |
-
sympy_str = _latex_to_sympy_str(part)
|
| 167 |
-
if not sympy_str or sympy_str in ('', '-', '()', '( )'):
|
| 168 |
-
continue
|
| 169 |
-
|
| 170 |
-
try:
|
| 171 |
-
# V9.0.5: Wrap CPU-bound sympify/parsing in a non-blocking thread with 300ms timeout
|
| 172 |
-
# We bypass the slow multiprocessing approach for SSE safety.
|
| 173 |
-
def parse_and_eval(s):
|
| 174 |
-
# Character Whitelist (from legacy _sympify_worker)
|
| 175 |
-
safe_pattern = r'^[a-zA-Z0-9\s\+\-\*\/\^\(\)\.\,\!\%\=]+$'
|
| 176 |
-
if not re.match(safe_pattern, s):
|
| 177 |
-
return False
|
| 178 |
-
res = parse_expr(s, evaluate=False)
|
| 179 |
-
if res is not None:
|
| 180 |
-
evaluated = res.doit()
|
| 181 |
-
if hasattr(evaluated, 'is_finite') and evaluated.is_finite is False:
|
| 182 |
-
return False
|
| 183 |
-
if hasattr(evaluated, 'is_nan') and evaluated.is_nan:
|
| 184 |
-
return False
|
| 185 |
-
return True
|
| 186 |
-
|
| 187 |
-
status = await MathPolygraph._run_safe_math_op(parse_and_eval, sympy_str)
|
| 188 |
-
|
| 189 |
-
if status is False:
|
| 190 |
-
logger.warning(f"๐ก๏ธ [SOFT FAIL] SymPy Parse Error on part '{part}'. Bypassing validator.")
|
| 191 |
-
return True, "" # Soft Fail: allow stream to continue
|
| 192 |
-
|
| 193 |
-
except asyncio.TimeoutError:
|
| 194 |
-
logger.warning(f"๐ก๏ธ [SOFT FAIL] SymPy TIMEOUT (300ms) on part '{part}'. Bypassing validator.")
|
| 195 |
-
return True, "" # Soft Fail: allow stream to continue
|
| 196 |
-
except Exception as e:
|
| 197 |
-
logger.warning(f"๐ก๏ธ [SOFT FAIL] Unexpected validation crash: {e} for part '{part}'. Bypassing.")
|
| 198 |
-
return True, "" # Soft Fail: allow stream to continue
|
| 199 |
-
|
| 200 |
-
return True, ""
|
| 201 |
-
|
| 202 |
-
@staticmethod
|
| 203 |
-
async def validate_step_sequence(steps: List[dict], topic: str = "GENERAL") -> Tuple[bool, str]:
|
| 204 |
-
if not steps:
|
| 205 |
-
return True, ""
|
| 206 |
-
|
| 207 |
-
# V8.9.4: Skip deep SymPy parsing for discrete sequence steps to avoid false-positive SyntaxErrors
|
| 208 |
-
is_sequence = topic and "SEQUENCE" in topic.upper()
|
| 209 |
-
|
| 210 |
-
for step in steps:
|
| 211 |
-
step_id = step.get('step_id', step.get('step_number', '?'))
|
| 212 |
-
math_fields = []
|
| 213 |
-
for field in ('math_latex', 'block_math', 'math'):
|
| 214 |
-
val = step.get(field)
|
| 215 |
-
if val and isinstance(val, str) and val.strip():
|
| 216 |
-
math_fields.append(val.strip())
|
| 217 |
-
if not math_fields:
|
| 218 |
-
continue
|
| 219 |
-
|
| 220 |
-
# If sequence, we only check if it's "valid-ish" LaTeX vs deep SymPy check
|
| 221 |
-
if is_sequence:
|
| 222 |
-
# Basic sanity check for LaTeX balance
|
| 223 |
-
if math_fields[0].count('{') != math_fields[0].count('}'):
|
| 224 |
-
return False, f"LATEX_BRACKET_MISMATCH:step_{step_id}"
|
| 225 |
-
continue
|
| 226 |
-
|
| 227 |
-
ok, reason = await MathPolygraph._validate_single(math_fields[0], step_id)
|
| 228 |
-
if not ok:
|
| 229 |
-
return False, reason
|
| 230 |
-
return True, ""
|
| 231 |
-
@staticmethod
|
| 232 |
-
async def are_equivalent(latex1: str, latex2: str) -> bool:
|
| 233 |
-
"""
|
| 234 |
-
V9.0.5: Checks if two LaTeX expressions are mathematically equivalent (Non-Blocking).
|
| 235 |
-
Supports expressions and equations (by converting to 'expr = 0').
|
| 236 |
-
"""
|
| 237 |
-
try:
|
| 238 |
-
# 1. Handle Equations in Equivalence Check
|
| 239 |
-
if '=' in latex1 and '=' in latex2 and latex1.count('=') == 1 and latex2.count('=') == 1:
|
| 240 |
-
parts1 = [p.strip() for p in latex1.split('=') if p.strip()]
|
| 241 |
-
parts2 = [p.strip() for p in latex2.split('=') if p.strip()]
|
| 242 |
-
if len(parts1) == 2 and len(parts2) == 2:
|
| 243 |
-
res1 = await MathPolygraph.are_equivalent(parts1[0], parts2[0])
|
| 244 |
-
res2 = await MathPolygraph.are_equivalent(parts1[1], parts2[1])
|
| 245 |
-
return res1 and res2
|
| 246 |
-
|
| 247 |
-
s1_raw = _latex_to_sympy_str(latex1)
|
| 248 |
-
s2_raw = _latex_to_sympy_str(latex2)
|
| 249 |
-
|
| 250 |
-
# Check for inequalities in raw LaTeX to be safe
|
| 251 |
-
inequalities = ['<', '>', r'\leq', r'\geq', r'\neq', r'\leq', r'\geq']
|
| 252 |
-
if any(iq in latex1 for iq in inequalities) or any(iq in latex2 for iq in inequalities):
|
| 253 |
-
return latex1.strip() == latex2.strip()
|
| 254 |
-
|
| 255 |
-
# Security: Strict Whitelist for Equivalence Check
|
| 256 |
-
safe_pattern = r'^[a-zA-Z0-9\s\+\-\*\/\^\(\)\.\,\!\=]+$'
|
| 257 |
-
def is_safe(s):
|
| 258 |
-
clean = s.replace('\\', '').replace('_', '').replace('{', '(').replace('}', ')')
|
| 259 |
-
return bool(re.match(safe_pattern, clean))
|
| 260 |
-
|
| 261 |
-
if not (is_safe(s1_raw) and is_safe(s2_raw)):
|
| 262 |
-
return latex1.strip() == latex2.strip()
|
| 263 |
-
|
| 264 |
-
# V9.0.5: Wrap CPU-bound SymPy simplify in a thread with timeout
|
| 265 |
-
def calc_equivalence(str1, str2):
|
| 266 |
-
expr1 = parse_expr(str1, evaluate=False)
|
| 267 |
-
expr2 = parse_expr(str2, evaluate=False)
|
| 268 |
-
# "Variable Trap": Basic structural equivalence if variables are involved
|
| 269 |
-
if len(expr1.free_symbols) > 0 or len(expr2.free_symbols) > 0:
|
| 270 |
-
return sympy.simplify(expr1 - expr2) == 0
|
| 271 |
-
# Numerical Identity check
|
| 272 |
-
diff = sympy.simplify(expr1 - expr2)
|
| 273 |
-
return diff == 0
|
| 274 |
-
|
| 275 |
-
return await MathPolygraph._run_safe_math_op(calc_equivalence, s1_raw, s2_raw)
|
| 276 |
-
|
| 277 |
-
except asyncio.TimeoutError:
|
| 278 |
-
logger.warning(f"๐ก๏ธ [SOFT FAIL] Equivalence check TIMEOUT (300ms) for {latex1} vs {latex2}")
|
| 279 |
-
return True # Soft Fail: Assume equivalent if we can't prove otherwise in time
|
| 280 |
-
except Exception as e:
|
| 281 |
-
logger.warning(f"๐ก๏ธ [SOFT FAIL] Equivalence check failed: {e}")
|
| 282 |
-
return True # Soft Fail: Assume equivalent on error
|
| 283 |
-
|
| 284 |
-
@staticmethod
|
| 285 |
-
async def verify_algebraic_consistency(steps: List[dict], topic: str = "GENERAL") -> Tuple[bool, str]:
|
| 286 |
-
"""
|
| 287 |
-
V1.3: Checks if a sequence of steps is algebraically consistent.
|
| 288 |
-
Currently checks if subsequent steps are equivalent (for simplifications).
|
| 289 |
-
"""
|
| 290 |
-
# V8.9.4: Skip deep SymPy parsing for discrete sequence steps
|
| 291 |
-
if topic and "SEQUENCE" in topic.upper():
|
| 292 |
-
return True, ""
|
| 293 |
-
|
| 294 |
-
math_steps = []
|
| 295 |
-
for step in steps:
|
| 296 |
-
math = step.get('math_latex') or step.get('block_math') or step.get('math')
|
| 297 |
-
if math and isinstance(math, str) and math.strip():
|
| 298 |
-
# Avoid validating plaintext logic blocks
|
| 299 |
-
if not _is_plaintext(math):
|
| 300 |
-
math_steps.append({'id': step.get('step_id', '?'), 'math': math})
|
| 301 |
-
|
| 302 |
-
if len(math_steps) < 2:
|
| 303 |
-
return True, ""
|
| 304 |
-
|
| 305 |
-
for i in range(len(math_steps) - 1):
|
| 306 |
-
s1 = math_steps[i]['math']
|
| 307 |
-
s2 = math_steps[i+1]['math']
|
| 308 |
-
|
| 309 |
-
# Simple heuristic: Only verify if they look like comparable equations/expressions
|
| 310 |
-
if not await MathPolygraph.are_equivalent(s1, s2):
|
| 311 |
-
logger.info(f"[POLYGRAPH] Consistency warning between {s1} and {s2}")
|
| 312 |
-
# We return False only if we are VERY sure.
|
| 313 |
-
# For now, we'll return False to trigger self-correction as requested.
|
| 314 |
-
return False, f"ALGEBRAIC_INCONSISTENCY:step_{math_steps[i+1]['id']}"
|
| 315 |
-
|
| 316 |
-
return True, ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/ontology.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
# domain/ontology.py
|
| 2 |
-
|
| 3 |
-
GLOBAL_ONTOLOGY = {
|
| 4 |
-
"algebra": {
|
| 5 |
-
"solve_linear_system": {
|
| 6 |
-
"concepts": ["ืืขืจืืช", "ืืขืจืืช ืืฉืืืืืช", "ืืืชืื ืืฉืจืื", "ืืฉืืืืช ืืืืืืื", "ื ืขืืืื", "ื ืฆืื", "ื ืืืฅ", "ืืฉืืืืืช", "ืืฉืชื ื", "ืขืจื"],
|
| 7 |
-
"tag": "ืืฉืืืื"
|
| 8 |
-
},
|
| 9 |
-
"isolate_variable": {
|
| 10 |
-
"concepts": ["ืืืืื ืืฉืชื ื", "ืืขืืจืช ืืืคืื", "ื ืืืง ืืืงืื", "ื ืืกืจ", "ื ืื ืก ืืืืจืื ืืืืื", "ื ืคืฉื", "ืืืืจืื"],
|
| 11 |
-
"tag": "ืืืืื ืืฉืชื ื"
|
| 12 |
-
},
|
| 13 |
-
"substitute": {
|
| 14 |
-
"concepts": ["ืืฆืื", "ื ืฆืื ืืช ืืขืจื", "ืืฉืืืื ืืงืืจืืช", "ืขืจื ื-y", "ืขืจื ื-x", "ืืฉืชื ื ืฉื ื"],
|
| 15 |
-
"tag": "ืืฆืื"
|
| 16 |
-
},
|
| 17 |
-
"general_algebra": {
|
| 18 |
-
"concepts": ["ืืฉืืืื", "ื ืขืื", "ืคืชืจืื", "ืืกืืจ", "ืืืฉืื", "ืฉืื"],
|
| 19 |
-
"tag": "ืืืืืจื ืืกืืกืืช"
|
| 20 |
-
}
|
| 21 |
-
},
|
| 22 |
-
"geometry": {
|
| 23 |
-
# Geometry terms will go here later
|
| 24 |
-
}
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
def get_allowed_concepts(category: str, rule_id: str) -> list[str]:
|
| 28 |
-
"""Fetch allowed concepts from ontology"""
|
| 29 |
-
domain = GLOBAL_ONTOLOGY.get(category.lower(), {})
|
| 30 |
-
rule_data = domain.get(rule_id, domain.get("general_algebra", {"concepts": []}))
|
| 31 |
-
return rule_data.get("concepts", [])
|
| 32 |
-
|
| 33 |
-
def get_pedagogical_tag(category: str, rule_id: str) -> str:
|
| 34 |
-
"""Fetch pedagogical tag from ontology"""
|
| 35 |
-
domain = GLOBAL_ONTOLOGY.get(category.lower(), {})
|
| 36 |
-
rule_data = domain.get(rule_id, domain.get("general_algebra", {"tag": "ืืืื"}))
|
| 37 |
-
return rule_data.get("tag", "ืืืื")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/pedagogical_renderer.py
DELETED
|
@@ -1,264 +0,0 @@
|
|
| 1 |
-
# domain/pedagogical_renderer.py - V7.2 (Deterministic Governed Reasoning Runtime)
|
| 2 |
-
import re
|
| 3 |
-
import logging
|
| 4 |
-
from typing import List, Optional
|
| 5 |
-
import domain.telemetry as telemetry
|
| 6 |
-
from domain.semantic_bank import get_diversity_engine
|
| 7 |
-
|
| 8 |
-
logger = logging.getLogger(__name__)
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
# ==================== V7.2: PLACEHOLDER GUARD ====================
|
| 13 |
-
|
| 14 |
-
def validate_placeholders(renderer_text: str, provided_ids: List[str]) -> bool:
|
| 15 |
-
"""
|
| 16 |
-
V7.2: Bi-directional placeholder validation.
|
| 17 |
-
|
| 18 |
-
Check 1: Every ID the server provided MUST appear in the text as {{id}}.
|
| 19 |
-
Check 2: No placeholder the LLM invented (not in provided_ids) may appear.
|
| 20 |
-
|
| 21 |
-
Either failure โ Fail Closed (Hint Mode).
|
| 22 |
-
"""
|
| 23 |
-
# Check 1: All provided IDs must be referenced
|
| 24 |
-
for step_id in provided_ids:
|
| 25 |
-
expected = "{{" + step_id + "}}"
|
| 26 |
-
if expected not in renderer_text:
|
| 27 |
-
logger.warning(
|
| 28 |
-
f"[RENDERER GUARD] Missing placeholder for server-provided ID '{step_id}'. "
|
| 29 |
-
f"Expected '{expected}' in renderer output."
|
| 30 |
-
)
|
| 31 |
-
telemetry.emit_renderer_placeholder_violation("missing", step_id)
|
| 32 |
-
return False
|
| 33 |
-
|
| 34 |
-
# Check 2: No invented placeholders allowed
|
| 35 |
-
found_placeholders = re.findall(r'\{\{(\w+)\}\}', renderer_text)
|
| 36 |
-
for ph in found_placeholders:
|
| 37 |
-
if ph not in provided_ids:
|
| 38 |
-
logger.warning(
|
| 39 |
-
f"[RENDERER GUARD] Invented placeholder '{{{{{{ph}}}}}}' detected! "
|
| 40 |
-
f"Not in server-provided IDs: {provided_ids}"
|
| 41 |
-
)
|
| 42 |
-
telemetry.emit_renderer_placeholder_violation("invented", ph)
|
| 43 |
-
return False
|
| 44 |
-
|
| 45 |
-
return True
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def inject_signed_results(renderer_text: str, signed_steps: List[dict]) -> str:
|
| 49 |
-
"""
|
| 50 |
-
Replaces {{step_id}} placeholders in the renderer's text with the actual
|
| 51 |
-
computed expressions from the server's signed step list.
|
| 52 |
-
Only called AFTER validate_placeholders() passes.
|
| 53 |
-
"""
|
| 54 |
-
result = renderer_text
|
| 55 |
-
for step in signed_steps:
|
| 56 |
-
placeholder = "{{" + step["id"] + "}}"
|
| 57 |
-
result = result.replace(placeholder, f"`{step['expression']}`")
|
| 58 |
-
return result
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
# ==================== V7.2: PEDAGOGICAL RENDERER ====================
|
| 62 |
-
|
| 63 |
-
class PedagogicalRenderer:
|
| 64 |
-
"""
|
| 65 |
-
V7.2: LLM #2 โ Pedagogical Renderer.
|
| 66 |
-
|
| 67 |
-
Receives the server's signed step IDs (NOT expressions) and the Planner's rationale.
|
| 68 |
-
Produces a pedagogical explanation in Hebrew using only {{step_id}} placeholders.
|
| 69 |
-
Never computes or writes math itself.
|
| 70 |
-
"""
|
| 71 |
-
|
| 72 |
-
def __init__(self, llm_gateway):
|
| 73 |
-
self.llm_gateway = llm_gateway
|
| 74 |
-
|
| 75 |
-
# V7.2.4 PROMPT โ Updated by CTO: Pedagogical Narrator framing + 1 approved example.
|
| 76 |
-
# Any further changes require full regression (chaos_test.py 18/18) + CTO sign-off.
|
| 77 |
-
def _build_renderer_prompt(self, pedagogical_rationale: str, step_ids: List[str]) -> str:
|
| 78 |
-
ids_list = ", ".join(f"{{{{{sid}}}}}" for sid in step_ids)
|
| 79 |
-
return f"""You are a Pedagogical Narrator for a Hebrew math tutoring system.
|
| 80 |
-
Your role is to explain the INTUITION behind each solution step, not to compute anything.
|
| 81 |
-
The server has already computed all mathematics with certainty โ your job is to tell the story.
|
| 82 |
-
|
| 83 |
-
PEDAGOGICAL RATIONALE:
|
| 84 |
-
{pedagogical_rationale}
|
| 85 |
-
|
| 86 |
-
SERVER PLACEHOLDERS (use EXACTLY as written):
|
| 87 |
-
{ids_list}
|
| 88 |
-
|
| 89 |
-
RULES:
|
| 90 |
-
1. Hebrew only.
|
| 91 |
-
2. Explain the intuition and logic behind each step in human, encouraging language.
|
| 92 |
-
Use analogies, real-world stories, or visual metaphors.
|
| 93 |
-
3. Embed ALL server placeholders exactly: {ids_list}
|
| 94 |
-
4. FORBIDDEN โ do NOT write: any digit, number, variable, operator, fraction, or math symbol.
|
| 95 |
-
Rule: if you feel the urge to write a number or expression, describe its meaning in words instead.
|
| 96 |
-
Use {{{{step_id}}}} to present the server's computed result. No '=', '/', '^', 'sin(x)', 'pi', etc.
|
| 97 |
-
5. No invented placeholders. Only server-provided IDs above.
|
| 98 |
-
|
| 99 |
-
EXAMPLE (single approved format โ "math textbook narrator style"):
|
| 100 |
-
โ
CORRECT: "ืืื ืืืฆืื ืืช ื ืงืืืืช ืืืืชืื ืขื ืฆืืจ ื-y, ื ืฆืื ืืคืก ืืขืจื ื-x ืื ืงืื: {{{{step_1}}}}."
|
| 101 |
-
โ FORBIDDEN: "ืืื ืืืฆืื ืืช ืืืืชืื ื ืฆืื x=0 ืื ืงืื y=4." (raw numbers/symbols โ blocked by Guardian)
|
| 102 |
-
"""
|
| 103 |
-
|
| 104 |
-
async def render(
|
| 105 |
-
self,
|
| 106 |
-
pedagogical_rationale: str,
|
| 107 |
-
signed_steps: List[dict],
|
| 108 |
-
action: Optional[str] = None, # V7.2.5: Planner Enum action for SemanticBank lookup
|
| 109 |
-
) -> dict:
|
| 110 |
-
"""
|
| 111 |
-
V7.2.5 Semantic Composer:
|
| 112 |
-
1. Try DiversityEngine.compose(action) โ deterministic narrative from SemanticBank.
|
| 113 |
-
2. If no bank entry exists โ falls back to LLM #2 (Graceful Degradation).
|
| 114 |
-
Both paths run renderer_guard() + scan_for_math_leakage() before returning.
|
| 115 |
-
"""
|
| 116 |
-
step_ids = [s["id"] for s in signed_steps]
|
| 117 |
-
|
| 118 |
-
# โโ Path A: Deterministic Semantic Composer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 119 |
-
if action is not None:
|
| 120 |
-
engine = get_diversity_engine()
|
| 121 |
-
composed = engine.compose(action, signed_steps)
|
| 122 |
-
if composed is not None:
|
| 123 |
-
logger.info(f"[RENDERER] ๐ SemanticBank hit for '{action}'. Using deterministic narrative.")
|
| 124 |
-
# Inject server results into {{placeholders}} in composed narrative
|
| 125 |
-
final_text = inject_signed_results(composed, signed_steps)
|
| 126 |
-
# Guard: composed output must still pass safety checks
|
| 127 |
-
if not renderer_guard(composed): # check pre-injection text
|
| 128 |
-
logger.error("[RENDERER] renderer_guard failed on SemanticBank output!")
|
| 129 |
-
return {"success": False, "reason": "SEMANTIC_BANK_GUARD_VIOLATION"}
|
| 130 |
-
if not scan_for_math_leakage(final_text):
|
| 131 |
-
logger.error("[RENDERER] Leakage scan failed on SemanticBank output!")
|
| 132 |
-
return {"success": False, "reason": "SEMANTIC_BANK_LEAKAGE"}
|
| 133 |
-
logger.info("โ
[RENDERER] Semantic Composer output approved by all guards.")
|
| 134 |
-
return {"success": True, "rendered_text": final_text, "source": "semantic_bank"}
|
| 135 |
-
|
| 136 |
-
# โโ Path B: LLM #2 Fallback (unknown action or no bank entry) โโโโโโโโโโโโ
|
| 137 |
-
logger.info(f"[RENDERER] No SemanticBank entry for action='{action}'. Using LLM #2 fallback.")
|
| 138 |
-
step_ids = [s["id"] for s in signed_steps]
|
| 139 |
-
|
| 140 |
-
system_prompt = self._build_renderer_prompt(pedagogical_rationale, step_ids)
|
| 141 |
-
user_prompt = (
|
| 142 |
-
f"Write the pedagogical explanation for this solution. "
|
| 143 |
-
f"Use the placeholders: {', '.join(step_ids)}"
|
| 144 |
-
)
|
| 145 |
-
|
| 146 |
-
logger.info(f"[RENDERER] Requesting pedagogical explanation for steps: {step_ids}")
|
| 147 |
-
|
| 148 |
-
try:
|
| 149 |
-
raw_text = await self.llm_gateway.generate_raw(system_prompt, user_prompt)
|
| 150 |
-
|
| 151 |
-
# Bi-directional placeholder guard
|
| 152 |
-
if not validate_placeholders(raw_text, step_ids):
|
| 153 |
-
logger.error("[RENDERER] Placeholder guard FAILED. Failing closed.")
|
| 154 |
-
return {"success": False, "reason": "PLACEHOLDER_GUARD_VIOLATION"}
|
| 155 |
-
|
| 156 |
-
# โโ Renderer Guard (V7.2.4) โ strict keyword + digit blacklist โโโโ
|
| 157 |
-
# Runs on raw LLM output BEFORE injection to catch trig keywords,
|
| 158 |
-
# individual digits, and operators the LLM "helpfully" wrote itself.
|
| 159 |
-
if not renderer_guard(raw_text):
|
| 160 |
-
logger.error("[RENDERER] renderer_guard FAILED on raw LLM output. Failing closed.")
|
| 161 |
-
telemetry.emit_renderer_leakage("renderer_guard_raw")
|
| 162 |
-
return {"success": False, "reason": "RENDERER_GUARD_VIOLATION"}
|
| 163 |
-
|
| 164 |
-
# Inject actual results from server
|
| 165 |
-
final_text = inject_signed_results(raw_text, signed_steps)
|
| 166 |
-
|
| 167 |
-
# โโ Renderer Guardian Scan (V7.2.2/V7.2.3) โ whitelist + consecutive chars โ
|
| 168 |
-
if not scan_for_math_leakage(final_text):
|
| 169 |
-
logger.error(
|
| 170 |
-
"[RENDERER GUARDIAN] Math leakage in final injected text! Failing closed."
|
| 171 |
-
)
|
| 172 |
-
telemetry.emit_renderer_leakage("post-injection-leak")
|
| 173 |
-
return {"success": False, "reason": "RENDERER_GUARDIAN_LEAKAGE"}
|
| 174 |
-
|
| 175 |
-
logger.info("[RENDERER] โ
Both guards passed. Pedagogical explanation approved.")
|
| 176 |
-
return {"success": True, "rendered_text": final_text}
|
| 177 |
-
|
| 178 |
-
except Exception as e:
|
| 179 |
-
logger.error(f"[RENDERER] LLM failure: {e}")
|
| 180 |
-
return {"success": False, "reason": f"LLM_FAILURE: {e}"}
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
# ==================== V7.2.4: RENDERER GUARD (Strict Blacklist) ====================
|
| 184 |
-
|
| 185 |
-
def renderer_guard(text: str) -> bool:
|
| 186 |
-
"""
|
| 187 |
-
V7.2.4 Structural Output Lock โ strict blacklist scan on RAW LLM output.
|
| 188 |
-
Runs BEFORE placeholder injection to catch violations at the source.
|
| 189 |
-
|
| 190 |
-
Blocks: individual digits, operator chars, named math functions (sin/cos/tan/sqrt/pi).
|
| 191 |
-
This is stricter than scan_for_math_leakage() which uses a whitelist on the FINAL text.
|
| 192 |
-
|
| 193 |
-
Returns True (safe) / False (fail closed).
|
| 194 |
-
"""
|
| 195 |
-
# Remove valid server-provided {{placeholders}} first
|
| 196 |
-
clean = re.sub(r'\{\{.*?\}\}', '', text)
|
| 197 |
-
|
| 198 |
-
# Blacklist pattern: any digit, basic operator, or named math function
|
| 199 |
-
FORBIDDEN_PATTERN = r'[0-9]|[+*/^=]|(?<![a-zA-Z\u0590-\u05FF])(sin|cos|tan|sqrt|pi)(?![a-zA-Z\u0590-\u05FF])'
|
| 200 |
-
|
| 201 |
-
match = re.search(FORBIDDEN_PATTERN, clean)
|
| 202 |
-
if match:
|
| 203 |
-
logger.warning(
|
| 204 |
-
f"[RENDERER_GUARD] Forbidden content in raw LLM output: '{match.group(0)}'. "
|
| 205 |
-
f"Failing closed."
|
| 206 |
-
)
|
| 207 |
-
return False
|
| 208 |
-
return True
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
# ==================== V7.2: TICKET 5 โ UI GATE WHITELIST SCAN ====================
|
| 212 |
-
|
| 213 |
-
def scan_for_math_leakage(rendered_text: str) -> bool:
|
| 214 |
-
"""
|
| 215 |
-
V7.2.3 Hardened Whitelist Scan (NOT a blacklist).
|
| 216 |
-
After removing {{...}} placeholders, the remaining text may ONLY contain:
|
| 217 |
-
- Hebrew letters (Unicode block U+0590โU+05FF)
|
| 218 |
-
- English letters (for natural language words)
|
| 219 |
-
- Spaces and basic punctuation (. , ; : ! ? ' " -)
|
| 220 |
-
|
| 221 |
-
Two-pass enforcement:
|
| 222 |
-
Pass 1 โ Full whitelist: any forbidden char โ REJECT.
|
| 223 |
-
Pass 2 โ Consecutive math chars: >2 consecutive math symbols outside {{}}
|
| 224 |
-
catches patterns like '3((9)*(+1))', '^2+4', '/6)' even if
|
| 225 |
-
pass 1 might miss edge-case single chars.
|
| 226 |
-
"""
|
| 227 |
-
# Remove all valid server-provided {{placeholders}} first
|
| 228 |
-
clean_text = re.sub(r'\{\{\w+\}\}', '', rendered_text).strip()
|
| 229 |
-
|
| 230 |
-
# V7.2.5 FIX: Remove backtick-wrapped server-signed expressions.
|
| 231 |
-
# inject_signed_results() wraps each signed expression in `backticks`.
|
| 232 |
-
# These are TRUSTED server content (SymPy output) โ NOT LLM-generated.
|
| 233 |
-
# They MUST be excluded from the leakage scan to avoid false positives.
|
| 234 |
-
clean_text = re.sub(r'`[^`]+`', '', clean_text).strip()
|
| 235 |
-
|
| 236 |
-
if not clean_text:
|
| 237 |
-
return True # Text was purely placeholders + signed expressions โ safe
|
| 238 |
-
|
| 239 |
-
# Pass 1: Full whitelist โ Hebrew + English + basic punctuation + typography + backticks ONLY
|
| 240 |
-
ALLOWED_PATTERN = r'^[\u0590-\u05FFa-zA-Z\s.,;:!?\-\"\'\u2014\u2013\u2012\u200f\u200e`]+$'
|
| 241 |
-
whitelist_ok = bool(re.match(ALLOWED_PATTERN, clean_text))
|
| 242 |
-
|
| 243 |
-
if not whitelist_ok:
|
| 244 |
-
# Show all characters that were NOT in the whitelist (sync this regex with ALLOWED_PATTERN)
|
| 245 |
-
violations = re.sub(r'[\u0590-\u05FFa-zA-Z\s.,;:!?\-\"\'\u2014\u2013\u2012\u200f\u200e`]+', '', clean_text)
|
| 246 |
-
logger.warning(f"[UI_GATE] Math leakage detected! Offending chars: '{violations[:50]}'")
|
| 247 |
-
telemetry.emit_renderer_leakage(violations[:50])
|
| 248 |
-
return False
|
| 249 |
-
|
| 250 |
-
# Pass 2: Consecutive math char sequence detection (V7.2.3 hardening)
|
| 251 |
-
# Catches: '3((9', '^2+', '/6)', etc. โ >2 non-whitespace math chars in a row
|
| 252 |
-
# IMPORTANT: exclude backtick (`) as it's used for styling, not as a math operator.
|
| 253 |
-
MATH_CHAR_PATTERN = r'[0-9=+\-*/^<>|\\(){}\[\]%@#&~]{3,}'
|
| 254 |
-
consecutive_match = re.search(MATH_CHAR_PATTERN, clean_text)
|
| 255 |
-
if consecutive_match:
|
| 256 |
-
chain = consecutive_match.group(0)
|
| 257 |
-
logger.warning(
|
| 258 |
-
f"[UI_GATE] Consecutive math char sequence detected: '{chain[:50]}'. "
|
| 259 |
-
f"Failing closed (Pass 2 โ V7.2.3 hardening)."
|
| 260 |
-
)
|
| 261 |
-
telemetry.emit_renderer_leakage(f"consecutive_chain:{chain[:30]}")
|
| 262 |
-
return False
|
| 263 |
-
|
| 264 |
-
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/processing_strategy.py
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
from enum import Enum
|
| 2 |
-
|
| 3 |
-
class ProcessingStrategy(str, Enum):
|
| 4 |
-
SIMPLE_ARITHMETIC = "SIMPLE_ARITHMETIC" # Fast Path
|
| 5 |
-
STRICT_SYMBOLIC = "STRICT_SYMBOLIC" # ืืืจืฉ ืืืืื / ืืื ื ืกืืื
|
| 6 |
-
HEURISTIC_DEDUCTION = "HEURISTIC_DEDUCTION" # ืืงืกืืืืื / ืื ืจื
|
| 7 |
-
CONVERSATIONAL = "CONVERSATIONAL" # ืฉืื ืืืคืฉื
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/proposal_engine.py
DELETED
|
@@ -1,189 +0,0 @@
|
|
| 1 |
-
# domain/proposal_engine.py - V7.2 (Deterministic Governed Reasoning Runtime)
|
| 2 |
-
import json
|
| 3 |
-
import logging
|
| 4 |
-
from typing import List
|
| 5 |
-
from enum import Enum
|
| 6 |
-
from pydantic import BaseModel, ValidationError
|
| 7 |
-
from .ontology import get_allowed_concepts
|
| 8 |
-
import domain.telemetry as telemetry
|
| 9 |
-
|
| 10 |
-
logger = logging.getLogger(__name__)
|
| 11 |
-
|
| 12 |
-
# ==================== V7.2: STRICT SCHEMA DEFINITIONS ====================
|
| 13 |
-
|
| 14 |
-
class ComputeAction(str, Enum):
|
| 15 |
-
"""V7.3: Hardcoded allowed actions. The Planner may NOT invent new ones."""
|
| 16 |
-
# โโ Algebraic (V7.2) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 17 |
-
SOLVE_EQUATION = "SOLVE_EQUATION"
|
| 18 |
-
SIMPLIFY = "SIMPLIFY"
|
| 19 |
-
FIND_DERIVATIVE = "FIND_DERIVATIVE"
|
| 20 |
-
FIND_INTEGRAL = "FIND_INTEGRAL"
|
| 21 |
-
FACTOR = "FACTOR"
|
| 22 |
-
EXPAND = "EXPAND"
|
| 23 |
-
SUBSTITUTE = "SUBSTITUTE"
|
| 24 |
-
# โโ Geometry / Analytic (V7.3) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 25 |
-
FIND_AXIS_INTERSECTIONS = "FIND_AXIS_INTERSECTIONS" # ืืฆื x=0 / y=0 ืืคืชืืจ
|
| 26 |
-
CALCULATE_SLOPE_AND_LINE = "CALCULATE_SLOPE_AND_LINE" # ืฉืืคืืข + ืืฉืืืช ืืฉืจ
|
| 27 |
-
CALCULATE_DISTANCE = "CALCULATE_DISTANCE" # ื ืืกืืช ืืจืืง + ืืืืงืช ืืืงืื
|
| 28 |
-
|
| 29 |
-
class ServerComputeTask(BaseModel):
|
| 30 |
-
action: ComputeAction # Enum enforced: unknown action โ ValidationError
|
| 31 |
-
target_step_ref: str # AST Node ID only (e.g. "ast_node_2"), NOT a math expression
|
| 32 |
-
|
| 33 |
-
class PlannerResponse(BaseModel):
|
| 34 |
-
pedagogical_rationale: str
|
| 35 |
-
requires_server_compute: List[ServerComputeTask]
|
| 36 |
-
|
| 37 |
-
class PlannerFormatError(Exception):
|
| 38 |
-
"""Raised when the Planner's response fails structural or schema validation."""
|
| 39 |
-
pass
|
| 40 |
-
|
| 41 |
-
MAX_SERVER_STEPS = 8 # DOS Prevention: Planner cannot request more than 8 server tasks
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
# ==================== V7.2: SAFE JSON EXTRACTION ====================
|
| 45 |
-
|
| 46 |
-
def extract_and_validate_plan(raw_response: str) -> PlannerResponse:
|
| 47 |
-
"""
|
| 48 |
-
V7.2: Index-based JSON extraction with PlannerFormatError on any failure.
|
| 49 |
-
Refuses split() to avoid silent IndexError.
|
| 50 |
-
"""
|
| 51 |
-
if "<JSON_START>" not in raw_response or "<JSON_END>" not in raw_response:
|
| 52 |
-
raise PlannerFormatError("Missing explicit JSON boundaries <JSON_START>/<JSON_END>.")
|
| 53 |
-
|
| 54 |
-
start = raw_response.index("<JSON_START>") + len("<JSON_START>")
|
| 55 |
-
end = raw_response.index("<JSON_END>")
|
| 56 |
-
raw_json = raw_response[start:end].strip()
|
| 57 |
-
|
| 58 |
-
try:
|
| 59 |
-
plan = PlannerResponse.model_validate_json(raw_json)
|
| 60 |
-
|
| 61 |
-
# Guard: DOS Prevention
|
| 62 |
-
if len(plan.requires_server_compute) > MAX_SERVER_STEPS:
|
| 63 |
-
raise PlannerFormatError(
|
| 64 |
-
f"DOS Guard: Plan requested {len(plan.requires_server_compute)} steps, max is {MAX_SERVER_STEPS}."
|
| 65 |
-
)
|
| 66 |
-
|
| 67 |
-
return plan
|
| 68 |
-
except (ValidationError, json.JSONDecodeError) as e:
|
| 69 |
-
raise PlannerFormatError(f"Validation failed: {e}")
|
| 70 |
-
except PlannerFormatError:
|
| 71 |
-
raise
|
| 72 |
-
except Exception as e:
|
| 73 |
-
raise PlannerFormatError(f"Unexpected extraction error: {e}")
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
# ==================== V7.2: PROPOSAL ENGINE ====================
|
| 77 |
-
|
| 78 |
-
class ProposalEngine:
|
| 79 |
-
"""
|
| 80 |
-
V7.2 Deterministic Governed Reasoning โ Pedagogical Planner (LLM #1)
|
| 81 |
-
|
| 82 |
-
The LLM receives only AST metadata (IDs and operations) and returns
|
| 83 |
-
a structured Plan (JSON). It NEVER writes math expressions โ only references AST Node IDs.
|
| 84 |
-
"""
|
| 85 |
-
|
| 86 |
-
def __init__(self, llm_gateway):
|
| 87 |
-
self.llm_gateway = llm_gateway
|
| 88 |
-
|
| 89 |
-
def _build_system_prompt(self, ast_metadata: dict, prompt_specialization: str, sub_question_text: str = "") -> str:
|
| 90 |
-
"""Build the strict Planner system prompt including boundary enforcement."""
|
| 91 |
-
allowed_concepts = get_allowed_concepts(
|
| 92 |
-
ast_metadata.get("detected_operations", ["general"])[0],
|
| 93 |
-
"general_algebra"
|
| 94 |
-
)
|
| 95 |
-
concepts_str = ", ".join(allowed_concepts) if allowed_concepts else "ืืื ืื ืืืืืจื ืืืืืงืื"
|
| 96 |
-
|
| 97 |
-
sub_q_line = f"\nSUB-QUESTION TO SOLVE NOW: {sub_question_text}" if sub_question_text else ""
|
| 98 |
-
|
| 99 |
-
return f"""You are a Pedagogical Planner for a math tutoring system.
|
| 100 |
-
Your role is to plan HOW to solve a math problem, NOT to solve it yourself.
|
| 101 |
-
|
| 102 |
-
PROBLEM CONTEXT (AST Metadata):
|
| 103 |
-
{json.dumps(ast_metadata, ensure_ascii=False, indent=2)}{sub_q_line}
|
| 104 |
-
|
| 105 |
-
CRITICAL RULES:
|
| 106 |
-
1. You MUST wrap your entire JSON response between <JSON_START> and <JSON_END> tags.
|
| 107 |
-
2. Do NOT write any math expressions or compute anything yourself.
|
| 108 |
-
3. When referring to AST nodes, use their IDs only (e.g. "ast_node_0").
|
| 109 |
-
4. Your response MUST conform to this exact JSON schema:
|
| 110 |
-
{{
|
| 111 |
-
"pedagogical_rationale": "<short explanation of the teaching strategy in Hebrew>",
|
| 112 |
-
"requires_server_compute": [
|
| 113 |
-
{{"action": "<ENUM_ACTION>", "target_step_ref": "<ast_node_id>"}},
|
| 114 |
-
...
|
| 115 |
-
]
|
| 116 |
-
}}
|
| 117 |
-
5. Allowed actions (ENUM only): SOLVE_EQUATION, SIMPLIFY, FIND_DERIVATIVE, FIND_INTEGRAL, FACTOR, EXPAND, SUBSTITUTE, FIND_AXIS_INTERSECTIONS, CALCULATE_SLOPE_AND_LINE, CALCULATE_DISTANCE.
|
| 118 |
-
6. Maximum {MAX_SERVER_STEPS} compute steps.
|
| 119 |
-
7. Pedagogical Constraint: {prompt_specialization}
|
| 120 |
-
8. Vocabulary: Prioritize these terms: [{concepts_str}].
|
| 121 |
-
9. SINGLE-ACTION CONSTRAINT (critical): For complex equations (circles, trigonometry, systems),
|
| 122 |
-
always use SOLVE_EQUATION directly on ast_node_0. DO NOT chain operations that reference
|
| 123 |
-
intermediate result nodes (ast_node_1, ast_node_2, etc.) โ those nodes do not exist in
|
| 124 |
-
the server registry. The SymPy engine handles simplification and expansion internally
|
| 125 |
-
as part of SOLVE_EQUATION. Violating this rule causes a server crash.
|
| 126 |
-
10. CONTEXTUAL ROUTING (V7.3 โ critical): Analyze the SUB-QUESTION TO SOLVE NOW carefully.
|
| 127 |
-
Select the action that matches the specific task:
|
| 128 |
-
- "ืืืชืื", "ืฆืืจืื" โ FIND_AXIS_INTERSECTIONS
|
| 129 |
-
- "ืืฉืจ", "ืฉืืคืืข", "ืืฉืืื" โ CALCULATE_SLOPE_AND_LINE
|
| 130 |
-
- "ืืจืืง", "ื ืงืืื ืขื ืืืขืื" โ CALCULATE_DISTANCE
|
| 131 |
-
- General algebra/equation โ SOLVE_EQUATION
|
| 132 |
-
You MUST choose the action that solves specifically this sub-question.
|
| 133 |
-
Do NOT use SOLVE_EQUATION when a more specific action applies."""
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
async def generate_draft_proposal(self, context_obj, prompt_specialization: str, ast_metadata: dict = None) -> dict:
|
| 137 |
-
"""
|
| 138 |
-
Asks LLM #1 (Planner) to produce a structured action plan.
|
| 139 |
-
Returns {"success": True, "plan": PlannerResponse} or {"success": False, "reason": ...}.
|
| 140 |
-
"""
|
| 141 |
-
if ast_metadata is None:
|
| 142 |
-
ast_metadata = {}
|
| 143 |
-
|
| 144 |
-
system_prompt = self._build_system_prompt(
|
| 145 |
-
ast_metadata, prompt_specialization,
|
| 146 |
-
sub_question_text=getattr(context_obj, 'sub_question_text', '')
|
| 147 |
-
)
|
| 148 |
-
user_prompt = (
|
| 149 |
-
f"Plan the solution for: {context_obj.math_input}\n"
|
| 150 |
-
f"Remember: Output JSON only, wrapped in <JSON_START>...<JSON_END> tags."
|
| 151 |
-
)
|
| 152 |
-
|
| 153 |
-
logger.info(f"[PLANNER] Requesting plan for: {context_obj.math_input}")
|
| 154 |
-
|
| 155 |
-
for attempt in range(2): # Max 1 retry (Full Strategy Re-run)
|
| 156 |
-
try:
|
| 157 |
-
raw_response = await self.llm_gateway.generate_raw(system_prompt, user_prompt)
|
| 158 |
-
plan = extract_and_validate_plan(raw_response)
|
| 159 |
-
|
| 160 |
-
logger.info(
|
| 161 |
-
f"[PLANNER] Valid plan received on attempt {attempt + 1}. "
|
| 162 |
-
f"Actions: {[t.action for t in plan.requires_server_compute]}"
|
| 163 |
-
)
|
| 164 |
-
# Phase 1 Live: Track which Enum actions the Planner chose (drift detection)
|
| 165 |
-
chosen_actions = [task.action.value for task in plan.requires_server_compute]
|
| 166 |
-
telemetry.emit_planner_strategy_distribution(chosen_actions)
|
| 167 |
-
return {"success": True, "plan": plan}
|
| 168 |
-
|
| 169 |
-
except PlannerFormatError as e:
|
| 170 |
-
logger.warning(f"[PLANNER] Attempt {attempt + 1} failed: {e}")
|
| 171 |
-
if attempt == 0:
|
| 172 |
-
# Full Strategy Re-run: inject hard feedback and retry
|
| 173 |
-
user_prompt = (
|
| 174 |
-
f"Your previous response was invalid: {e}\n"
|
| 175 |
-
f"Try again. Plan the solution for: {context_obj.math_input}\n"
|
| 176 |
-
f"You MUST use <JSON_START> and <JSON_END> tags. Output valid JSON only."
|
| 177 |
-
)
|
| 178 |
-
continue
|
| 179 |
-
else:
|
| 180 |
-
logger.error("[PLANNER] Max retries exhausted. Failing closed.")
|
| 181 |
-
return {"success": False, "reason": str(e)}
|
| 182 |
-
|
| 183 |
-
except Exception as e:
|
| 184 |
-
logger.error(f"[PLANNER] LLM failure on attempt {attempt + 1}: {e}")
|
| 185 |
-
if attempt == 0:
|
| 186 |
-
continue
|
| 187 |
-
return {"success": False, "reason": f"LLM_FAILURE: {e}"}
|
| 188 |
-
|
| 189 |
-
return {"success": False, "reason": "MAX_RETRIES_EXHAUSTED"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/risk_engine.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
| 1 |
-
# domain/risk_engine.py
|
| 2 |
-
import logging
|
| 3 |
-
from typing import Dict, Any, List
|
| 4 |
-
from .curriculum_classifier import CurriculumClassifier
|
| 5 |
-
|
| 6 |
-
logger = logging.getLogger(__name__)
|
| 7 |
-
|
| 8 |
-
class CognitiveRiskEngine:
|
| 9 |
-
"""
|
| 10 |
-
V6.1 Phase 4: Risk Scoring
|
| 11 |
-
Evaluates the cognitive risk of a proposal based on AST complexity,
|
| 12 |
-
step count, retries, and symbolic stability.
|
| 13 |
-
"""
|
| 14 |
-
|
| 15 |
-
WEIGHTS = {
|
| 16 |
-
"ast_complexity": 0.30,
|
| 17 |
-
"reasoning_depth": 0.20,
|
| 18 |
-
"retry_penalty": 0.25,
|
| 19 |
-
"symbolic_instability": 0.25
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
@classmethod
|
| 23 |
-
def calculate_risk_score(cls,
|
| 24 |
-
math_input: str,
|
| 25 |
-
draft_steps: List[Dict[str, Any]],
|
| 26 |
-
retry_count: int,
|
| 27 |
-
validation_errors: int) -> Dict[str, Any]:
|
| 28 |
-
"""
|
| 29 |
-
Calculates the Cognitive Risk Score (CRS) between 0.0 and 1.0.
|
| 30 |
-
"""
|
| 31 |
-
# 1. AST Complexity (normalized 0-1)
|
| 32 |
-
# We reuse CurriculumClassifier's internal complexity estimation if possible,
|
| 33 |
-
# but here we normalize it against a ceiling of 20.
|
| 34 |
-
raw_complexity = CurriculumClassifier.estimate_complexity(math_input)
|
| 35 |
-
ast_risk = min(raw_complexity / 20.0, 1.0)
|
| 36 |
-
|
| 37 |
-
# 2. Reasoning Depth (normalized 0-1)
|
| 38 |
-
# 10 steps is considered high depth for our MVP
|
| 39 |
-
depth_risk = min(len(draft_steps) / 10.0, 1.0)
|
| 40 |
-
|
| 41 |
-
# 3. Retry Penalty
|
| 42 |
-
# 0 retries = 0.0, 1 retry = 1.0 (since max retry is 1 in V6.1)
|
| 43 |
-
retry_risk = 1.0 if retry_count > 0 else 0.0
|
| 44 |
-
|
| 45 |
-
# 4. Symbolic Instability
|
| 46 |
-
# Based on validation failures during the process
|
| 47 |
-
instability_risk = min(validation_errors / 5.0, 1.0)
|
| 48 |
-
|
| 49 |
-
# Weighted Final Score
|
| 50 |
-
crs = (ast_risk * cls.WEIGHTS["ast_complexity"] +
|
| 51 |
-
depth_risk * cls.WEIGHTS["reasoning_depth"] +
|
| 52 |
-
retry_risk * cls.WEIGHTS["retry_penalty"] +
|
| 53 |
-
instability_risk * cls.WEIGHTS["symbolic_instability"])
|
| 54 |
-
|
| 55 |
-
result = {
|
| 56 |
-
"risk_score": round(crs, 3),
|
| 57 |
-
"features": {
|
| 58 |
-
"ast_risk": round(ast_risk, 3),
|
| 59 |
-
"depth_risk": round(depth_risk, 3),
|
| 60 |
-
"retry_risk": retry_risk,
|
| 61 |
-
"instability_risk": round(instability_risk, 3)
|
| 62 |
-
}
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
logger.info(f"๐ง [RISK_ENGINE] Calculated CRS: {result['risk_score']} (AST: {ast_risk}, Depth: {depth_risk}, Retry: {retry_risk})")
|
| 66 |
-
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/schemas.py
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
from enum import Enum
|
| 2 |
-
from typing import Optional, Any, Dict
|
| 3 |
-
from pydantic import BaseModel, Field
|
| 4 |
-
|
| 5 |
-
class BuddyState(str, Enum):
|
| 6 |
-
STRATEGY_READY = "STRATEGY_READY"
|
| 7 |
-
SECTION_WORKING = "SECTION_WORKING"
|
| 8 |
-
SECTION_READY = "SECTION_READY"
|
| 9 |
-
COMPLETE = "COMPLETE"
|
| 10 |
-
ERROR = "ERROR"
|
| 11 |
-
|
| 12 |
-
class BuddyEvent(BaseModel):
|
| 13 |
-
question_id: str = Field(..., description="Unique ID for the question session")
|
| 14 |
-
state: BuddyState = Field(..., description="Current state of the micro-agent")
|
| 15 |
-
current_section_id: Optional[str] = Field(None, description="The ID of the section (e.g., 'ื', 'ื') currently being processed")
|
| 16 |
-
payload: Dict[str, Any] = Field(default_factory=dict, description="Payload containing state-specific data")
|
| 17 |
-
|
| 18 |
-
class Config:
|
| 19 |
-
use_enum_values = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/semantic_bank.py
DELETED
|
@@ -1,366 +0,0 @@
|
|
| 1 |
-
# domain/semantic_bank.py โ V7.2.5: Semantic Composer
|
| 2 |
-
#
|
| 3 |
-
# The SemanticBank is a governed, deterministic source of pedagogical narratives.
|
| 4 |
-
# Instead of letting LLM #2 generate free text (risky), the NarrativeComposer picks
|
| 5 |
-
# from curated, teacher-grade Hebrew templates and injects server-signed results.
|
| 6 |
-
#
|
| 7 |
-
# CTO directive: text must sound like a real private tutor speaking to a 10th/12th grader.
|
| 8 |
-
# Write at eye level โ encouraging, intuitive, NOT like system error messages.
|
| 9 |
-
|
| 10 |
-
import random
|
| 11 |
-
import logging
|
| 12 |
-
from dataclasses import dataclass, field
|
| 13 |
-
from collections import Counter
|
| 14 |
-
from typing import List, Dict, Optional
|
| 15 |
-
|
| 16 |
-
from domain import telemetry
|
| 17 |
-
|
| 18 |
-
logger = logging.getLogger(__name__)
|
| 19 |
-
|
| 20 |
-
DRIFT_ALERT_THRESHOLD = 0.35 # Emit alert when one variant dominates above this fraction
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
# โโโ Data Model โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 24 |
-
|
| 25 |
-
@dataclass
|
| 26 |
-
class SemanticEntry:
|
| 27 |
-
"""
|
| 28 |
-
A single pedagogical concept in the bank.
|
| 29 |
-
Each entry holds multiple variant fragments to ensure narrative diversity.
|
| 30 |
-
"""
|
| 31 |
-
concept_tag: str # matches Planner Enum action (e.g. "SOLVE_EQUATION")
|
| 32 |
-
openings: List[str] # 3 opening phrases โ how to START the explanation
|
| 33 |
-
bridges: List[str] # 3 logic bridges โ HOW to connect the steps
|
| 34 |
-
analogies: List[str] # 2 analogy/metaphor sentences โ the "aha!" moments
|
| 35 |
-
closing: str # 1 closing encouragement sentence
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
# โโโ The Bank โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 39 |
-
|
| 40 |
-
SEMANTIC_BANK: Dict[str, SemanticEntry] = {
|
| 41 |
-
|
| 42 |
-
"SOLVE_EQUATION": SemanticEntry(
|
| 43 |
-
concept_tag="SOLVE_EQUATION",
|
| 44 |
-
openings=[
|
| 45 |
-
"ืืืื ื ืคืชืืจ ืืช ืืืฉืืืื ืฆืขื ืืืจ ืฆืขื, ืื ืชืืื, ืื ืคืืืช ืืคืืื ืืฉื ืจืื.",
|
| 46 |
-
"ืืื ืืืืืช ืืช ืขืจื ืื ืขืื, ืื ืื ื ืืืืืื ืืืืื ืืืชื ืืฉื ื ืฆืืื ืืืฉืืืื.",
|
| 47 |
-
"ื ืืฉืื ืขื ืืืฉืืืื ืืื ืืืื ืืื: ืื ืื ืฉืขืืฉืื ืืฆื ืืืื, ืขืืฉืื ืื ืืฆื ืฉืืื.",
|
| 48 |
-
],
|
| 49 |
-
bridges=[
|
| 50 |
-
"ืืืจื ืืคืขืืื ืืื, ืื ืื ืฉื ืืชืจ ืืื:",
|
| 51 |
-
"ืืืฉื ืคืฉื ืืช ืื ืื ืฉืงืืืื ื, ืืชืืฆืื ืืชืืื:",
|
| 52 |
-
"ืืืฉืืืื 'ืืชื ืงืชื' ืืขืืฉืื ืืจืืจ ืฉืืชืฉืืื ืืื:",
|
| 53 |
-
],
|
| 54 |
-
analogies=[
|
| 55 |
-
"ืืฉืื ืขื ืื ืืื ืืืื โ ืื ืื ื ืืกืืจืื ืจืืืื ืืื ืืื ืขื ืฉืื ืขืื ืืฉืืฃ ืืืืจื.",
|
| 56 |
-
"ืืืืืง ืืื ืฉืืฉืืงืื ืืืืืืื โ ืื ืื ื ืืืคืฉืื ืืช x ืขื ืฉืืื ืืืจ ืื ืืืื ืืืชืืื.",
|
| 57 |
-
],
|
| 58 |
-
closing="ืขืฉืืช ืืช ืื! ืื ืืืจื ืืจืฉืืืช ืฉืื ืืชืืืืงืืื ืคืืชืจืื ืืฉืืืืืช.",
|
| 59 |
-
),
|
| 60 |
-
|
| 61 |
-
"SIMPLIFY": SemanticEntry(
|
| 62 |
-
concept_tag="SIMPLIFY",
|
| 63 |
-
openings=[
|
| 64 |
-
"ืืืืืื ืืื ื ืจืื ืืกืืื, ืืื ืื ื ืกืืจ ืืืชื โ ืืื ืืชืงืคื ืืฆืืจื ืืจืื ืืืชืจ ื ืงืืื.",
|
| 65 |
-
"ืืคืืฉืื, ืืืืจื ืืื ืืืชืื ืืช ืืืชื ืืืืจ โ ืจืง ืืฆืืจื ืืืื ืืืืืช ืืืืชืจ.",
|
| 66 |
-
"ืืื ืืกืืจ ืืืจ ืืืืืื โ ืื ืื ื ืืืกืคืื ืืืืจืื ืืืืื ืืืกืืงืื ืื ืฉืืืืชืจ.",
|
| 67 |
-
],
|
| 68 |
-
bridges=[
|
| 69 |
-
"ืืืจื ืฉื ืืกืืฃ ืืช ืื ืืืืืจืื ืืืืืื ืืื, ืืืืืื ืืคืฉืื ืืื:",
|
| 70 |
-
"ืืฉืืกืืงืื ืืช ืื 'ืืจืขืฉ ืืืชืืื', ืื ืฉื ืฉืืจ ืืื:",
|
| 71 |
-
"ืืฆืืจื ืืคืฉืืื ืืื ืงืืื ืืืืชืจ ืฉื ืืืืืื:",
|
| 72 |
-
],
|
| 73 |
-
analogies=[
|
| 74 |
-
"ืืืืื ืฉืืฉ ืื ืืจืื ืฉืืจืืช ืฉื ืืกืฃ โ ืืชื ืืืืืฃ ืืืชื ืืฉืืจ ืืื ืืืื. ืื ืืืืืง ืื ืฉืื ืื ื ืขืืฉืื ืขื ืืืืืื.",
|
| 75 |
-
"ืืื ืืงืคื ืืคืช ืขื ืง โ ืืกืืฃ ืงืืืืช ืืช ืืืชื ืืืืืข, ืจืง ืืืืื ืฉื ืื ืืฉืืช.",
|
| 76 |
-
],
|
| 77 |
-
closing="ืืืคื! ืคืืฉืื ืื ืืืช ืืืืฉืืจืื ืืื ืฉืืืืฉืืื ืืืชืืืืงื โ ืืขืืฉืื ืืชื ืืืืข ืืขืฉืืช ืืช ืื.",
|
| 78 |
-
),
|
| 79 |
-
|
| 80 |
-
"FACTOR": SemanticEntry(
|
| 81 |
-
concept_tag="FACTOR",
|
| 82 |
-
openings=[
|
| 83 |
-
"ืคืืจืืง ืืืืจืืื ืื ืืื ืืืฆืื ืืช 'ืืื ืืื ืื' ืฉืืจืืืืื ืืช ืืืืืื ืฉืื ื.",
|
| 84 |
-
"ืืืงืื ืืจืืืช ืืืืื ืืื ืืืื, ืื ืื ื ืืืคืฉืื ืฉื ื ืืืืืืื ืงืื ืื ืฉืืืืคืืื ืื ืืื.",
|
| 85 |
-
"ืืืจืืง ืืคืืจืืง ืืืืจืืื ืืื ืืืืืช ืื 'ืืกืชืชืจ' ืืชืื ืืืืืื ืืืคืฉืจ ืืืืฆืื ืืืืฆื.",
|
| 86 |
-
],
|
| 87 |
-
bridges=[
|
| 88 |
-
"ืืฉื ืืฆืื ืืช ืืืืจื ืืืฉืืชืฃ, ื ืงืื:",
|
| 89 |
-
"ืืืจื ืืคืืจืืง, ืืืืืื ื ืืชื ืืฆืืจื ืืืคืืืช:",
|
| 90 |
-
"ืืืืจืืื ืฉืืจืืืืื ืืช ืืืืืื ืื:",
|
| 91 |
-
],
|
| 92 |
-
analogies=[
|
| 93 |
-
"ืคืืจืืง ืืืืจืืื ืืื ืืื ืืคืจืง ืืกืคืจ ืืืืืงื ืจืืฉืื ืืช โ ืื ืื ื ืืืฆืืื ืืช ื-DNA ืืืชืืื ืฉื ืืืืืื.",
|
| 94 |
-
"ืืืืื ืฉืืชื ืืคืจืง ืืจืื ืืืื ืืงืืคืกืืืช ืงืื ืืช โ ืื ืงืืคืกื ืืื ืืืจื. ืืืื ืื ืืจืืืืืช ืืช ืืืงืืจ.",
|
| 95 |
-
],
|
| 96 |
-
closing="ืคืืจืืง ืืืืจืืื ืืื ืืืืืง ืื ืฉืืืคืฉืจ ืื ื ืืคืชืืจ ืืฉืืืืืช ืจืืืืขืืืช โ ืชืืืจ ืืช ืืืื ืืื ืืื.",
|
| 97 |
-
),
|
| 98 |
-
|
| 99 |
-
"EXPAND": SemanticEntry(
|
| 100 |
-
concept_tag="EXPAND",
|
| 101 |
-
openings=[
|
| 102 |
-
"ืขืืฉืื ื ืคืจืืฉ ืืช ืืกืืืจืืื โ ื ืืคืื ืื ืืืืจ ืืชืื ืืกืืืจืืื ืขื ืื ืื ืฉืืืืฆื ืืื.",
|
| 103 |
-
"ืคืชืืืช ืกืืืจืืื ืืื ืืื 'ืืคืชืื' ืืชื ื ืขืืืคื โ ืื ืฉืืกืชืชืจ ืืคื ืื ืืืฆื ืืืืจ.",
|
| 104 |
-
"ื ืฉืชืืฉ ืืืืง ืืคืืืื ืืื ืืืจืืื ืืช ืืืืืื ืืืจืืืช ืืช ืื ืืืืืจืื ืื ืคืจื.",
|
| 105 |
-
],
|
| 106 |
-
bridges=[
|
| 107 |
-
"ืืืืจ ืคืชืืืช ืืกืืืจืืื ืืืืกืืฃ ืืืืืจืื ืืืืืื:",
|
| 108 |
-
"ืืฉืืจืืืืื ืืื ืืืกืืจืื:",
|
| 109 |
-
"ืืืืืื ืืืืจืื, ืขื ืื ืืืืืจืื ืืืืืื:",
|
| 110 |
-
],
|
| 111 |
-
analogies=[
|
| 112 |
-
"ืืฉืื ืขื ืื ืืื ืืืืคืื ืชืืืืจ โ ืื ืงื ืืช ืฉืืืฉื ืฉืงืื, ืื ืืื ืขื ืชืคืืืื ืืื ื ืืช, ืืชื ืืืฉื ืืื ืชืคืืืื ืืืื ืื ื ืืช ืืฉ ืืกื ืืื.",
|
| 113 |
-
"ืคืชืืืช ืกืืืจืืื ืืื ืืื ืืืืื ืืฆืง โ ืืืืฆืื ืืืจืืืืื ืขื ืฉืืื ืฉืืื ืื ืจืื.",
|
| 114 |
-
],
|
| 115 |
-
closing="ืืจืืืช ืืืืืื ืื ืืืืื ืืช ืืกืืกืืช ืฉืชืฉืชืืฉ ืื ืฉืื ืืฉืื โ ืืืืจ ืฉืืืช ืื.",
|
| 116 |
-
),
|
| 117 |
-
|
| 118 |
-
"FIND_DERIVATIVE": SemanticEntry(
|
| 119 |
-
concept_tag="FIND_DERIVATIVE",
|
| 120 |
-
openings=[
|
| 121 |
-
"ืื ืืืจืช ืืืืจืช ืื ื ืืื ืืืจ ืืคืื ืงืฆืื ืขืืื ืื ืืืจืืช โ ืืื ืืื 'ืื ืืืืืจืืช' ืฉื ืืืจืฃ.",
|
| 122 |
-
"ืืื ืืืฆืื ืืช ืื ืืืจืช, ืื ืื ื ืฉืืืืื: ืื x ืื ืงืฆืช ืงืืืื โ ืืื y ืืฉืชื ื?",
|
| 123 |
-
"ื ืืฉื ืืช ืื ืืืจืช ืืคื ืืืื ืืืืืจื ืฉื ืืืื โ ืื ืืจืื ืืืชืจ ืืืืจ ืืืืืืจื ืืืกืืกืืช.",
|
| 124 |
-
],
|
| 125 |
-
bridges=[
|
| 126 |
-
"ืืคื ืืื ืืืืืจื ืืจืฉืืืช ืื ืืืจืืช, ืงืืืื ื:",
|
| 127 |
-
"ืื ืืืจืช, ืฉืืืืฆืืช ืืช ืฉืืคืืข ืืืฉืืง ืืคืื ืงืฆืื, ืืื:",
|
| 128 |
-
"ืงืฆื ืืฉืื ืื ืืืืืื ืฉื ืืคืื ืงืฆืื ืืื:",
|
| 129 |
-
],
|
| 130 |
-
analogies=[
|
| 131 |
-
"ืื ืืคืื ืงืฆืื ืืื ืืกืืื ื ืกืืขื, ืื ืืืจืช ืืื ืืื-ืืืืจืืช โ ืืื ืืืืจืช ืื ืืื ืืืจ ืืชื ื ืข ืืื ืจืืข.",
|
| 132 |
-
"ืืืืื ืฉืืชื ืืืคืก ืขื ืืจ โ ืื ืืืจืช ืืืืจืช ืื ืืื ื ืงืืื ืขื ืืื ืืชืืืืืช. ืืืืืืช = ืขืืืื, ืฉืืืืืช = ืืืจืืื.",
|
| 133 |
-
],
|
| 134 |
-
closing="ืื ืืืจืช ืคืืชืืช ืขืืื ืฉืื โ ืื ืืชืื ืงืฆืืืช ืืขื ืืื ืืงื. ืื ืืืืื ืขื ืืืืืื ืืช ืืื.",
|
| 135 |
-
),
|
| 136 |
-
|
| 137 |
-
"FIND_INTEGRAL": SemanticEntry(
|
| 138 |
-
concept_tag="FIND_INTEGRAL",
|
| 139 |
-
openings=[
|
| 140 |
-
"ืืืื ืืืจื ืืื ืืคืขืืื ืืืคืืื ืืืืืจื โ ืื ืื ื ืฉืืืืื 'ืื ืืคืื ืงืฆืื ืฉืื ืืืจืช ืฉืื ืืื ืื?'.",
|
| 141 |
-
"ืืื ืืืฉื ืืช ืืืื ืืืจื, ื ืฉืชืืฉ ืื ืืกืืืืช ืืืกืืกืืืช ืืืืืง ื-C, ื-ืงืืืข ืืืื ืืืจืฆืื.",
|
| 142 |
-
"ืืืื ืืืจื ืืื ืืกืืื ื ืืชื ืื ื ืืฉืคืื ืฉืืื ืฉื ืคืื ืงืฆืืืช โ ืฉืื ืืช ืื ืืื ืืงืืืข ืืืื.",
|
| 143 |
-
],
|
| 144 |
-
bridges=[
|
| 145 |
-
"ืืืจื ืืื ืืืจืฆืื ืืคื ืืื ืืืคืืื ืฉื ืื ืืืจืช:",
|
| 146 |
-
"ืืคืื ืงืฆืื ืืคืจืืืืืืืืช ืฉืืจืืืื ืืช ืืืื ืืืจื ืืื:",
|
| 147 |
-
"ืืืฆืืข ืืืื ืืืจืฆืื ื ืืชื ืื ื:",
|
| 148 |
-
],
|
| 149 |
-
analogies=[
|
| 150 |
-
"ืื ืื ืืืจืช ืืื ืื-ืืืืืจืืช, ืืืื ืืืจื ืืื ืื-ืืืจืืง โ ืืื ืืืกืฃ ืืช ืื ืืฉืื ืืืื ืืงืื ืื ืืกืืื ืืื.",
|
| 151 |
-
"ืืฉืื ืขื ืืื ืืืจืฆืื ืืื ืืืืื ืกืจื ืืืืืจ โ ืื ืื ื 'ืืืืืจืื' ืืช ืืืืืจื ืื ืงืืืช ืืืืฆื ืฉืื.",
|
| 152 |
-
],
|
| 153 |
-
closing="ืืื ืืืจืฆืื ืืื ืื ืืฉืืื ืืื ืคืื ืืืกืืืื โ ืืขืืฉืื ืืฉ ืื ืืช ืืืื ืืืฉื ืฉืืืื, ื ืคืืื ืืขืื.",
|
| 154 |
-
),
|
| 155 |
-
|
| 156 |
-
"SUBSTITUTE": SemanticEntry(
|
| 157 |
-
concept_tag="SUBSTITUTE",
|
| 158 |
-
openings=[
|
| 159 |
-
"ืืื ืืืืืง ืืช ืืคืชืจืื (ืื ืืืฉืืื ืืืฉืื), ื ืฆืื ืืช ืืขืจื ืืืืืข ืื ืคืฉื.",
|
| 160 |
-
"ืืืฆืื ืืื ืืืจื ืฉืื ื ืืืืจ ืืื ืฉื ื ืืืงื ืืืขืื โ ืืฆืืืื ืื ืฉืืืืขืื ืืจืืืื ืื ืืืฆื.",
|
| 161 |
-
"ื ืืืืฃ ืืช ืืืฉืชื ื ืืขืจื ืื ืชืื ืื ืืฉื โ ืื ืืฉืืืจ ืื ื ืืืืื ืืจืื ืืืชืจ ืคืฉืื.",
|
| 162 |
-
],
|
| 163 |
-
bridges=[
|
| 164 |
-
"ืืืืจ ืืืฆืื ืืืคืืฉืื:",
|
| 165 |
-
"ืืฉื ืฆืื ืื ืคืฉื ืืช ืืืืืื ืฉืงืืืื ื:",
|
| 166 |
-
"ืขืจื ืืืฆืื ืื ืื:",
|
| 167 |
-
],
|
| 168 |
-
analogies=[
|
| 169 |
-
"ืืฆืื ืืื ืืื ืืืื ืฉื ืืืืคืก โ ืื ืื ื ืืืืืคืื ืืช 'ืื ืขืื' ืืขืจื ืืืืฉื ืฉืืฆืื ื.",
|
| 170 |
-
"ืืืืื ืืชืืื ืฉืืืืจ 'ืกืคื ืกืืืจ' โ ืืจืืข ืฉืืชื ืืืืข ืืื ืืืื ืืกืคื, ืืชื ืืืื ืืืฉื ืืืืช ืืืืืงืช.",
|
| 171 |
-
],
|
| 172 |
-
closing="ืืฆืืื! ืืืฆืื ืืื ืื ืืจื ืืฆืืื ืช ืืืืืง ืฉืืคืชืจืื ืฉืืฆืืช ืืื ื ืืื.",
|
| 173 |
-
),
|
| 174 |
-
|
| 175 |
-
# โโ V7.3: Geometry / Analytic Entries โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 176 |
-
|
| 177 |
-
"FIND_AXIS_INTERSECTIONS": SemanticEntry(
|
| 178 |
-
concept_tag="FIND_AXIS_INTERSECTIONS",
|
| 179 |
-
openings=[
|
| 180 |
-
"ืืื ืืืืืช ืืืคื ืืืขืื ืคืืืฉ ืืช ืืฆืืจืื, ื ืฉืื ืฉืืื ืคืฉืืื: ืื ืงืืจื ืืฉื ืงืืื ื ืืฆืืช ืืืฉ ืขื ืฆืืจ ื-x? ื-y ืฉืื ืฉืืื ืืืคืก! ืืืืคื.",
|
| 181 |
-
"ื ืงืืืืช ืืืชืื ืขื ืืฆืืจืื ืื ืื ืงืืืืช ืฉืืื ืืืขืื ืืืฆื ืืช 'ืืืืืฉืื' ืฉื ืืขืจืืช ืืฆืืจืื.",
|
| 182 |
-
"ืืืจื ืืืชืจ ืืช ื ืงืืืืช ืืืืชืื ืขื ืืฆืืจืื ืืื ืืืฆืื ืืคืก: ืคืขื ืขืืืจ x ืืคืขื ืขืืืจ y, ืืืจืืืช ืื ืืคืชืจืื ืฉืืืฆื.",
|
| 183 |
-
],
|
| 184 |
-
bridges=[
|
| 185 |
-
"ืืฉื ืฆืื ืื ืคืชืืจ, ืื ืงืืืืช ืฉืืฆืื ื ืื:",
|
| 186 |
-
"ืืืืจ ืืืฆืื ืืงืืืช ืืคืชืจืื ืืช, ื ืงืืืืช ืืืืชืื ืขื ืืฆืืจืื ืื:",
|
| 187 |
-
"ืืคืชืจืื ืืช ืฉืงืืืื ื ืืฆืืื ืื ืืช ืื ืงืืืืช ืฉืืื ืืืขืื ืืืฆื ืืช ืืฆืืจืื:",
|
| 188 |
-
],
|
| 189 |
-
analogies=[
|
| 190 |
-
"ืืฉืื ืขื ืืขืื ืืื ืืืืจ ืฉืืชืืืื ืขื ืจืฆืคื โ ื ืงืืืืช ืืืืชืื ืขื ืฆืืจ ื-x ืื ืืืืืง ืืืงืืืืช ืฉืืืืืจ ื ืืืข ืืจืฆืคื.",
|
| 191 |
-
"ืืืืื ืฉืืชื ืืกืชืื ืขื ืืคื ืืืืคืฉ ืืืคื ืืืืฉ ืจืืฉื ืืืฆื ืืช ืื ืืจ โ ืื ืืืืืง ืื ืฉืื ืื ื ืขืืฉืื ืขื ืืืขืื ืืืฆืืจืื.",
|
| 192 |
-
],
|
| 193 |
-
closing="ืืฆืื ื ืืช ืื ื ืงืืืืช ืืืืข ืฉื ืืืขืื ืขื ืืขืจืืช ืืฆืืจืื โ ืขืืืื ืืืืืงืช!",
|
| 194 |
-
),
|
| 195 |
-
|
| 196 |
-
"CALCULATE_SLOPE_AND_LINE": SemanticEntry(
|
| 197 |
-
concept_tag="CALCULATE_SLOPE_AND_LINE",
|
| 198 |
-
openings=[
|
| 199 |
-
"ืืื ืืืฆืื ืืช ืืฉืืืืช ืืืฉืจ ืืขืืืจ ืืจื ืฉืชื ื ืงืืืืช, ื ืืฉื ืงืืื ืืช ืืฉืืคืืข โ ืืื 'ืชืืื' ืืืฉืจ.",
|
| 200 |
-
"ืฉืืคืืข ืืืฉืจ ืืื ืืืืก ืืื ืืฉืื ืื ืืืืื ืืฉืื ืื ืืืืงืฃ: ืขืืืื ืืืงื ืืืื ืืืคืงืืช.",
|
| 201 |
-
"ืืืฉืจ ืฉืืืืจ ืฉืชื ื ืงืืืืช ืืืืืจ ืืืืืืื ืขื ืืื ืืฉืืคืืข ืฉืื ืื ืงืืืช ืืขืืจ ืืืช.",
|
| 202 |
-
],
|
| 203 |
-
bridges=[
|
| 204 |
-
"ืืืืจ ืืืฉืื ืืฉืืคืืข, ืืฉืืืืช ืืืฉืจ ืืื:",
|
| 205 |
-
"ืืฉื ืืกืืฃ ืืช ืืฉืืคืืข ืฉืืฆืื ื ืื ืืกืืช ืืืฉืจ, ื ืงืื:",
|
| 206 |
-
"ืืืฉืจ ืฉืืืืจ ืืจืื ืืืขืื ืืจืืฉืืช ืืฆืืจืื ืืื:",
|
| 207 |
-
],
|
| 208 |
-
analogies=[
|
| 209 |
-
"ืฉืืคืืข ืืื ืืื ืืืจืืืช โ ืื ืขืืืื ืืจืืข ืงืืืืช ืืื ืฉืืืฉื ืฆืขืืื ืงืืืื, ืืฉืืคืืข ืืื ืืจืืขื ืืืงื ืฉืืืฉื.",
|
| 210 |
-
"ืืืืื ืืฉืจ ืืื ืืจื ืืื ืฉืชื ืขืจืื โ ืืฉืืคืืข ืืื ืืื ืืืจืื ืขืืืื ืืื ืงืืืืืืจ ืฉื ืืกืขืื.",
|
| 211 |
-
],
|
| 212 |
-
closing="ืงืืืื ื ืืช ืืฉืืืืช ืืืฉืจ ืืฆืืจื ืืืคืืจืฉืช โ ืงื ืืฉืจ ืฉืขืืืจ ืืืืืง ืืจื ืฉืชื ืื ืงืืืืช ืื ืชืื ืืช.",
|
| 213 |
-
),
|
| 214 |
-
|
| 215 |
-
"CALCULATE_DISTANCE": SemanticEntry(
|
| 216 |
-
concept_tag="CALCULATE_DISTANCE",
|
| 217 |
-
openings=[
|
| 218 |
-
"ืืื ืืืืืง ืืื ื ืงืืื ื ืืฆืืช ืขื ืืืขืื, ื ืืืื ืืช ืืืจืืง ืืื ื ืืืื ืืจืื ืืืขืื ืื ืฉืืื ืืจืืืืก.",
|
| 219 |
-
"ื ืืกืืช ืืืจืืง ืืื ืฉืชื ื ืงืืืืช ืืื ืฉืืจืฉ ืฉื ืกืืื ืจืืืืขื ืืืคืจืฉืื โ ืืืืืง ืืื ืืฉืคื ืคืืชืืืจืก.",
|
| 220 |
-
"ืืฉืืื ืคืฉืืื: ืืื ืื ืงืืื ื ืืฆืืช ืืืืืง ืืจืืืืก ืืืืจืื, ืงืจืื ืืืชืจ, ืื ืจืืืง ืืืชืจ?",
|
| 221 |
-
],
|
| 222 |
-
bridges=[
|
| 223 |
-
"ืืืืจ ืืืฉืื ืืืจืืง, ืืืืฆื ืืื:",
|
| 224 |
-
"ืืฉืืฉืืืื ืืช ืืืจืืง ืฉืืฆืื ื ืืจืืืืก ืืืขืื, ืืชืืจืจ ืฉ:",
|
| 225 |
-
"ืชืืฆืืช ืืืฉืื ื๏ฟฝ๏ฟฝืจืืง ืืื ืืจืืืืก:",
|
| 226 |
-
],
|
| 227 |
-
analogies=[
|
| 228 |
-
"ืืืืื ืืืืื ืขืืืื โ ืื ื ืงืืื ืขื ืืื ื ืืฆืืช ืืืืืง ืืืืชื ืืจืืง ืืืืจืื. ืื ื ืงืืื ืงืจืืื ืืืชืจ โ ืืื ืืชืืื. ืจืืืงื ืืืชืจ โ ืืืืฆื ืื.",
|
| 229 |
-
"ืืจืืง ืฉืชื ื ืงืืืืช ืื ืืื ืืืืื ืขื ืกืจืื ืืื ืฉืชื ืขืจืื ืขื ืืคื โ ืคืืชืืืจืก ืขืืฉื ืืช ืืขืืืื.",
|
| 230 |
-
],
|
| 231 |
-
closing="ืืืงื ื ืืืืืง: ืืืจืืง ืืืฉื, ืืืฉืืื ืืจืืืืก, ืืืืงืื ืื ืงืืื ื ืงืืข ืืืืืืืช ืืชืืืืช.",
|
| 232 |
-
),
|
| 233 |
-
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
# โโโ Diversity Engine โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 238 |
-
|
| 239 |
-
class DiversityEngine:
|
| 240 |
-
"""
|
| 241 |
-
Selects narrative variants from the SemanticBank with diversity enforcement.
|
| 242 |
-
|
| 243 |
-
Tracks selection history per concept to detect "narrative laziness" (Drift):
|
| 244 |
-
if one variant is chosen more than DRIFT_ALERT_THRESHOLD fraction of the time,
|
| 245 |
-
emit a PEDAGOGICAL_DRIFT telemetry alert.
|
| 246 |
-
"""
|
| 247 |
-
|
| 248 |
-
def __init__(self):
|
| 249 |
-
# Counter per concept_tag: tracks how many times each (variant_type, index) was picked
|
| 250 |
-
self._history: Dict[str, Counter] = {}
|
| 251 |
-
|
| 252 |
-
def _record(self, concept_tag: str, variant_key: str):
|
| 253 |
-
if concept_tag not in self._history:
|
| 254 |
-
self._history[concept_tag] = Counter()
|
| 255 |
-
self._history[concept_tag][variant_key] += 1
|
| 256 |
-
|
| 257 |
-
def drift_score(self, concept_tag: str) -> float:
|
| 258 |
-
"""
|
| 259 |
-
Returns the dominance fraction of the most-selected variant for this concept.
|
| 260 |
-
Score = max_count / total_count.
|
| 261 |
-
0.0 = perfectly uniform, 1.0 = always the same variant.
|
| 262 |
-
"""
|
| 263 |
-
if concept_tag not in self._history or not self._history[concept_tag]:
|
| 264 |
-
return 0.0
|
| 265 |
-
counts = self._history[concept_tag]
|
| 266 |
-
total = sum(counts.values())
|
| 267 |
-
if total == 0:
|
| 268 |
-
return 0.0
|
| 269 |
-
return max(counts.values()) / total
|
| 270 |
-
|
| 271 |
-
def _pick(self, concept_tag: str, variant_type: str, options: List[str]) -> str:
|
| 272 |
-
"""
|
| 273 |
-
Weighted random selection that avoids the most recently over-used variants.
|
| 274 |
-
Falls back to uniform random if history is thin (< 5 picks).
|
| 275 |
-
"""
|
| 276 |
-
n = len(options)
|
| 277 |
-
history_key_prefix = f"{variant_type}:"
|
| 278 |
-
if concept_tag not in self._history or sum(self._history[concept_tag].values()) < 5:
|
| 279 |
-
# Not enough history โ uniform random
|
| 280 |
-
idx = random.randrange(n)
|
| 281 |
-
else:
|
| 282 |
-
counts = self._history[concept_tag]
|
| 283 |
-
# Weight = inverse of how often we've picked this index
|
| 284 |
-
weights = []
|
| 285 |
-
for i in range(n):
|
| 286 |
-
pick_count = counts.get(f"{history_key_prefix}{i}", 0)
|
| 287 |
-
weights.append(1.0 / (1 + pick_count))
|
| 288 |
-
# Weighted choice
|
| 289 |
-
total_w = sum(weights)
|
| 290 |
-
r = random.uniform(0, total_w)
|
| 291 |
-
cumulative = 0.0
|
| 292 |
-
idx = n - 1 # fallback
|
| 293 |
-
for i, w in enumerate(weights):
|
| 294 |
-
cumulative += w
|
| 295 |
-
if r <= cumulative:
|
| 296 |
-
idx = i
|
| 297 |
-
break
|
| 298 |
-
|
| 299 |
-
key = f"{history_key_prefix}{idx}"
|
| 300 |
-
self._record(concept_tag, key)
|
| 301 |
-
|
| 302 |
-
# Check drift and emit telemetry
|
| 303 |
-
score = self.drift_score(concept_tag)
|
| 304 |
-
if score > DRIFT_ALERT_THRESHOLD:
|
| 305 |
-
logger.warning(
|
| 306 |
-
f"[DRIFT_MONITOR] Pedagogical drift detected for '{concept_tag}': "
|
| 307 |
-
f"drift_score={score:.2f} > threshold={DRIFT_ALERT_THRESHOLD}. "
|
| 308 |
-
f"Selection history: {dict(self._history[concept_tag])}"
|
| 309 |
-
)
|
| 310 |
-
telemetry.emit_pedagogical_drift(concept_tag, score)
|
| 311 |
-
|
| 312 |
-
return options[idx]
|
| 313 |
-
|
| 314 |
-
def compose(self, action: str, signed_steps: list) -> str:
|
| 315 |
-
"""
|
| 316 |
-
Main entry point: builds a full Hebrew pedagogical narrative for the given action.
|
| 317 |
-
Inserts {{step_id}} placeholders where the server results will be shown.
|
| 318 |
-
|
| 319 |
-
Returns a string with {{placeholder}} notation (not yet injected).
|
| 320 |
-
"""
|
| 321 |
-
entry = SEMANTIC_BANK.get(action)
|
| 322 |
-
if entry is None:
|
| 323 |
-
logger.warning(
|
| 324 |
-
f"[SEMANTIC_BANK] No entry for action '{action}'. "
|
| 325 |
-
f"Falling back to LLM #2 renderer."
|
| 326 |
-
)
|
| 327 |
-
return None # Signal to caller: use LLM fallback
|
| 328 |
-
|
| 329 |
-
concept_tag = entry.concept_tag
|
| 330 |
-
|
| 331 |
-
# Pick diverse variants
|
| 332 |
-
opening = self._pick(concept_tag, "opening", entry.openings)
|
| 333 |
-
bridge = self._pick(concept_tag, "bridge", entry.bridges)
|
| 334 |
-
analogy = self._pick(concept_tag, "analogy", entry.analogies)
|
| 335 |
-
closing = entry.closing # Always the same (one closing per concept by design)
|
| 336 |
-
|
| 337 |
-
# Build placeholder string from signed steps
|
| 338 |
-
if not signed_steps:
|
| 339 |
-
placeholder_str = ""
|
| 340 |
-
elif len(signed_steps) == 1:
|
| 341 |
-
placeholder_str = f"{{{{{signed_steps[0]['id']}}}}}"
|
| 342 |
-
else:
|
| 343 |
-
parts = [f"{{{{{s['id']}}}}}" for s in signed_steps]
|
| 344 |
-
placeholder_str = " โ ".join(parts)
|
| 345 |
-
|
| 346 |
-
# Compose the narrative (V7.2.5: no emojis/em-dashes โ must pass scan_for_math_leakage)
|
| 347 |
-
narrative = (
|
| 348 |
-
f"{opening}\n\n"
|
| 349 |
-
f"{analogy}\n\n"
|
| 350 |
-
f"{bridge} {placeholder_str}\n\n"
|
| 351 |
-
f"{closing}"
|
| 352 |
-
)
|
| 353 |
-
|
| 354 |
-
logger.info(
|
| 355 |
-
f"[SEMANTIC_BANK] Composed narrative for '{action}' | "
|
| 356 |
-
f"drift_score={self.drift_score(concept_tag):.2f}"
|
| 357 |
-
)
|
| 358 |
-
return narrative
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
# Module-level singleton โ shared across all requests in the process
|
| 362 |
-
_engine = DiversityEngine()
|
| 363 |
-
|
| 364 |
-
def get_diversity_engine() -> DiversityEngine:
|
| 365 |
-
"""Returns the process-level DiversityEngine singleton."""
|
| 366 |
-
return _engine
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/step_types.py
DELETED
|
@@ -1,71 +0,0 @@
|
|
| 1 |
-
# domain/step_types.py - V7.4 (TYPED DOMAIN CONTRACTS)
|
| 2 |
-
"""
|
| 3 |
-
V7.4: Typed SignedStep โ Domain-Aware Validation Contract.
|
| 4 |
-
|
| 5 |
-
Replaces the raw dict `{"id", "hash", "expression"}` with a typed dataclass
|
| 6 |
-
that carries a StepType so the ConsistencyGate validates by semantic domain,
|
| 7 |
-
not by string shape.
|
| 8 |
-
|
| 9 |
-
CTO directive:
|
| 10 |
-
- expression โ display string only (for renderer injection)
|
| 11 |
-
- payload โ semantic truth (list[str] for geometry, str for algebraic)
|
| 12 |
-
- step_type โ routing key for ConsistencyGate dispatch
|
| 13 |
-
|
| 14 |
-
Backwards compatibility: SignedStep emulates a dict fully so ALL existing
|
| 15 |
-
dict-access code (step["id"], step.get("hash"), "id" in step, step.items())
|
| 16 |
-
in pedagogical_renderer.py / semantic_bank.py continues to work unchanged.
|
| 17 |
-
"""
|
| 18 |
-
from enum import Enum
|
| 19 |
-
from dataclasses import dataclass, asdict
|
| 20 |
-
from typing import Any, Optional
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
class StepType(Enum):
|
| 24 |
-
ALGEBRAIC = "algebraic" # SymPy-parseable expression โ validated with sympify
|
| 25 |
-
GEOMETRY = "geometry" # Points, distances, labels โ validated structurally
|
| 26 |
-
NUMERIC = "numeric" # Pure numeric results โ future use
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
@dataclass
|
| 30 |
-
class SignedStep:
|
| 31 |
-
"""
|
| 32 |
-
V7.4: Typed, SHA256-signed computation step.
|
| 33 |
-
|
| 34 |
-
Fields:
|
| 35 |
-
id โ step identifier used as {{id}} placeholder in renderer
|
| 36 |
-
expression โ human-readable display string (injected into pedagogy text)
|
| 37 |
-
payload โ semantic truth: list[str] for geometry, str for algebraic
|
| 38 |
-
step_type โ StepType enum โ routes ConsistencyGate validation
|
| 39 |
-
hash โ SHA256 digest seeded with canonical form + problem_id + step_id
|
| 40 |
-
"""
|
| 41 |
-
id: str
|
| 42 |
-
expression: str
|
| 43 |
-
payload: Any
|
| 44 |
-
step_type: StepType
|
| 45 |
-
hash: str
|
| 46 |
-
|
| 47 |
-
# โโ Full dict emulation (CTO requirement) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 48 |
-
# Allows ALL existing code using step["key"], step.get(...), "key" in step,
|
| 49 |
-
# step.items() to work without any changes.
|
| 50 |
-
|
| 51 |
-
_FIELDS = ("id", "expression", "payload", "step_type", "hash")
|
| 52 |
-
|
| 53 |
-
def __getitem__(self, key: str):
|
| 54 |
-
if key not in self._FIELDS:
|
| 55 |
-
raise KeyError(key)
|
| 56 |
-
return getattr(self, key)
|
| 57 |
-
|
| 58 |
-
def get(self, key: str, default=None):
|
| 59 |
-
try:
|
| 60 |
-
return self[key]
|
| 61 |
-
except KeyError:
|
| 62 |
-
return default
|
| 63 |
-
|
| 64 |
-
def keys(self):
|
| 65 |
-
return list(self._FIELDS)
|
| 66 |
-
|
| 67 |
-
def items(self):
|
| 68 |
-
return [(k, self[k]) for k in self._FIELDS]
|
| 69 |
-
|
| 70 |
-
def __contains__(self, key: object) -> bool:
|
| 71 |
-
return key in self._FIELDS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/telemetry.py
DELETED
|
@@ -1,177 +0,0 @@
|
|
| 1 |
-
# domain/telemetry.py - V7.2 Observability Layer
|
| 2 |
-
"""
|
| 3 |
-
BuddyMath V7.2 Runtime Telemetry
|
| 4 |
-
Emits structured log-based metrics compatible with Datadog/Grafana log pipelines.
|
| 5 |
-
|
| 6 |
-
All metrics are emitted as structured JSON log lines on the logger named "buddymath.metrics".
|
| 7 |
-
DevOps should configure a log-based metric parser for lines containing "METRIC_EVENT".
|
| 8 |
-
|
| 9 |
-
Metric naming convention: <component>.<event_name>
|
| 10 |
-
"""
|
| 11 |
-
import logging
|
| 12 |
-
import json
|
| 13 |
-
import time
|
| 14 |
-
from datetime import datetime, timezone
|
| 15 |
-
from typing import Optional
|
| 16 |
-
|
| 17 |
-
metrics_logger = logging.getLogger("buddymath.metrics")
|
| 18 |
-
|
| 19 |
-
# ==================== METRIC KEY CONSTANTS ====================
|
| 20 |
-
# Use these constants everywhere to prevent typos and enable grep-ability.
|
| 21 |
-
|
| 22 |
-
class M:
|
| 23 |
-
"""V7.2 Metric Key Registry"""
|
| 24 |
-
|
| 25 |
-
# Core runtime outcome (feeds the Pie Chart)
|
| 26 |
-
RUNTIME_OUTCOME = "runtime.outcome" # values: success | hint_mode | planner_retry | leakage_fail | placeholder_fail | crs_block
|
| 27 |
-
|
| 28 |
-
# Fail Closed Rate (derived from RUNTIME_OUTCOME != success)
|
| 29 |
-
FAIL_CLOSED = "fail_closed" # emitted on any non-success outcome
|
| 30 |
-
|
| 31 |
-
# Planner (LLM #1)
|
| 32 |
-
PLANNER_RETRY = "planner.retry.count" # emitted on each retry attempt
|
| 33 |
-
PLANNER_ENUM_VIOLATION = "planner.enum_violation" # emitted when ComputeAction enum is violated
|
| 34 |
-
PLANNER_JSON_ERROR = "planner.json_error" # emitted on JSON parse / boundary failure
|
| 35 |
-
PLANNER_STRATEGY_DIST = "planner.strategy_distribution" # which Enum actions are chosen (drift detection)
|
| 36 |
-
|
| 37 |
-
# Renderer (LLM #2)
|
| 38 |
-
RENDERER_LEAKAGE_FAIL = "renderer.leakage_fail" # Whitelist scan failed
|
| 39 |
-
RENDERER_PLACEHOLDER_FAIL = "renderer.placeholder_violation" # Missing or invented placeholder
|
| 40 |
-
RENDERER_LEAKAGE_CHARS = "renderer.leakage_chars" # Offending chars (for slicing)
|
| 41 |
-
|
| 42 |
-
# Solver / Server Determinism
|
| 43 |
-
SOLVER_EXECUTION_MS = "solver.execution_time_ms" # Math engine wall-clock time
|
| 44 |
-
SIGNATURE_HASH_COLLISION = "signature.hash_collision" # MUST always be 0
|
| 45 |
-
|
| 46 |
-
# CRS Pre-Flight
|
| 47 |
-
CRS_BLOCK = "crs.preflight_block" # Emitted when CRS > 0.7 blocks LLM call
|
| 48 |
-
CRS_VALUE = "crs.value" # Raw CRS score for histogram
|
| 49 |
-
|
| 50 |
-
# Security
|
| 51 |
-
SUSPICIOUS_INPUT = "suspicious.input_pattern" # Potential prompt injection detected
|
| 52 |
-
|
| 53 |
-
# Pedagogical Diversity (V7.2.5)
|
| 54 |
-
PEDAGOGICAL_DRIFT = "pedagogical.drift_score" # Narrative laziness: > 0.35 = same template always chosen
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
# ==================== EMITTER ====================
|
| 58 |
-
|
| 59 |
-
def emit(metric: str, value, tags: Optional[dict] = None):
|
| 60 |
-
"""
|
| 61 |
-
Emit a single metric event as a structured JSON log line.
|
| 62 |
-
Datadog/Grafana log-based metric pipelines should parse lines with 'METRIC_EVENT'.
|
| 63 |
-
|
| 64 |
-
Format:
|
| 65 |
-
{"event": "METRIC_EVENT", "metric": "<name>", "value": <v>, "tags": {...}, "timestamp": "..."}
|
| 66 |
-
"""
|
| 67 |
-
payload = {
|
| 68 |
-
"event": "METRIC_EVENT",
|
| 69 |
-
"metric": metric,
|
| 70 |
-
"value": value,
|
| 71 |
-
"tags": tags or {},
|
| 72 |
-
"timestamp": datetime.now(timezone.utc).isoformat()
|
| 73 |
-
}
|
| 74 |
-
metrics_logger.info(json.dumps(payload, ensure_ascii=False))
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def emit_runtime_outcome(outcome: str, problem_id: str = "unknown", grade: str = "unknown"):
|
| 78 |
-
"""Feeds the main Pie Chart and Fail Closed Rate gauge."""
|
| 79 |
-
emit(M.RUNTIME_OUTCOME, outcome, {"problem_id": problem_id, "grade": grade})
|
| 80 |
-
if outcome != "success":
|
| 81 |
-
emit(M.FAIL_CLOSED, 1, {"reason": outcome})
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def emit_planner_retry(attempt: int, reason: str):
|
| 85 |
-
emit(M.PLANNER_RETRY, attempt, {"reason": reason})
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def emit_planner_error(error_type: str, details: str = ""):
|
| 89 |
-
"""error_type: 'enum_violation' | 'json_error'"""
|
| 90 |
-
key = M.PLANNER_ENUM_VIOLATION if error_type == "enum_violation" else M.PLANNER_JSON_ERROR
|
| 91 |
-
emit(key, 1, {"details": details[:120]})
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def emit_planner_strategy_distribution(actions: list):
|
| 95 |
-
"""
|
| 96 |
-
Phase 1 Live: Emit one metric event per chosen Enum action.
|
| 97 |
-
Feeds the planner.strategy_distribution dashboard panel.
|
| 98 |
-
A sudden shift in distribution signals Model Drift.
|
| 99 |
-
Example: [SOLVE_EQUATION, SIMPLIFY, SOLVE_EQUATION] โ 3 events
|
| 100 |
-
"""
|
| 101 |
-
for action in actions:
|
| 102 |
-
emit(M.PLANNER_STRATEGY_DIST, 1, {"action": str(action)})
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def emit_renderer_leakage(offending_chars: str):
|
| 106 |
-
emit(M.RENDERER_LEAKAGE_FAIL, 1, {"offending_chars": offending_chars[:50]})
|
| 107 |
-
emit(M.RENDERER_LEAKAGE_CHARS, offending_chars[:50])
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
def emit_renderer_placeholder_violation(violation_type: str, missing_id: str = ""):
|
| 111 |
-
"""violation_type: 'missing' | 'invented'"""
|
| 112 |
-
emit(M.RENDERER_PLACEHOLDER_FAIL, 1, {"type": violation_type, "id": missing_id})
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
def emit_solver_timing(start_time: float):
|
| 116 |
-
"""Call with time.time() snapshot taken BEFORE solver runs."""
|
| 117 |
-
elapsed_ms = round((time.time() - start_time) * 1000, 2)
|
| 118 |
-
emit(M.SOLVER_EXECUTION_MS, elapsed_ms)
|
| 119 |
-
return elapsed_ms
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
def emit_hash_collision(step_id: str, problem_id: str):
|
| 123 |
-
"""
|
| 124 |
-
CRITICAL: This MUST never be emitted in a correct system.
|
| 125 |
-
If it fires, it means two different expressions produced the same hash โ P0 alert.
|
| 126 |
-
"""
|
| 127 |
-
metrics_logger.critical(
|
| 128 |
-
json.dumps({
|
| 129 |
-
"event": "METRIC_EVENT",
|
| 130 |
-
"metric": M.SIGNATURE_HASH_COLLISION,
|
| 131 |
-
"value": 1,
|
| 132 |
-
"tags": {"step_id": step_id, "problem_id": problem_id},
|
| 133 |
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
| 134 |
-
"severity": "P0_CRITICAL"
|
| 135 |
-
})
|
| 136 |
-
)
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
def emit_crs_block(crs_value: float, problem_id: str = "unknown"):
|
| 140 |
-
emit(M.CRS_BLOCK, 1, {"crs": crs_value, "problem_id": problem_id})
|
| 141 |
-
emit(M.CRS_VALUE, crs_value)
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
def emit_crs_value(crs_value: float):
|
| 145 |
-
emit(M.CRS_VALUE, crs_value)
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
def emit_suspicious_input(pattern: str, problem_id: str = "unknown"):
|
| 149 |
-
emit(M.SUSPICIOUS_INPUT, 1, {"pattern": pattern[:80], "problem_id": problem_id})
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
def emit_pedagogical_drift(concept_tag: str, drift_score: float):
|
| 153 |
-
"""
|
| 154 |
-
V7.2.5: Emitted when DiversityEngine detects narrative laziness.
|
| 155 |
-
drift_score > 0.35 means one template variant is dominating selection.
|
| 156 |
-
Alert DevOps: Renderer is repeating the same pedagogical phrasing โ student experience degrades.
|
| 157 |
-
"""
|
| 158 |
-
emit(M.PEDAGOGICAL_DRIFT, round(drift_score, 3), {"concept": concept_tag})
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
# ==================== TIMER CONTEXT MANAGER ====================
|
| 162 |
-
|
| 163 |
-
class SolverTimer:
|
| 164 |
-
"""
|
| 165 |
-
Context manager for measuring Math Engine execution time.
|
| 166 |
-
Usage:
|
| 167 |
-
with SolverTimer() as t:
|
| 168 |
-
result = sympy_solve(...)
|
| 169 |
-
print(t.elapsed_ms)
|
| 170 |
-
"""
|
| 171 |
-
def __enter__(self):
|
| 172 |
-
self._start = time.time()
|
| 173 |
-
return self
|
| 174 |
-
|
| 175 |
-
def __exit__(self, *args):
|
| 176 |
-
self.elapsed_ms = round((time.time() - self._start) * 1000, 2)
|
| 177 |
-
emit(M.SOLVER_EXECUTION_MS, self.elapsed_ms)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
domain/validator.py
DELETED
|
@@ -1,148 +0,0 @@
|
|
| 1 |
-
# domain/validator.py - V7.4 (DOMAIN-AWARE CONSISTENCY GATE)
|
| 2 |
-
"""
|
| 3 |
-
V7.4 ConsistencyGate: Domain-Aware Validation.
|
| 4 |
-
|
| 5 |
-
Validates by StepType โ NOT by string shape.
|
| 6 |
-
|
| 7 |
-
ALGEBRAIC โ sympy.sympify() (existing behaviour, unchanged)
|
| 8 |
-
GEOMETRY โ structural validation of payload (list[str])
|
| 9 |
-
NUMERIC โ {future expansion point}
|
| 10 |
-
|
| 11 |
-
CTO directive:
|
| 12 |
-
- Geometry steps must NEVER reach sympify (Category Error prevention)
|
| 13 |
-
- Geometry gets its own structural validator, not a "skip blind"
|
| 14 |
-
- expression is display-only โ validation uses payload for geometry
|
| 15 |
-
"""
|
| 16 |
-
import sympy
|
| 17 |
-
import logging
|
| 18 |
-
from typing import List, Tuple
|
| 19 |
-
from domain.step_types import StepType, SignedStep
|
| 20 |
-
|
| 21 |
-
logger = logging.getLogger(__name__)
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
class ConsistencyGate:
|
| 25 |
-
"""
|
| 26 |
-
V7.4 Wall 3: Post-Execution Domain-Aware Consistency Gate.
|
| 27 |
-
|
| 28 |
-
Receives the list of SignedStep objects from the Deterministic Solver and
|
| 29 |
-
verifies structural and logical integrity before passing to the Renderer.
|
| 30 |
-
|
| 31 |
-
Checks performed (all domains):
|
| 32 |
-
1. Non-empty: at least one signed step exists.
|
| 33 |
-
2. Hash integrity: every step has a non-empty SHA256 hash.
|
| 34 |
-
3a. ALGEBRAIC: expression is SymPy-parseable (sympy.sympify).
|
| 35 |
-
3b. GEOMETRY: payload is a non-empty list[str] (structural validation).
|
| 36 |
-
"""
|
| 37 |
-
|
| 38 |
-
@staticmethod
|
| 39 |
-
def _validate_algebraic(step: SignedStep) -> Tuple[bool, str]:
|
| 40 |
-
"""
|
| 41 |
-
Validates an ALGEBRAIC step by attempting sympy.sympify on each
|
| 42 |
-
sub-expression (split on " OR " for multi-solution results).
|
| 43 |
-
"""
|
| 44 |
-
expr_str = step.expression
|
| 45 |
-
if not expr_str:
|
| 46 |
-
logger.error(f"[CONSISTENCY_GATE] Step '{step.id}' has an empty expression.")
|
| 47 |
-
return False, f"EMPTY_EXPRESSION:{step.id}"
|
| 48 |
-
|
| 49 |
-
parts = [p.strip() for p in expr_str.split(" OR ")]
|
| 50 |
-
for part in parts:
|
| 51 |
-
try:
|
| 52 |
-
sympy.sympify(part)
|
| 53 |
-
except Exception as e:
|
| 54 |
-
logger.error(
|
| 55 |
-
f"[CONSISTENCY_GATE] Step '{step.id}' expression not parseable: "
|
| 56 |
-
f"'{part}' โ {e}"
|
| 57 |
-
)
|
| 58 |
-
return False, f"UNPARSEABLE_EXPRESSION:{step.id}"
|
| 59 |
-
return True, ""
|
| 60 |
-
|
| 61 |
-
@staticmethod
|
| 62 |
-
def _validate_geometry(step: SignedStep) -> Tuple[bool, str]:
|
| 63 |
-
"""
|
| 64 |
-
Validates a GEOMETRY step structurally via payload.
|
| 65 |
-
|
| 66 |
-
Geometry output (points, distances, labels) is NOT SymPy-parseable.
|
| 67 |
-
We verify:
|
| 68 |
-
- payload is a non-empty list
|
| 69 |
-
- every element is a string
|
| 70 |
-
Hash integrity (checked earlier) is the cryptographic guarantee.
|
| 71 |
-
"""
|
| 72 |
-
payload = step.payload
|
| 73 |
-
if not isinstance(payload, list) or len(payload) == 0:
|
| 74 |
-
logger.error(
|
| 75 |
-
f"[CONSISTENCY_GATE] Geometry step '{step.id}' has empty or non-list payload."
|
| 76 |
-
)
|
| 77 |
-
return False, f"GEOMETRY_EMPTY_PAYLOAD:{step.id}"
|
| 78 |
-
if not all(isinstance(p, str) for p in payload):
|
| 79 |
-
logger.error(
|
| 80 |
-
f"[CONSISTENCY_GATE] Geometry step '{step.id}' payload contains non-string items."
|
| 81 |
-
)
|
| 82 |
-
return False, f"GEOMETRY_INVALID_PAYLOAD_TYPE:{step.id}"
|
| 83 |
-
logger.info(
|
| 84 |
-
f"[CONSISTENCY_GATE] Geometry step '{step.id}' passed structural validation "
|
| 85 |
-
f"({len(payload)} point(s))."
|
| 86 |
-
)
|
| 87 |
-
return True, ""
|
| 88 |
-
|
| 89 |
-
@staticmethod
|
| 90 |
-
def validate(
|
| 91 |
-
signed_steps: List,
|
| 92 |
-
ast_registry: dict,
|
| 93 |
-
problem_id: str
|
| 94 |
-
) -> Tuple[bool, str]:
|
| 95 |
-
"""
|
| 96 |
-
Returns (True, "") if all checks pass.
|
| 97 |
-
Returns (False, reason) if any check fails โ caller must Fail Closed.
|
| 98 |
-
|
| 99 |
-
Accepts both SignedStep objects and legacy dicts for backwards compatibility
|
| 100 |
-
during any partial migration.
|
| 101 |
-
"""
|
| 102 |
-
# Check 1: Non-empty output
|
| 103 |
-
if not signed_steps:
|
| 104 |
-
logger.error("[CONSISTENCY_GATE] No signed steps produced by solver.")
|
| 105 |
-
return False, "EMPTY_SOLVER_OUTPUT"
|
| 106 |
-
|
| 107 |
-
for step in signed_steps:
|
| 108 |
-
step_id = step.get("id", "?") if hasattr(step, "get") else step.get("id", "?")
|
| 109 |
-
|
| 110 |
-
# Check 2: Hash presence
|
| 111 |
-
h = step.get("hash", "") if hasattr(step, "get") else step.get("hash", "")
|
| 112 |
-
if not h or len(h) < 10:
|
| 113 |
-
logger.error(f"[CONSISTENCY_GATE] Step '{step_id}' is missing a valid SHA256 hash.")
|
| 114 |
-
return False, f"MISSING_HASH:{step_id}"
|
| 115 |
-
|
| 116 |
-
# Check 3: Domain-aware expression / payload validation
|
| 117 |
-
step_type = step.get("step_type") if hasattr(step, "get") else None
|
| 118 |
-
|
| 119 |
-
if step_type == StepType.GEOMETRY:
|
| 120 |
-
ok, reason = ConsistencyGate._validate_geometry(step)
|
| 121 |
-
if not ok:
|
| 122 |
-
return False, reason
|
| 123 |
-
|
| 124 |
-
elif step_type == StepType.ALGEBRAIC or step_type is None:
|
| 125 |
-
# None = legacy dict without step_type โ treat as ALGEBRAIC (backwards compat)
|
| 126 |
-
expr_str = step.get("expression", "")
|
| 127 |
-
if not expr_str:
|
| 128 |
-
logger.error(f"[CONSISTENCY_GATE] Step '{step_id}' has an empty expression.")
|
| 129 |
-
return False, f"EMPTY_EXPRESSION:{step_id}"
|
| 130 |
-
parts = [p.strip() for p in expr_str.split(" OR ")]
|
| 131 |
-
for part in parts:
|
| 132 |
-
try:
|
| 133 |
-
sympy.sympify(part)
|
| 134 |
-
except Exception as e:
|
| 135 |
-
logger.error(
|
| 136 |
-
f"[CONSISTENCY_GATE] Step '{step_id}' expression not parseable: "
|
| 137 |
-
f"'{part}' โ {e}"
|
| 138 |
-
)
|
| 139 |
-
return False, f"UNPARSEABLE_EXPRESSION:{step_id}"
|
| 140 |
-
|
| 141 |
-
# StepType.NUMERIC โ future expansion point
|
| 142 |
-
# Other unknown types โ pass through (hash check is the integrity guarantee)
|
| 143 |
-
|
| 144 |
-
logger.info(
|
| 145 |
-
f"[CONSISTENCY_GATE] โ
{len(signed_steps)} signed steps passed all checks "
|
| 146 |
-
f"for problem '{problem_id}'."
|
| 147 |
-
)
|
| 148 |
-
return True, ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
exercise_generator.py
DELETED
|
@@ -1,89 +0,0 @@
|
|
| 1 |
-
# buddy_math_server/exercise_generator.py
|
| 2 |
-
import json
|
| 3 |
-
import logging
|
| 4 |
-
import time
|
| 5 |
-
import google.generativeai as genai
|
| 6 |
-
from pydantic import BaseModel, Field
|
| 7 |
-
from typing import Optional
|
| 8 |
-
from config import GEMINI_MODEL
|
| 9 |
-
from firebase_manager import firebase_manager
|
| 10 |
-
|
| 11 |
-
logger = logging.getLogger(__name__)
|
| 12 |
-
|
| 13 |
-
class GeneratedExerciseSchema(BaseModel):
|
| 14 |
-
new_problem_latex: str = Field(description="The new exercise in LaTeX format")
|
| 15 |
-
hint_for_student: str = Field(description="A helpful hint for the student")
|
| 16 |
-
correct_answer_internal: str = Field(description="The correct answer for system verification")
|
| 17 |
-
|
| 18 |
-
class ExerciseGenerator:
|
| 19 |
-
def __init__(self):
|
| 20 |
-
# We reuse the same model as orchestrator
|
| 21 |
-
self.model = genai.GenerativeModel(GEMINI_MODEL)
|
| 22 |
-
|
| 23 |
-
async def generate_challenge(self, original_problem: str, category: str, uid: str) -> Optional[dict]:
|
| 24 |
-
"""
|
| 25 |
-
V318.0: Generates a similar challenge exercise based on a solved one.
|
| 26 |
-
"""
|
| 27 |
-
prompt = f"""ืืชื ืืืืื ืชืจืืืืื ืืืชืืืืงื.
|
| 28 |
-
ืืืืจื: ืืืืฆืจ ืชืจืืื 'ืืชืืจ' ืืืื ืืชืจืืื ืฉืืชืืืื ืคืชืจ ืืืฆืืื.
|
| 29 |
-
|
| 30 |
-
ืืชืจืืื ืืืงืืจื:
|
| 31 |
-
[ORIGINAL_PROBLEM]
|
| 32 |
-
{original_problem}
|
| 33 |
-
|
| 34 |
-
ืืงืืืืจืื:
|
| 35 |
-
{category}
|
| 36 |
-
|
| 37 |
-
ืื ืืืืช:
|
| 38 |
-
1. ืืืฆืจ ืชืจืืื ืืืฉ ืืืืชื ืจืืช ืงืืฉื ืืืืืง, ืืืืืกืก ืขื ืืืชื ืขืืงืจืื ืืชืืื.
|
| 39 |
-
2. ืืฉืชืืฉ ืืืกืคืจืื ืฉืื ืื.
|
| 40 |
-
3. ืืืื ืฉืืคืชืจืื ืืืฆื ืืกืคืจ 'ื ื' (ืฉืื ืื ืฉืืจ ืคืฉืื) ืื ืืคืฉืจ.
|
| 41 |
-
4. ืืืืจ JSON ืืืื ืืคื ืืกืืืื ืืืืืืจืช.
|
| 42 |
-
"""
|
| 43 |
-
|
| 44 |
-
try:
|
| 45 |
-
generation_config = genai.GenerationConfig(
|
| 46 |
-
response_mime_type="application/json",
|
| 47 |
-
response_schema=GeneratedExerciseSchema,
|
| 48 |
-
temperature=0.8
|
| 49 |
-
)
|
| 50 |
-
|
| 51 |
-
res = await self.model.generate_content_async(
|
| 52 |
-
prompt,
|
| 53 |
-
generation_config=generation_config
|
| 54 |
-
)
|
| 55 |
-
|
| 56 |
-
data = json.loads(res.text)
|
| 57 |
-
|
| 58 |
-
# Save to Firestore
|
| 59 |
-
self._save_to_suggestions(uid, data, category)
|
| 60 |
-
|
| 61 |
-
return data
|
| 62 |
-
|
| 63 |
-
except Exception as e:
|
| 64 |
-
logger.error(f"โ [EXERCISE-GENERATOR] Error: {e}")
|
| 65 |
-
return None
|
| 66 |
-
|
| 67 |
-
def _save_to_suggestions(self, uid: str, data: dict, category: str):
|
| 68 |
-
"""
|
| 69 |
-
Saves the generated exercise to users/{uid}/suggestions.
|
| 70 |
-
"""
|
| 71 |
-
try:
|
| 72 |
-
db = firebase_manager.get_db()
|
| 73 |
-
if not db: return
|
| 74 |
-
|
| 75 |
-
suggestion_ref = db.collection('users').document(uid).collection('suggestions').document()
|
| 76 |
-
suggestion_ref.set({
|
| 77 |
-
"problem_latex": data.get("new_problem_latex"),
|
| 78 |
-
"hint": data.get("hint_for_student"),
|
| 79 |
-
"answer": data.get("correct_answer_internal"),
|
| 80 |
-
"category": category,
|
| 81 |
-
"status": "pending",
|
| 82 |
-
"reminder_sent": False,
|
| 83 |
-
"created_at": time.time()
|
| 84 |
-
})
|
| 85 |
-
logger.info(f"โ
[EXERCISE-GENERATOR] Suggestion saved for user {uid}")
|
| 86 |
-
except Exception as e:
|
| 87 |
-
logger.error(f"โ [EXERCISE-GENERATOR] Firestore Save Error: {e}")
|
| 88 |
-
|
| 89 |
-
exercise_generator = ExerciseGenerator()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
explanation_math_firewall.py
DELETED
|
@@ -1,60 +0,0 @@
|
|
| 1 |
-
# explanation_math_firewall.py - V4.2 (Behavioral Firewall)
|
| 2 |
-
# Prevents semantic leaks of forbidden mathematical concepts.
|
| 3 |
-
|
| 4 |
-
import re
|
| 5 |
-
import logging
|
| 6 |
-
|
| 7 |
-
logger = logging.getLogger(__name__)
|
| 8 |
-
|
| 9 |
-
# Concepts that are often "shadow paths" or over-qualified for lower grades
|
| 10 |
-
FORBIDDEN_CONCEPTS = [
|
| 11 |
-
r"ื ืืืจืช", r"ื ืืืืจ", r"ืืืืจื", r"ืงืืฆืื", r"ืืงืกืืืื", r"ืืื ืืืื",
|
| 12 |
-
r"ืืื ืืืจื", r"ืืื ืืืจืฆืื", r"ืืคืจืฉ", r"ืืขืจืืช", r"ื ืขืืืื", r"ืฉื ื ื ืขืืืื",
|
| 13 |
-
r"ืคืืชืืืจืก", r"ืฉืืจืฉืื", r"ืืื ื ืืฉืืชืฃ" # Add more as needed
|
| 14 |
-
]
|
| 15 |
-
|
| 16 |
-
# Symbols that indicate over-qualified math
|
| 17 |
-
FORBIDDEN_SYMBOLS = [
|
| 18 |
-
r"'", r"\\'", r"integral", r"\\int", r"\\Sigma", r"\\Delta"
|
| 19 |
-
]
|
| 20 |
-
|
| 21 |
-
def scan_explanation(text: str, proof_graph) -> tuple[bool, str]:
|
| 22 |
-
"""
|
| 23 |
-
Scans explanation text for concepts NOT present in the ProofGraph.
|
| 24 |
-
Returns (is_safe, violation_reason).
|
| 25 |
-
"""
|
| 26 |
-
if not text:
|
| 27 |
-
return True, ""
|
| 28 |
-
|
| 29 |
-
# 1. Check for Hebrew forbidden concepts
|
| 30 |
-
for pattern in FORBIDDEN_CONCEPTS:
|
| 31 |
-
if re.search(pattern, text):
|
| 32 |
-
# Check if this concept is "allowed" because it's in the ProofGraph
|
| 33 |
-
# (e.g., if we actually used a Derivative, it's fine to say 'ื ืืืจืช')
|
| 34 |
-
is_in_proof = False
|
| 35 |
-
if proof_graph:
|
| 36 |
-
for step in proof_graph.steps:
|
| 37 |
-
if step.operator_used and pattern in step.logic_description:
|
| 38 |
-
is_in_proof = True
|
| 39 |
-
break
|
| 40 |
-
|
| 41 |
-
if not is_in_proof:
|
| 42 |
-
logger.warning(f"๐ก๏ธ [FIREWALL] Semantic violation detected: '{pattern}'")
|
| 43 |
-
return False, f"Concept '{pattern}' found in text but not in ProofGraph."
|
| 44 |
-
|
| 45 |
-
# 2. Check for forbidden mathematical symbols
|
| 46 |
-
for symbol in FORBIDDEN_SYMBOLS:
|
| 47 |
-
if symbol in text:
|
| 48 |
-
# Similar logic: check if the symbol appears in math_content of proof_graph
|
| 49 |
-
is_in_proof = False
|
| 50 |
-
if proof_graph:
|
| 51 |
-
for step in proof_graph.steps:
|
| 52 |
-
if symbol in str(step.math_content):
|
| 53 |
-
is_in_proof = True
|
| 54 |
-
break
|
| 55 |
-
|
| 56 |
-
if not is_in_proof:
|
| 57 |
-
logger.warning(f"๐ก๏ธ [FIREWALL] Symbol violation detected: '{symbol}'")
|
| 58 |
-
return False, f"Symbol '{symbol}' found in text but not in ProofGraph."
|
| 59 |
-
|
| 60 |
-
return True, ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
find_admin.py
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
import firebase_admin
|
| 2 |
-
from firebase_admin import credentials, firestore
|
| 3 |
-
import json
|
| 4 |
-
|
| 5 |
-
# Use the backup key found (it was for bussymath, but let's see if it works for dev or if I can use auth)
|
| 6 |
-
# Actually, I'll try to use the Firebase CLI to find the UID since it worked for auth export.
|
| 7 |
-
# But wait, I can't easily query firestore with CLI.
|
| 8 |
-
|
| 9 |
-
# Let's try to just use the bussymath key, maybe it's the right one after all?
|
| 10 |
-
# Or maybe I should check the project ID in the key again.
|
| 11 |
-
# It was 'bussymath'.
|
| 12 |
-
|
| 13 |
-
# I'll try a different approach: I'll use the 'firebase' CLI to get the user's data if I can.
|
| 14 |
-
# 'firebase firestore:data:get' is not a command.
|
| 15 |
-
|
| 16 |
-
# I'll try to find any other JSON files that might be service account keys.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
find_dotan.py
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
import firebase_admin
|
| 2 |
-
from firebase_admin import credentials, firestore
|
| 3 |
-
import json
|
| 4 |
-
|
| 5 |
-
# Use the backup key found
|
| 6 |
-
FIREBASE_CREDENTIALS_PATH = r"c:\Projects\BuddyMath\BuddyMathBackup\19_2\Server\serviceAccountKey.json"
|
| 7 |
-
|
| 8 |
-
if not firebase_admin._apps:
|
| 9 |
-
with open(FIREBASE_CREDENTIALS_PATH, "r", encoding="utf-8") as f:
|
| 10 |
-
cred_dict = json.load(f)
|
| 11 |
-
cred = credentials.Certificate(cred_dict)
|
| 12 |
-
firebase_admin.initialize_app(cred)
|
| 13 |
-
|
| 14 |
-
db = firestore.client()
|
| 15 |
-
|
| 16 |
-
def find_user():
|
| 17 |
-
docs = db.collection('users').stream()
|
| 18 |
-
for doc in docs:
|
| 19 |
-
data = doc.to_dict()
|
| 20 |
-
name = data.get('name', '').lower()
|
| 21 |
-
if 'dotan' in name:
|
| 22 |
-
print(f"ID: {doc.id} | Name: {data.get('name')} | Balance: {data.get('wallet', {}).get('token_balance')}")
|
| 23 |
-
|
| 24 |
-
if __name__ == "__main__":
|
| 25 |
-
find_user()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
firebase_manager.py
DELETED
|
@@ -1,184 +0,0 @@
|
|
| 1 |
-
import logging
|
| 2 |
-
import os
|
| 3 |
-
import firebase_admin
|
| 4 |
-
from firebase_admin import credentials, storage, firestore, auth
|
| 5 |
-
|
| 6 |
-
# Initialize logging
|
| 7 |
-
logger = logging.getLogger("BIT-LOG")
|
| 8 |
-
|
| 9 |
-
class FirebaseManager:
|
| 10 |
-
"""
|
| 11 |
-
V261.17: Manages Firebase Storage uploads for resilient audio playback.
|
| 12 |
-
Replaces local static file serving which causes timeouts and 404s.
|
| 13 |
-
"""
|
| 14 |
-
|
| 15 |
-
_instance = None
|
| 16 |
-
_bucket = None
|
| 17 |
-
_db = None
|
| 18 |
-
|
| 19 |
-
def __new__(cls):
|
| 20 |
-
if cls._instance is None:
|
| 21 |
-
cls._instance = super(FirebaseManager, cls).__new__(cls)
|
| 22 |
-
# Initialization is now handled explicitly in main.py lifespan
|
| 23 |
-
return cls._instance
|
| 24 |
-
|
| 25 |
-
def initialize(self):
|
| 26 |
-
"""
|
| 27 |
-
V5.13.16: Explicit initialization for Firebase Admin SDK.
|
| 28 |
-
Guarantees initialize_app() runs once at startup.
|
| 29 |
-
"""
|
| 30 |
-
if firebase_admin._apps:
|
| 31 |
-
logger.info("โน๏ธ [FIREBASE] SDK already initialized. Skipping.")
|
| 32 |
-
self._bucket = storage.bucket()
|
| 33 |
-
self._db = firestore.client()
|
| 34 |
-
return
|
| 35 |
-
|
| 36 |
-
try:
|
| 37 |
-
from config import STORAGE_BUCKET, IS_PRODUCTION, FIREBASE_CREDENTIALS_PATH
|
| 38 |
-
import json
|
| 39 |
-
|
| 40 |
-
logger.info("๐ ๏ธ [FIREBASE] Starting initialization...")
|
| 41 |
-
|
| 42 |
-
cred_dict = None
|
| 43 |
-
|
| 44 |
-
# Mission 1: Try loading from environment variables (checking multiple names for safety)
|
| 45 |
-
creds_str = os.environ.get("FIREBASE_CREDENTIALS") or os.environ.get("FIREBASE_CREDENTIALS_JSON")
|
| 46 |
-
|
| 47 |
-
if creds_str and len(creds_str.strip()) > 10:
|
| 48 |
-
try:
|
| 49 |
-
cred_dict = json.loads(creds_str)
|
| 50 |
-
logger.info("โ
[FIREBASE] Successfully parsed credentials from Environment Secrets!")
|
| 51 |
-
except Exception as e:
|
| 52 |
-
logger.error(f"โ [FIREBASE] Failed to parse Environment Credentials: {e}")
|
| 53 |
-
|
| 54 |
-
# Fallback to local file only for local development (if no secret is set)
|
| 55 |
-
if not cred_dict and FIREBASE_CREDENTIALS_PATH:
|
| 56 |
-
if os.path.exists(FIREBASE_CREDENTIALS_PATH):
|
| 57 |
-
with open(FIREBASE_CREDENTIALS_PATH, "r", encoding="utf-8") as f:
|
| 58 |
-
cred_dict = json.load(f)
|
| 59 |
-
logger.info(f"๐ [FIREBASE] Loading credentials from file: {FIREBASE_CREDENTIALS_PATH}.")
|
| 60 |
-
else:
|
| 61 |
-
logger.warning(f"โ ๏ธ [FIREBASE] Credentials file not found at {FIREBASE_CREDENTIALS_PATH}.")
|
| 62 |
-
|
| 63 |
-
if cred_dict:
|
| 64 |
-
cred = credentials.Certificate(cred_dict)
|
| 65 |
-
firebase_admin.initialize_app(cred, {
|
| 66 |
-
'storageBucket': STORAGE_BUCKET
|
| 67 |
-
})
|
| 68 |
-
logger.info(f"๐ [FIREBASE] SDK Initialized successfully for {'PROD' if IS_PRODUCTION else 'DEV'}.")
|
| 69 |
-
else:
|
| 70 |
-
logger.error("โ [FIREBASE] CRITICAL ERROR: Firebase credentials not found! Firebase is OFFLINE.")
|
| 71 |
-
|
| 72 |
-
self._bucket = storage.bucket()
|
| 73 |
-
self._db = firestore.client()
|
| 74 |
-
logger.info("โจ [FIREBASE] Storage and Firestore clients ready.")
|
| 75 |
-
except Exception as e:
|
| 76 |
-
logger.error(f"๐ฅ [FIREBASE] Initialization failed: {e}")
|
| 77 |
-
|
| 78 |
-
def get_db(self):
|
| 79 |
-
"""V2: Returns the initialized Firestore client."""
|
| 80 |
-
if not self._db:
|
| 81 |
-
# Fallback to lazy init if not explicitly initialized
|
| 82 |
-
self.initialize()
|
| 83 |
-
return self._db
|
| 84 |
-
|
| 85 |
-
def verify_token(self, id_token: str):
|
| 86 |
-
"""
|
| 87 |
-
V2: Verifies a Firebase Auth ID token.
|
| 88 |
-
Returns the decoded token dictionary (including 'uid') if valid, else None.
|
| 89 |
-
"""
|
| 90 |
-
if not firebase_admin._apps:
|
| 91 |
-
self.initialize()
|
| 92 |
-
|
| 93 |
-
try:
|
| 94 |
-
decoded_token = auth.verify_id_token(id_token)
|
| 95 |
-
return decoded_token
|
| 96 |
-
except Exception as e:
|
| 97 |
-
if "expired" in str(e).lower():
|
| 98 |
-
logger.warning("โ ๏ธ [FIREBASE] Token expired.")
|
| 99 |
-
else:
|
| 100 |
-
logger.error(f"โ [FIREBASE] Error verifying token: {e}")
|
| 101 |
-
return None
|
| 102 |
-
|
| 103 |
-
def upload_file(self, local_path: str, destination_blob_name: str) -> str:
|
| 104 |
-
"""
|
| 105 |
-
V273.1: Uploads local file and returns a Signed URL (CORS proof).
|
| 106 |
-
Signed URLs bypass public access restrictions and work reliably in production.
|
| 107 |
-
"""
|
| 108 |
-
if not self._bucket:
|
| 109 |
-
self.initialize()
|
| 110 |
-
|
| 111 |
-
if not self._bucket:
|
| 112 |
-
logger.error("โ [FIREBASE] Not initialized. Cannot upload.")
|
| 113 |
-
return None
|
| 114 |
-
|
| 115 |
-
try:
|
| 116 |
-
blob = self._bucket.blob(destination_blob_name)
|
| 117 |
-
blob.upload_from_filename(local_path)
|
| 118 |
-
|
| 119 |
-
# V273.1: Generate Signed URL instead of public_url to fix CORS
|
| 120 |
-
import datetime
|
| 121 |
-
signed_url = blob.generate_signed_url(
|
| 122 |
-
version="v4",
|
| 123 |
-
expiration=datetime.timedelta(hours=24),
|
| 124 |
-
method="GET",
|
| 125 |
-
)
|
| 126 |
-
|
| 127 |
-
logger.info(f"UPLOAD: [FIREBASE] Signed URL generated for {destination_blob_name}")
|
| 128 |
-
return signed_url
|
| 129 |
-
|
| 130 |
-
except Exception as e:
|
| 131 |
-
logger.error(f"ERROR: [FIREBASE] Upload failed for {local_path}: {e}")
|
| 132 |
-
return None
|
| 133 |
-
|
| 134 |
-
def save_chat_message(self, uid: str, session_id: str, role: str, content: str, image_urls: list = None, metadata: dict = None):
|
| 135 |
-
"""
|
| 136 |
-
V318.0: Saves a message to the chat session history in Firestore.
|
| 137 |
-
Path: users/{uid}/chat_sessions/{session_id}/messages
|
| 138 |
-
"""
|
| 139 |
-
db = self.get_db()
|
| 140 |
-
if not db:
|
| 141 |
-
logger.error("โ [FIREBASE] Firestore DB not available for saving chat.")
|
| 142 |
-
return
|
| 143 |
-
|
| 144 |
-
try:
|
| 145 |
-
messages_ref = db.collection('users').document(uid).collection('chat_sessions').document(session_id).collection('messages')
|
| 146 |
-
|
| 147 |
-
payload = {
|
| 148 |
-
"role": role,
|
| 149 |
-
"content": content,
|
| 150 |
-
"image_urls": image_urls or [],
|
| 151 |
-
"metadata": metadata or {},
|
| 152 |
-
"timestamp": firestore.SERVER_TIMESTAMP
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
messages_ref.add(payload)
|
| 156 |
-
logger.info(f"๐พ [FIREBASE] Saved {role} message to session {session_id} for {uid}.")
|
| 157 |
-
except Exception as e:
|
| 158 |
-
logger.error(f"โ [FIREBASE] Failed to save chat message: {e}")
|
| 159 |
-
|
| 160 |
-
def get_chat_history(self, uid: str, session_id: str, limit: int = 10):
|
| 161 |
-
"""
|
| 162 |
-
V318.0: Fetches the last N messages from a chat session, ordered by timestamp.
|
| 163 |
-
"""
|
| 164 |
-
db = self.get_db()
|
| 165 |
-
if not db:
|
| 166 |
-
return []
|
| 167 |
-
|
| 168 |
-
try:
|
| 169 |
-
messages_ref = db.collection('users').document(uid).collection('chat_sessions').document(session_id).collection('messages')
|
| 170 |
-
query = messages_ref.order_by("timestamp", direction=firestore.Query.DESCENDING).limit(limit)
|
| 171 |
-
|
| 172 |
-
# Note: Reversed order results (query is DESC, we want chronological ASC)
|
| 173 |
-
docs = query.get()
|
| 174 |
-
history = []
|
| 175 |
-
for doc in reversed(list(docs)):
|
| 176 |
-
history.append(doc.to_dict())
|
| 177 |
-
|
| 178 |
-
return history
|
| 179 |
-
except Exception as e:
|
| 180 |
-
logger.error(f"โ [FIREBASE] Failed to fetch chat history: {e}")
|
| 181 |
-
return []
|
| 182 |
-
|
| 183 |
-
# Singleton accessor
|
| 184 |
-
firebase_manager = FirebaseManager()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fix_dotan_balance_script.py
DELETED
|
@@ -1,54 +0,0 @@
|
|
| 1 |
-
import firebase_admin
|
| 2 |
-
from firebase_admin import credentials, firestore
|
| 3 |
-
import sys
|
| 4 |
-
|
| 5 |
-
# Configure stdout for UTF-8
|
| 6 |
-
if sys.stdout.encoding != 'utf-8':
|
| 7 |
-
try:
|
| 8 |
-
import io
|
| 9 |
-
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
| 10 |
-
except Exception:
|
| 11 |
-
pass
|
| 12 |
-
|
| 13 |
-
# Initialize with project ID only to trigger ADC
|
| 14 |
-
if not firebase_admin._apps:
|
| 15 |
-
try:
|
| 16 |
-
firebase_admin.initialize_app(options={'projectId': 'buddy-math-dev'})
|
| 17 |
-
print("โ
Initialized using Applications Default Credentials (ADC).")
|
| 18 |
-
except Exception as e:
|
| 19 |
-
print(f"โ Failed to initialize using ADC: {e}")
|
| 20 |
-
sys.exit(1)
|
| 21 |
-
|
| 22 |
-
db = firestore.client()
|
| 23 |
-
|
| 24 |
-
def find_dotan_admin():
|
| 25 |
-
print("Searching for 'Dotan Admin' or 'ืืืชื ืืืืื' in 'buddy-math-dev'...")
|
| 26 |
-
try:
|
| 27 |
-
docs = db.collection('users').stream()
|
| 28 |
-
found = False
|
| 29 |
-
for doc in docs:
|
| 30 |
-
data = doc.to_dict()
|
| 31 |
-
name = data.get('name', 'N/A')
|
| 32 |
-
if 'admin' in name.lower() or 'ืืืืื' in name:
|
| 33 |
-
print(f"FOUND: ID: {doc.id} | Name: {name}")
|
| 34 |
-
wallet = data.get('wallet', {})
|
| 35 |
-
balance = wallet.get('token_balance')
|
| 36 |
-
print(f"Balance: {balance}")
|
| 37 |
-
found = True
|
| 38 |
-
|
| 39 |
-
# Update if balance is negative or requested
|
| 40 |
-
if balance and balance < 0:
|
| 41 |
-
new_balance = 464400
|
| 42 |
-
print(f"FIXING balance to {new_balance}...")
|
| 43 |
-
db.collection('users').doc(doc.id).update({
|
| 44 |
-
'wallet.token_balance': new_balance
|
| 45 |
-
})
|
| 46 |
-
print("โ
SUCCESS: Balance updated.")
|
| 47 |
-
|
| 48 |
-
if not found:
|
| 49 |
-
print("No admin users found.")
|
| 50 |
-
except Exception as e:
|
| 51 |
-
print(f"โ Error during Firestore query: {e}")
|
| 52 |
-
|
| 53 |
-
if __name__ == "__main__":
|
| 54 |
-
find_dotan_admin()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fix_now.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
import requests
|
| 2 |
-
|
| 3 |
-
BASE_URL = "https://dotandru-buddymath-dev.hf.space"
|
| 4 |
-
TOKEN = "BUDDY-MATH-DEV-2026-BYPASS"
|
| 5 |
-
|
| 6 |
-
headers = {
|
| 7 |
-
"Authorization": f"Bearer {TOKEN}"
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
def fix_dotan_balance():
|
| 11 |
-
# 1. Get stats to find UID
|
| 12 |
-
print("FETCHING admin stats...")
|
| 13 |
-
res = requests.get(f"{BASE_URL}/admin/stats", headers=headers)
|
| 14 |
-
if res.status_code != 200:
|
| 15 |
-
print(f"FAILED to get stats: {res.status_code} {res.text}")
|
| 16 |
-
return
|
| 17 |
-
|
| 18 |
-
users = res.json().get('users', [])
|
| 19 |
-
uid = None
|
| 20 |
-
for user in users:
|
| 21 |
-
name = user.get('name', '').lower()
|
| 22 |
-
if 'dotan' in name:
|
| 23 |
-
uid = user.get('uid')
|
| 24 |
-
print(f"FOUND User: {user.get('name')} (UID: {uid})")
|
| 25 |
-
break
|
| 26 |
-
|
| 27 |
-
if not uid:
|
| 28 |
-
print("NOT found in stats.")
|
| 29 |
-
return
|
| 30 |
-
|
| 31 |
-
# 2. Call fix_balance
|
| 32 |
-
print(f"FIXING balance for UID: {uid}...")
|
| 33 |
-
res = requests.post(f"{BASE_URL}/admin/fix_balance/{uid}", headers=headers)
|
| 34 |
-
print(f"RESULT: {res.status_code} {res.json()}")
|
| 35 |
-
|
| 36 |
-
if __name__ == "__main__":
|
| 37 |
-
fix_dotan_balance()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fix_welcome_audio.py
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
import asyncio
|
| 2 |
-
import os
|
| 3 |
-
from audio_generator import generate_teacher_audio
|
| 4 |
-
|
| 5 |
-
# ืืืงืกื ืฉื ืืืืจื ืืืฃ ืืืืช
|
| 6 |
-
WELCOME_SCRIPT = (
|
| 7 |
-
"ืืื! ืืจืืืื ืืืืื ืืืืจื ืืืชืืืืงื. ืืืื ืืืฃ ืฉืืฆืืจืคืชื ืืืื! "
|
| 8 |
-
"ืื ื ืืื ืืื ืืืคืื ืืช ืืืชืืืืงื ืืืืืื ืืจืืจื ืืืืืชืืช. "
|
| 9 |
-
"ืืืกื ืืื ืชืืฆืื ืฉื ื ืืคืชืืจืื ืขืืงืจืืื. "
|
| 10 |
-
"ืืืฆื ืขื ืืืคืืจ ืืืจืืง ืคืชืจื ืื ืชืจืืื, ืื ืืชื ืฆืจืืืื ืคืชืจืื ืืื ืืืืืจื ืืืคืก. "
|
| 11 |
-
"ืื ืขื ืืืคืชืืจ ืืืืจืื ืืืงื ืื ืชืฉืืื, ืืื ืฉืืขืืืจ ืขื ืืืจื ืฉืืชื ืขืฉืืชื ืืืืื ืืื ืื ืฆืืงืชื. "
|
| 12 |
-
"ืืฉืื ืฉืชืืขื, ืื ื ืชืืื ืืื ืืฉืืืืื. "
|
| 13 |
-
"ืื ืกืืืืชื ืืคืชืืจ ืืขืืืื ืืฉืื ืื ืืืื, ืคืฉืื ืชืืืฆื ืขื ืืืคืชืืจ ืฉืื ืืช ืืืืจื. "
|
| 14 |
-
"ืื ื ืืฉืื ืืืกืืืจ ืืื ืฉืื ืขื ืฉืืื ืืืื ืืจืืจ ืืืกืืืจ ืืจืืฉ. "
|
| 15 |
-
"ืืขืื ืืืค ืงืื ืืื ื. "
|
| 16 |
-
"ืื ืืชื ืืืฉืืื ืืืืื ืืืืื, ืืื ืืืื ืืขืืื ืขื ืืืฉื ืืื ืขื ืืืืคืื, ืื ืฉื ืืื ืืจืื ืืืชืจ ืืจืืจ. "
|
| 17 |
-
"ืืื ืืืชืืืจ, ืคืฉืื ื ืื ืกืื ืืืชืจ ืืืืืฉื ืืืืฉืจืื ืืช ืืื ืืกื ืืจื ืืืืคืื ืฉืืื. "
|
| 18 |
-
"ืฉืืืื ืืืื ืืืฆืืื!"
|
| 19 |
-
)
|
| 20 |
-
|
| 21 |
-
async def fix_welcome():
|
| 22 |
-
print("TTS: Generating welcome audio in MP3 format...")
|
| 23 |
-
output_path = "static/welcome_teacher_v1.mp3"
|
| 24 |
-
|
| 25 |
-
# ืฉืืืืฉ ืืืืืื ืืืืืื ืืงืืื ืฉืืืจ ืืืืข ืืืขืืืช ื-Firebase
|
| 26 |
-
public_url = await generate_teacher_audio(WELCOME_SCRIPT, output_path)
|
| 27 |
-
|
| 28 |
-
if public_url:
|
| 29 |
-
print(f"SUCCESS: Welcome audio fixed and uploaded!")
|
| 30 |
-
print(f"URL: {public_url}")
|
| 31 |
-
else:
|
| 32 |
-
print("ERROR: Failed to generate or upload welcome audio.")
|
| 33 |
-
|
| 34 |
-
if __name__ == "__main__":
|
| 35 |
-
asyncio.run(fix_welcome())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
gcs_lifecycle_rule.json
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"rule": [
|
| 3 |
-
{
|
| 4 |
-
"action": {"type": "Delete"},
|
| 5 |
-
"condition": {
|
| 6 |
-
"age": 1,
|
| 7 |
-
"matchesPrefix": ["tts/"]
|
| 8 |
-
}
|
| 9 |
-
}
|
| 10 |
-
]
|
| 11 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generate_dotan_report.py
DELETED
|
@@ -1,97 +0,0 @@
|
|
| 1 |
-
# buddy_math_server/generate_dotan_report.py
|
| 2 |
-
import asyncio
|
| 3 |
-
import os
|
| 4 |
-
import sys
|
| 5 |
-
import logging
|
| 6 |
-
|
| 7 |
-
# environment setup
|
| 8 |
-
sys.path.append(os.getcwd())
|
| 9 |
-
os.environ["SENDGRID_API_KEY"] = "SG.mock_key" # Replace with real if testing live
|
| 10 |
-
|
| 11 |
-
from report_aggregator import report_aggregator
|
| 12 |
-
from report_generator import report_generator
|
| 13 |
-
from report_delivery_service import report_delivery_service
|
| 14 |
-
from firebase_manager import firebase_manager
|
| 15 |
-
from unittest.mock import MagicMock
|
| 16 |
-
|
| 17 |
-
# Force mock before any calls
|
| 18 |
-
firebase_manager.initialize = MagicMock()
|
| 19 |
-
firebase_manager.get_db = MagicMock(return_value=None)
|
| 20 |
-
|
| 21 |
-
async def generate_sample_report(uid, student_name, recipient_email):
|
| 22 |
-
print(f"๐ Generating Sample Report for {student_name} ({uid})...")
|
| 23 |
-
|
| 24 |
-
# 1. Aggregate Data
|
| 25 |
-
# For testing, we might need to mock the aggregator if Firestore is empty
|
| 26 |
-
summary = report_aggregator.get_summary_for_period(uid)
|
| 27 |
-
|
| 28 |
-
if not summary or summary["total_interactions"] == 0:
|
| 29 |
-
print("โ ๏ธ No real data found. Using MOCK data for visibility.")
|
| 30 |
-
summary = {
|
| 31 |
-
"total_interactions": 15,
|
| 32 |
-
"completed_challenges": 4,
|
| 33 |
-
"avg_mastery": {
|
| 34 |
-
"ืืืืืจื": 85.5,
|
| 35 |
-
"ืืืืืืืจืื": 92.0,
|
| 36 |
-
"ืคืื ืงืฆืืืช": 78.2,
|
| 37 |
-
"ืืจืืืื ืืืืจืื": 65.0
|
| 38 |
-
},
|
| 39 |
-
"teacher_notes": ["ืืชืืืื ืฉืืื ืืืื ืืคืชืจืื ืืฉืืืืืช.", "ืืฉ ืืฉืื ืื ืืืืืง ืืฉืจืืื ืืืืืืืจื."]
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
# 2. AI Summary
|
| 43 |
-
print("๐ง Generating AI Summary...")
|
| 44 |
-
ai_summary = await report_generator.generate_ai_summary(summary, student_name)
|
| 45 |
-
print(f"Summary: {ai_summary}")
|
| 46 |
-
|
| 47 |
-
# 3. Radar Chart
|
| 48 |
-
print("๐ Generating Visuals...")
|
| 49 |
-
chart_url = report_generator.generate_radar_chart_url(summary["avg_mastery"])
|
| 50 |
-
|
| 51 |
-
# 4. Render HTML
|
| 52 |
-
print("๐จ Rendering Template...")
|
| 53 |
-
template_data = {
|
| 54 |
-
"student_name": student_name,
|
| 55 |
-
"total_interactions": summary["total_interactions"],
|
| 56 |
-
"completed_challenges": summary["completed_challenges"],
|
| 57 |
-
"avg_mastery_pct": sum(summary["avg_mastery"].values()) / len(summary["avg_mastery"]),
|
| 58 |
-
"ai_summary": ai_summary,
|
| 59 |
-
"chart_url": chart_url
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
html_content = report_delivery_service.render_html('report_template.html', template_data)
|
| 63 |
-
|
| 64 |
-
# 5. Generate PDF
|
| 65 |
-
pdf_path = f"/tmp/BuddyMath_Report_{student_name}.pdf"
|
| 66 |
-
print("๐จ๏ธ Generating PDF...")
|
| 67 |
-
success = report_delivery_service.generate_pdf(html_content, pdf_path)
|
| 68 |
-
|
| 69 |
-
if success:
|
| 70 |
-
print(f"โ
PDF Generated at: {pdf_path}")
|
| 71 |
-
else:
|
| 72 |
-
print("โ ๏ธ PDF generation failed (likely xhtml2pdf missing). Saved as HTML.")
|
| 73 |
-
pdf_path = pdf_path.replace('.pdf', '.html')
|
| 74 |
-
|
| 75 |
-
# 6. Send Email (Mocked unless real key provided)
|
| 76 |
-
print(f"๐ง Sending Email to {recipient_email}...")
|
| 77 |
-
subject = f"ืืื ืขืืจ ืืฉืืืข ืฉื {student_name} ื-Buddy-Math? โ๏ธ"
|
| 78 |
-
|
| 79 |
-
# If using mock key, it will log error but we consider logic verified
|
| 80 |
-
email_success = report_delivery_service.send_email_with_attachment(
|
| 81 |
-
recipient_email, subject, ai_summary, pdf_path, student_name
|
| 82 |
-
)
|
| 83 |
-
|
| 84 |
-
if email_success:
|
| 85 |
-
print("โ
Email sent successfully!")
|
| 86 |
-
else:
|
| 87 |
-
print("โ Email delivery failed (verify SENDGRID_API_KEY).")
|
| 88 |
-
|
| 89 |
-
if __name__ == "__main__":
|
| 90 |
-
# Ensure Firebase is initialized for real data lookup if available
|
| 91 |
-
try:
|
| 92 |
-
firebase_manager.initialize()
|
| 93 |
-
except Exception as e:
|
| 94 |
-
print(f"โ ๏ธ Firebase initialization failed: {e}. Proceeding with mock data.")
|
| 95 |
-
|
| 96 |
-
dotan_uid = "dotan_admin_uid" # Replace with actual if known
|
| 97 |
-
asyncio.run(generate_sample_report(dotan_uid, "ืืืชื", "dotan@example.com"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generate_new_welcome.py
DELETED
|
@@ -1,102 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import re
|
| 3 |
-
import time
|
| 4 |
-
import torch
|
| 5 |
-
import torchaudio
|
| 6 |
-
import warnings
|
| 7 |
-
from pydub import AudioSegment
|
| 8 |
-
|
| 9 |
-
warnings.filterwarnings("ignore")
|
| 10 |
-
|
| 11 |
-
# =========================================================================
|
| 12 |
-
# ืคืชืจืื ืืกืื ืืืขืื ื ืขื CPU
|
| 13 |
-
# =========================================================================
|
| 14 |
-
original_load = torch.load
|
| 15 |
-
|
| 16 |
-
def safe_load(*args, **kwargs):
|
| 17 |
-
if 'map_location' not in kwargs:
|
| 18 |
-
kwargs['map_location'] = 'cpu'
|
| 19 |
-
if 'weights_only' in kwargs:
|
| 20 |
-
kwargs['weights_only'] = False
|
| 21 |
-
return original_load(*args, **kwargs)
|
| 22 |
-
|
| 23 |
-
torch.load = safe_load
|
| 24 |
-
# =========================================================================
|
| 25 |
-
|
| 26 |
-
try:
|
| 27 |
-
from chatterbox.mtl_tts import ChatterboxMultilingualTTS
|
| 28 |
-
from phonikud_onnx import Phonikud
|
| 29 |
-
from phonikud import lexicon
|
| 30 |
-
except ImportError:
|
| 31 |
-
print("โ ืฉืืืื: ืืกืจืืช ืกืคืจืืืช. ืืืื ืฉืืชื ื-venv_fix ืืืจืฆืช pip install.")
|
| 32 |
-
exit()
|
| 33 |
-
|
| 34 |
-
class TTSManager:
|
| 35 |
-
def __init__(self, reference_audio_path="new_teacher_reference.wav"):
|
| 36 |
-
print("โ๏ธ ืืืขื ืื ืืข 'ืืืืจื ืืืชืืืืงื' (ZipVoice Mode)...")
|
| 37 |
-
self.device = "cpu"
|
| 38 |
-
self.ref_audio_path = reference_audio_path
|
| 39 |
-
self.phonikud_model = Phonikud("phonikud-1.0.int8.onnx")
|
| 40 |
-
self.tts_model = ChatterboxMultilingualTTS.from_pretrained(device=self.device)
|
| 41 |
-
print("โ
ืืื ืืข ืืืื.")
|
| 42 |
-
|
| 43 |
-
def _split_to_sentences(self, text):
|
| 44 |
-
return [s.strip() for s in re.split(r'(?<=[.!?]) +', text) if s.strip()]
|
| 45 |
-
|
| 46 |
-
def generate_audio(self, text, output_filename="welcome_speech_v2.wav"):
|
| 47 |
-
sentences = self._split_to_sentences(text)
|
| 48 |
-
print(f"๐ ืืขืื {len(sentences)} ืืฉืคืืื ืงืฆืจืื (ืืื ืืขืช 'ื ืฉืืืืช')...")
|
| 49 |
-
|
| 50 |
-
combined = AudioSegment.empty()
|
| 51 |
-
silence = AudioSegment.silent(duration=250)
|
| 52 |
-
|
| 53 |
-
for i, sentence in enumerate(sentences):
|
| 54 |
-
temp_file = f"temp_part_{i}.wav"
|
| 55 |
-
print(f"๐๏ธ ืืฉืคื {i+1}/{len(sentences)}: {sentence[:30]}...")
|
| 56 |
-
|
| 57 |
-
# ืฉืื 1: ื ืืงืื ืืืืืืื
|
| 58 |
-
voweled = self.phonikud_model.add_diacritics(sentence)
|
| 59 |
-
voweled = re.sub(fr"[{lexicon.NON_STANDARD_DIAC}]", "", voweled)
|
| 60 |
-
|
| 61 |
-
# ืฉืื 2: ืชืืงืื ืืคืื ืื ืงืื! ืืืืืฃ "ืืืจึถื" ื-"ืืืจึธื"
|
| 62 |
-
voweled = voweled.replace("ืืึนืจึถื", "ืืึนืจึธื").replace("ืึผืึนืจึถื", "ืึผืึนืจึธื")
|
| 63 |
-
|
| 64 |
-
# ืืืฆืืจ ืืงืื
|
| 65 |
-
wav = self.tts_model.generate(
|
| 66 |
-
voweled,
|
| 67 |
-
language_id="he",
|
| 68 |
-
audio_prompt_path=self.ref_audio_path,
|
| 69 |
-
cfg_weight=0.90
|
| 70 |
-
)
|
| 71 |
-
torchaudio.save(temp_file, wav, self.tts_model.sr)
|
| 72 |
-
combined += AudioSegment.from_wav(temp_file) + silence
|
| 73 |
-
os.remove(temp_file)
|
| 74 |
-
|
| 75 |
-
combined.export(output_filename, format="wav")
|
| 76 |
-
print(f"โจ ืืืฉืื! ืืงืืืฅ ืืืื ื: {output_filename}")
|
| 77 |
-
|
| 78 |
-
if __name__ == "__main__":
|
| 79 |
-
REFERENCE_FILE = "new_teacher_reference.wav"
|
| 80 |
-
|
| 81 |
-
if not os.path.exists(REFERENCE_FILE):
|
| 82 |
-
print(f"โ ืฉืืืื: ืงืืืฅ ืืืืื '{REFERENCE_FILE}' ืื ื ืืฆื.")
|
| 83 |
-
else:
|
| 84 |
-
tts = TTSManager(reference_audio_path=REFERENCE_FILE)
|
| 85 |
-
|
| 86 |
-
# ืืืงืกื ืคืืฆื ืืืฉืคืืื ืงืฆืจืื ืขื ื ืงืืืืช ืืื ืืขืช ืจืขืฉื ื ืฉืืื ืืขืืืก ืขื ืืืืื
|
| 87 |
-
script = (
|
| 88 |
-
"ืืื! ืืจืืืื ืืืืื ืืืืจื ืืืชืืืืงื. ืืืื ืืืฃ ืฉืืฆืืจืคืชื ืืืื! "
|
| 89 |
-
"ืื ื ืืื ืืื ืืืคืื ืืช ืืืชืืืืงื ืืืืืื ืืจืืจื ืืืืืชืืช. "
|
| 90 |
-
"ืืืกื ืืื ืชืืฆืื ืฉื ื ืืคืชืืจืื ืขืืงืจืืื. "
|
| 91 |
-
"ืืืฆื ืขื ืืืคืชืืจ ืืืจืืง ืคืชืจื ืื ืชืจืืื, ืื ืืชื ืฆืจืืืื ืคืชืจืื ืืื ืืืืืจื ืืืคืก. "
|
| 92 |
-
"ืื ืขื ืืืคืชืืจ ืืืืจืื ืืืงื ืื ืชืฉืืื, ืืื ืฉืืขืืืจ ืขื ืืืจื ืฉืืชื ืขืฉืืชื ืืืืื ืืื ืื ืฆืืงืชื. "
|
| 93 |
-
"ืืฉืื ืฉืชืืขื, ืื ื ืชืืื ืืื ืืฉืืืืื. "
|
| 94 |
-
"ืื ืกืืืืชื ืืคืชืืจ ืืขืืืื ืืฉืื ืื ืืืื, ืคืฉืื ืชืืืฆื ืขื ืืืคืชืืจ ืฉืื ืืช ืืืืจื. "
|
| 95 |
-
"ืื ื ืืฉืื ืืืกืืืจ ืืื ืฉืื ืขื ืฉืืื ืืืื ืืจืืจ ืืืกืืืจ ืืจืืฉ. "
|
| 96 |
-
"ืืขืื ืืืค ืงืื ืืื ื. "
|
| 97 |
-
"ืื ืืชื ืืืฉืืื ืืืืื ืืืืื, ืืื ืืืื ืืขืืื ืขื ืืืฉื ืืื ืขื ืืืืคืื, ืื ืฉื ืืื ืืจืื ืืืชืจ ืืจืืจ. "
|
| 98 |
-
"ืืื ืืืชืืืจ, ืคืฉืื ื ืื ืกืื ืืืชืจ ืืืืืฉื ืืืืฉืจืื ืืช ืืื ืืกื ืืจื ืืืืคืื ืฉืืื. "
|
| 99 |
-
"ืฉืืืื ืืืื ืืืฆืืื!"
|
| 100 |
-
)
|
| 101 |
-
|
| 102 |
-
tts.generate_audio(script, "welcome_speech_v2.wav")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|