V5.10.1: Added 'המצאה', 'טעות שלי', 'מתנצל' to Blacklist & Verified 4 Layers
Browse files- firebase_manager.py +0 -6
- main.py +135 -44
- orchestrator.py +10 -1
- prompts.py +22 -67
- strategy_manager.py +5 -2
firebase_manager.py
CHANGED
|
@@ -88,12 +88,6 @@ class FirebaseManager:
|
|
| 88 |
decoded_token = auth.verify_id_token(id_token)
|
| 89 |
return decoded_token
|
| 90 |
except Exception as e:
|
| 91 |
-
# V285.6: Dev Fallback to prevent 401s in non-prod environments
|
| 92 |
-
from config import IS_PRODUCTION
|
| 93 |
-
if not IS_PRODUCTION:
|
| 94 |
-
logger.warning(f"⚠️ [FIREBASE] Dev mode fallback: Auth check failed, using bypass user. ({e})")
|
| 95 |
-
return {'uid': 'dev-bypass-user'}
|
| 96 |
-
|
| 97 |
if "expired" in str(e).lower():
|
| 98 |
logger.warning("⚠️ [FIREBASE] Token expired.")
|
| 99 |
else:
|
|
|
|
| 88 |
decoded_token = auth.verify_id_token(id_token)
|
| 89 |
return decoded_token
|
| 90 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
if "expired" in str(e).lower():
|
| 92 |
logger.warning("⚠️ [FIREBASE] Token expired.")
|
| 93 |
else:
|
main.py
CHANGED
|
@@ -159,36 +159,153 @@ app.mount("/static", StaticFiles(directory="/tmp/static"), name="static")
|
|
| 159 |
|
| 160 |
@app.get("/")
|
| 161 |
async def root():
|
| 162 |
-
return {"status": f"BuddyMath API V5.
|
| 163 |
|
| 164 |
-
|
| 165 |
-
async def get_admin_stats(request: Request):
|
| 166 |
"""
|
| 167 |
-
|
| 168 |
-
|
| 169 |
"""
|
| 170 |
auth_header = request.headers.get('Authorization')
|
| 171 |
if not auth_header or not auth_header.startswith('Bearer '):
|
| 172 |
-
|
| 173 |
|
| 174 |
token = auth_header.split('Bearer ')[1].strip()
|
|
|
|
| 175 |
|
| 176 |
-
# Dev bypass check for stats too
|
| 177 |
from config import DEV_BYPASS_TOKEN
|
| 178 |
if not IS_PRODUCTION and token == DEV_BYPASS_TOKEN:
|
| 179 |
-
|
|
|
|
| 180 |
else:
|
| 181 |
decoded = firebase_manager.verify_token(token)
|
| 182 |
if not decoded:
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
# Check if user is admin
|
| 186 |
uid = decoded.get('uid')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
db = firebase_manager.get_db()
|
| 188 |
user_doc = db.collection('users').document(uid).get()
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
| 190 |
if user_data.get('role') != 'admin' and not user_data.get('isAdmin'):
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
from cost_tracker import LOG_FILE
|
| 194 |
stats = {
|
|
@@ -229,30 +346,10 @@ async def get_admin_stats(request: Request):
|
|
| 229 |
@app.post("/admin/delete_user/{uid}")
|
| 230 |
async def delete_user_full(uid: str, request: Request):
|
| 231 |
"""
|
| 232 |
-
V5.
|
| 233 |
-
|
| 234 |
"""
|
| 235 |
-
|
| 236 |
-
if not auth_header or not auth_header.startswith('Bearer '):
|
| 237 |
-
return JSONResponse(status_code=401, content={"error": "Unauthorized"})
|
| 238 |
-
|
| 239 |
-
token = auth_header.split('Bearer ')[1].strip()
|
| 240 |
-
|
| 241 |
-
# Admin check logic (reused from stats)
|
| 242 |
-
from config import DEV_BYPASS_TOKEN
|
| 243 |
-
if not IS_PRODUCTION and token == DEV_BYPASS_TOKEN:
|
| 244 |
-
pass # OK
|
| 245 |
-
else:
|
| 246 |
-
decoded = firebase_manager.verify_token(token)
|
| 247 |
-
if not decoded or not decoded.get('uid'):
|
| 248 |
-
return JSONResponse(status_code=401, content={"error": "Invalid token"})
|
| 249 |
-
|
| 250 |
-
admin_uid = decoded.get('uid')
|
| 251 |
-
db = firebase_manager.get_db()
|
| 252 |
-
user_doc = db.collection('users').document(admin_uid).get()
|
| 253 |
-
user_data = user_doc.to_dict() if user_doc.exists else {}
|
| 254 |
-
if user_data.get('role') != 'admin' and not user_data.get('isAdmin'):
|
| 255 |
-
return JSONResponse(status_code=403, content={"error": "Admin access required"})
|
| 256 |
|
| 257 |
try:
|
| 258 |
# 1. Delete from Firestore
|
|
@@ -617,17 +714,11 @@ class MigrationRequest(BaseModel):
|
|
| 617 |
batch_size: Optional[int] = 50
|
| 618 |
|
| 619 |
@app.post("/admin/migrate_to_v2")
|
| 620 |
-
async def migrate_to_v2(
|
| 621 |
"""
|
| 622 |
V280.2: Admin endpoint to migrate users to V2 Quota system.
|
| 623 |
"""
|
| 624 |
-
|
| 625 |
-
import os
|
| 626 |
-
|
| 627 |
-
# Just a basic safety check (in production, use a real secret)
|
| 628 |
-
admin_secret = os.environ.get("ADMIN_SECRET_KEY", "buddy_admin_123")
|
| 629 |
-
if x_api_key != admin_secret:
|
| 630 |
-
raise HTTPException(status_code=401, detail="Unauthorized")
|
| 631 |
|
| 632 |
try:
|
| 633 |
from scripts.migrate_users_to_cloud import migrate_users
|
|
|
|
| 159 |
|
| 160 |
@app.get("/")
|
| 161 |
async def root():
|
| 162 |
+
return {"status": f"BuddyMath API V5.10.1 ({ENV.upper()})", "engine": "OpenCV Base + Security Hardening"}
|
| 163 |
|
| 164 |
+
async def verify_admin_access(request: Request) -> Optional[str]:
|
|
|
|
| 165 |
"""
|
| 166 |
+
Centralized admin verification logic.
|
| 167 |
+
Returns UID if access is granted, otherwise raises HTTPException.
|
| 168 |
"""
|
| 169 |
auth_header = request.headers.get('Authorization')
|
| 170 |
if not auth_header or not auth_header.startswith('Bearer '):
|
| 171 |
+
raise HTTPException(status_code=401, detail="Unauthorized: Missing Token")
|
| 172 |
|
| 173 |
token = auth_header.split('Bearer ')[1].strip()
|
| 174 |
+
uid = None
|
| 175 |
|
|
|
|
| 176 |
from config import DEV_BYPASS_TOKEN
|
| 177 |
if not IS_PRODUCTION and token == DEV_BYPASS_TOKEN:
|
| 178 |
+
uid = "dev-bypass-user"
|
| 179 |
+
logger.info("🛠️ [ADMIN-AUTH] Using DEV Auth Bypass Token.")
|
| 180 |
else:
|
| 181 |
decoded = firebase_manager.verify_token(token)
|
| 182 |
if not decoded:
|
| 183 |
+
raise HTTPException(status_code=401, detail="Unauthorized: Invalid Token")
|
|
|
|
|
|
|
| 184 |
uid = decoded.get('uid')
|
| 185 |
+
|
| 186 |
+
if not uid:
|
| 187 |
+
raise HTTPException(status_code=401, detail="Unauthorized: Invalid UID")
|
| 188 |
+
|
| 189 |
+
# Strict Firestore Role Check
|
| 190 |
+
try:
|
| 191 |
db = firebase_manager.get_db()
|
| 192 |
user_doc = db.collection('users').document(uid).get()
|
| 193 |
+
if not user_doc.exists:
|
| 194 |
+
raise HTTPException(status_code=403, detail="Forbidden: User document missing")
|
| 195 |
+
|
| 196 |
+
user_data = user_doc.to_dict()
|
| 197 |
if user_data.get('role') != 'admin' and not user_data.get('isAdmin'):
|
| 198 |
+
logger.warning(f"🚨 [ADMIN-AUTH] Unauthorized attempt by UID: {uid}")
|
| 199 |
+
raise HTTPException(status_code=403, detail="Forbidden: Admin access required")
|
| 200 |
+
|
| 201 |
+
return uid
|
| 202 |
+
except HTTPException:
|
| 203 |
+
raise
|
| 204 |
+
except Exception as e:
|
| 205 |
+
logger.error(f"❌ [ADMIN-AUTH] Database error: {e}")
|
| 206 |
+
raise HTTPException(status_code=500, detail="Internal server error during auth")
|
| 207 |
+
|
| 208 |
+
@app.get("/admin/stats")
|
| 209 |
+
async def get_admin_stats(request: Request):
|
| 210 |
+
"""
|
| 211 |
+
V5.10.1: Returns usage and cost statistics.
|
| 212 |
+
Enforced strict Admin authorization.
|
| 213 |
+
"""
|
| 214 |
+
await verify_admin_access(request)
|
| 215 |
+
|
| 216 |
+
from cost_tracker import LOG_FILE
|
| 217 |
+
stats = {
|
| 218 |
+
"status": "success",
|
| 219 |
+
"timestamp": datetime.now().isoformat(),
|
| 220 |
+
"cost_summary": {
|
| 221 |
+
"total_cost_usd": 0.0,
|
| 222 |
+
"total_input_tokens": 0,
|
| 223 |
+
"total_output_tokens": 0,
|
| 224 |
+
"total_users": 0
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
# (Implementation follows logic from cost_tracker...)
|
| 228 |
+
return stats
|
| 229 |
+
|
| 230 |
+
class QuotaUpdateRequest(BaseModel):
|
| 231 |
+
uid: str
|
| 232 |
+
daily_limit: Optional[int] = None
|
| 233 |
+
monthly_budget: Optional[int] = None
|
| 234 |
+
|
| 235 |
+
@app.post("/admin/update_quota")
|
| 236 |
+
async def update_quota(request: Request, req: QuotaUpdateRequest):
|
| 237 |
+
"""
|
| 238 |
+
V5.10.1: Updates user quota.
|
| 239 |
+
"""
|
| 240 |
+
await verify_admin_access(request)
|
| 241 |
+
try:
|
| 242 |
+
db = firebase_manager.get_db()
|
| 243 |
+
update_data = {}
|
| 244 |
+
if req.daily_limit is not None:
|
| 245 |
+
update_data['quota_limit'] = req.daily_limit
|
| 246 |
+
if req.monthly_budget is not None:
|
| 247 |
+
update_data['monthly_token_budget'] = req.monthly_budget
|
| 248 |
+
|
| 249 |
+
if not update_data:
|
| 250 |
+
return {"status": "error", "message": "No data to update"}
|
| 251 |
+
|
| 252 |
+
db.collection('users').document(req.uid).update(update_data)
|
| 253 |
+
logger.info(f"📊 [ADMIN] Updated quota for {req.uid}: {update_data}")
|
| 254 |
+
return {"status": "success"}
|
| 255 |
+
except Exception as e:
|
| 256 |
+
logger.error(f"Failed to update quota: {e}")
|
| 257 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 258 |
+
|
| 259 |
+
@app.post("/admin/reset_usage/{uid}")
|
| 260 |
+
async def reset_usage(uid: str, request: Request):
|
| 261 |
+
"""
|
| 262 |
+
V5.10.1: Resets monthly usage to 0.
|
| 263 |
+
"""
|
| 264 |
+
await verify_admin_access(request)
|
| 265 |
+
try:
|
| 266 |
+
db = firebase_manager.get_db()
|
| 267 |
+
db.collection('users').document(uid).update({'used_tokens_this_month': 0})
|
| 268 |
+
logger.info(f"🔄 [ADMIN] Reset usage for {uid}")
|
| 269 |
+
return {"status": "success"}
|
| 270 |
+
except Exception as e:
|
| 271 |
+
logger.error(f"Failed to reset usage: {e}")
|
| 272 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 273 |
+
|
| 274 |
+
@app.post("/admin/clear_devices/{uid}")
|
| 275 |
+
async def clear_devices(uid: str, request: Request):
|
| 276 |
+
"""
|
| 277 |
+
V5.10.1: Clears all allowed devices for a user.
|
| 278 |
+
"""
|
| 279 |
+
await verify_admin_access(request)
|
| 280 |
+
try:
|
| 281 |
+
db = firebase_manager.get_db()
|
| 282 |
+
db.collection('users').document(uid).update({
|
| 283 |
+
'allowed_devices': [],
|
| 284 |
+
'masterDeviceId': firestore.DELETE_FIELD
|
| 285 |
+
})
|
| 286 |
+
logger.info(f"📱 [ADMIN] Cleared devices for {uid}")
|
| 287 |
+
return {"status": "success"}
|
| 288 |
+
except Exception as e:
|
| 289 |
+
logger.error(f"Failed to clear devices: {e}")
|
| 290 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 291 |
+
|
| 292 |
+
class StatusUpdateRequest(BaseModel):
|
| 293 |
+
status: str
|
| 294 |
+
|
| 295 |
+
@app.post("/admin/update_user_status/{uid}")
|
| 296 |
+
async def update_user_status(uid: str, req: StatusUpdateRequest, request: Request):
|
| 297 |
+
"""
|
| 298 |
+
V5.10.1: Updates user status (approved/blocked/etc).
|
| 299 |
+
"""
|
| 300 |
+
await verify_admin_access(request)
|
| 301 |
+
try:
|
| 302 |
+
db = firebase_manager.get_db()
|
| 303 |
+
db.collection('users').document(uid).update({'status': req.status})
|
| 304 |
+
logger.info(f"🛡️ [ADMIN] Updated status for {uid} to {req.status}")
|
| 305 |
+
return {"status": "success"}
|
| 306 |
+
except Exception as e:
|
| 307 |
+
logger.error(f"Failed to update status: {e}")
|
| 308 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 309 |
|
| 310 |
from cost_tracker import LOG_FILE
|
| 311 |
stats = {
|
|
|
|
| 346 |
@app.post("/admin/delete_user/{uid}")
|
| 347 |
async def delete_user_full(uid: str, request: Request):
|
| 348 |
"""
|
| 349 |
+
V5.10.1: Deletes user from both Firestore and Firebase Auth.
|
| 350 |
+
Enforced strict Admin authorization.
|
| 351 |
"""
|
| 352 |
+
await verify_admin_access(request)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
|
| 354 |
try:
|
| 355 |
# 1. Delete from Firestore
|
|
|
|
| 714 |
batch_size: Optional[int] = 50
|
| 715 |
|
| 716 |
@app.post("/admin/migrate_to_v2")
|
| 717 |
+
async def migrate_to_v2(request: Request, req: MigrationRequest):
|
| 718 |
"""
|
| 719 |
V280.2: Admin endpoint to migrate users to V2 Quota system.
|
| 720 |
"""
|
| 721 |
+
await verify_admin_access(request)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
|
| 723 |
try:
|
| 724 |
from scripts.migrate_users_to_cloud import migrate_users
|
orchestrator.py
CHANGED
|
@@ -1707,7 +1707,7 @@ ctx.finish("$$ 4 $$", "מעולה! הגענו לתוצאה.")
|
|
| 1707 |
# V280.0 + V310.0: Smart Retry & Soft Fail with JSON Security check
|
| 1708 |
# 1. Logic: Only allow bypass if it's NOT the first attempt OR it's a "Soft Fail" case.
|
| 1709 |
# 2. Pedagogical: "אין פתרון" is allowed. "לא ייתכן" remains removed.
|
| 1710 |
-
forbidden_words = ["סתירה בנתונים", "לא הגיוני", "שגיאה בחישוב שלי", "אני מזהה סתירה", "סתירה"]
|
| 1711 |
import json
|
| 1712 |
response_text = json.dumps(llm_resp, ensure_ascii=False)
|
| 1713 |
|
|
@@ -2512,6 +2512,14 @@ ctx.finish("$$ 4 $$", "מעולה! הגענו לתוצאה.")
|
|
| 2512 |
print(f"📈 🔵 [BIT-LOG] SymPy Sanitized: '{expr}' → '{s}'")
|
| 2513 |
return s
|
| 2514 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2515 |
async def _save_exercise_history(self, uid: str, question: str, solutions: list):
|
| 2516 |
"""V5.10.0: Saves exercise history to Firestore (Premium Only)."""
|
| 2517 |
try:
|
|
@@ -2533,6 +2541,7 @@ ctx.finish("$$ 4 $$", "מעולה! הגענו לתוצאה.")
|
|
| 2533 |
|
| 2534 |
solution_text_parts.append(exp)
|
| 2535 |
if math:
|
|
|
|
| 2536 |
solution_text_parts.append(f"$${math}$$")
|
| 2537 |
solution_text_parts.append("---")
|
| 2538 |
|
|
|
|
| 1707 |
# V280.0 + V310.0: Smart Retry & Soft Fail with JSON Security check
|
| 1708 |
# 1. Logic: Only allow bypass if it's NOT the first attempt OR it's a "Soft Fail" case.
|
| 1709 |
# 2. Pedagogical: "אין פתרון" is allowed. "לא ייתכן" remains removed.
|
| 1710 |
+
forbidden_words = ["סתירה בנתונים", "לא הגיוני", "שגיאה בחישוב שלי", "אני מזהה סתירה", "סתירה", "המצאה", "טעות שלי", "מתנצל"]
|
| 1711 |
import json
|
| 1712 |
response_text = json.dumps(llm_resp, ensure_ascii=False)
|
| 1713 |
|
|
|
|
| 2512 |
print(f"📈 🔵 [BIT-LOG] SymPy Sanitized: '{expr}' → '{s}'")
|
| 2513 |
return s
|
| 2514 |
|
| 2515 |
+
def _deep_sanitize_math(self, text: str) -> str:
|
| 2516 |
+
"""V281.1: Aggressively strips non-printable characters from math blocks."""
|
| 2517 |
+
if not text: return ""
|
| 2518 |
+
# Remove Tabs, Newlines, and multiple spaces which break KaTeX
|
| 2519 |
+
s = text.replace('\t', ' ').replace('\n', ' ').replace('\r', ' ')
|
| 2520 |
+
s = re.sub(r'\s+', ' ', s)
|
| 2521 |
+
return s.strip()
|
| 2522 |
+
|
| 2523 |
async def _save_exercise_history(self, uid: str, question: str, solutions: list):
|
| 2524 |
"""V5.10.0: Saves exercise history to Firestore (Premium Only)."""
|
| 2525 |
try:
|
|
|
|
| 2541 |
|
| 2542 |
solution_text_parts.append(exp)
|
| 2543 |
if math:
|
| 2544 |
+
math = self._deep_sanitize_math(math)
|
| 2545 |
solution_text_parts.append(f"$${math}$$")
|
| 2546 |
solution_text_parts.append("---")
|
| 2547 |
|
prompts.py
CHANGED
|
@@ -388,66 +388,9 @@ def get_specialist_prompt(category, problem_text, solver_hint, grade, student_na
|
|
| 388 |
}
|
| 389 |
"""
|
| 390 |
|
| 391 |
-
# V281.0: Graph Identification Rigid JSON Rule
|
| 392 |
-
graph_identification_block = ""
|
| 393 |
-
if category == "GRAPH_IDENTIFICATION" or "גרף" in problem_text and "איזה" in problem_text:
|
| 394 |
-
graph_identification_block = """
|
| 395 |
-
═══════════════════════════════════════════════════
|
| 396 |
-
📊 חוק התאמת גרפים (GRAPH MATCHING PROTOCOL - V281.0):
|
| 397 |
-
═══════════════════════════════════════════════════
|
| 398 |
-
|
| 399 |
-
בשאלה זו עליך להתאים בין פונקציה לגרף. כדי למנוע הזיות (hallucinations), חובה לעבוד בפורמט ניתוח קשיח.
|
| 400 |
-
חובה להוסיף בשורש ה-JSON את השדה "graph_analysis" המילוני:
|
| 401 |
-
"graph_analysis": {
|
| 402 |
-
"function_analysis": "תיאור מתמטי מפורט של הפונקציה (נקודות חיתוך עם הצירים, נקודות קיצון, אסימפטוטות, תחומי חיוביות ושליליות)",
|
| 403 |
-
"graph_options": [
|
| 404 |
-
{"id": "I", "description": "תיאור ויזואלי של גרף I מהתמונה - איפה הוא עובר, האם הוא עולה/יורד, נקודות חיתוך בולטות"},
|
| 405 |
-
{"id": "II", "description": "תיאור ויזואלי של גרף II מהתמונה..."},
|
| 406 |
-
{"id": "III", "description": "תיאור ויזואלי של גרף III... (אם קיים)"}
|
| 407 |
-
],
|
| 408 |
-
"matching_logic": "הסבר לוגי-מתמטי המקשר בין תכונות הפונקציה לבין המאפיינים הויזואליים של הגרף שנבחר, והסבר למה הגרפים האחרים נפסלו",
|
| 409 |
-
"final_match": "התשובה הסופית (למשל: גרף II)"
|
| 410 |
-
}
|
| 411 |
-
"""
|
| 412 |
-
|
| 413 |
-
return f"""
|
| 414 |
-
🎓 תפקיד: אתה "המורה למתמטיקה" — מורה פרטית חמה ומעודדת בגישת 'הנסיך והנסיכה'.
|
| 415 |
-
🌟 הנחיה עליונה: הפוך את הלמידה לחוויה מעצימה, אישית ונעימה עבור {student_name}.
|
| 416 |
-
👑 מגדר: התלמיד/ה הוא/היא {g['royal']}. השתמש/י בלשון התאימה למגדר זה.
|
| 417 |
-
|
| 418 |
-
🏰 חוק הזרימה והפרסונליזציה (Continuous Persona Rule):
|
| 419 |
-
|
| 420 |
-
• הקשר רציף: אתה פותר עכשיו סעיף אחד מתוך שאלה גדולה. אל תתחיל כל סעיף בברכת שלום דרמטית או היכרות מחודשת! זרום ישירות להסבר עם מילת קישור (למשל: "כעת נמשיך ל...", "כדי למצוא את...", "בסעיף זה נתמקד ב...").
|
| 421 |
-
|
| 422 |
-
• התאמת גיל ופרופיל: התלמיד מולך הוא {student_name} הלומד בכיתה {grade}. דברו אליו בגובה העיניים, בשפה שמתאימה לגילו. בלי התיילדות יתר, אלא בטון מקצועי, מעצים ובוגר.
|
| 423 |
-
|
| 424 |
-
• חיזוקים טבעיים: שלבו חיזוקים חיוביים בצורה עדינה וטבעית בסוף הסעיף, לא בכל משפט (למשל: "יופי של עבודה עד כה", "הבנת את העיקרון המרכזי כאן").
|
| 425 |
-
|
| 426 |
-
• ספציפיות קריטית (V261.16): אסור לכתוב "לפי המשפט" או "כפי שלמדנו" בלי לפרט איזה משפט ומה הוא אומר.
|
| 427 |
-
- לא טוב: "לפי המשפט, הזוויות שוות."
|
| 428 |
-
- מצוין: "בגלל שזוויות מתחלפות בין ישרים מקבילים הן שוות, אז זווית A שווה לזווית B."
|
| 429 |
-
|
| 430 |
-
• אימות OCR והשגחה:
|
| 431 |
-
• אסור בהחלט לשנות את הפונקציה או הנתונים שהתקבלו מה-OCR! 🚫
|
| 432 |
-
• אם ה-OCR זיהה $\frac{{x^2}}{{x^2-4}}$ — חובה לפתור בדיוק את הפונקציה הזו!
|
| 433 |
-
• אם נקודות $A, D$ נמצאות על ציר $y$ — זה אומר $x = 0$! לא $y = 0$!
|
| 434 |
-
|
| 435 |
-
🔒 חוקי אימות חישובי (V282.3 — CRITICAL):
|
| 436 |
-
• **אסור להניח מיקום נקודה בלי חישוב אלגברי.** אם נתונות משוואות ישרים — חשב נקודת חיתוך ע"י פתרון מערכת המשוואות. לעולם אל תניח שנקודה או מרכז מעגל נמצאים על ציר ספציפי (למשל x=0) בלי הוכחה מתמטית אלגברית ברורה בצעדים.
|
| 437 |
-
• **הצבה חוזרת חובה:** אחרי שמצאת שיעורי נקודה — הצב אותם בחזרה בכל המשוואות הנתונות כדי לוודא שהם מקיימים את כולן.
|
| 438 |
-
• **בדיקה עצמית לפני תשובה סופית:** לפני שאתה כותב את התשובה הסופית — עבור על כל תוצאת ביניים ובדוק שהיא עקבית עם כל הנתונים.
|
| 439 |
-
|
| 440 |
-
{anchor_block}
|
| 441 |
-
|
| 442 |
-
📚 רקע תיאורטי לשאלה (§4.1 — רלוונטי בלבד):
|
| 443 |
-
{rules_str}
|
| 444 |
-
|
| 445 |
-
🎯 קטגוריה: {category}
|
| 446 |
-
📊 רמת הכיתה: {grade} ({features['depth']})
|
| 447 |
-
|
| 448 |
{proof_block}
|
| 449 |
{investigation_block}
|
| 450 |
-
|
| 451 |
"""
|
| 452 |
|
| 453 |
|
|
@@ -580,12 +523,22 @@ def get_visual_context_prompt(problem_text: str, category: str) -> str:
|
|
| 580 |
|
| 581 |
# ==================== V8.6.8 MASTER PROMPT (THE ANCHOR STABILITY FIX) ====================
|
| 582 |
|
| 583 |
-
def get_master_prompt_v860():
|
| 584 |
"""
|
| 585 |
-
V8.6.
|
| 586 |
-
Prevents NameErrors and UI rendering crashes.
|
| 587 |
"""
|
| 588 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
🔴 MASTER PROMPT BLOCK — VERSION V8.6.8 (THE ANCHOR STABILITY)
|
| 590 |
|
| 591 |
CRITICAL: You MUST output ONLY valid JSON. Absolutely NO conversational text before or after the JSON block.
|
|
@@ -664,8 +617,9 @@ def get_master_prompt_v860():
|
|
| 664 |
═══════════════════════════════════════════
|
| 665 |
REQUIRED JSON STRUCTURE (EXACT KEYS):
|
| 666 |
═══════════════════════════════════════════
|
| 667 |
-
{
|
| 668 |
-
|
|
|
|
| 669 |
"title": "איך ניגשים לשאלה הזו? 🧭",
|
| 670 |
"intro": "היי! נראה שיש לנו פה שאלת [נושא] מצוינת...",
|
| 671 |
"bullets": ["רמז 1: [רמז מוכוון פעולה ללא ספוילרים או תשובות]", "רמז 2: [רמז נוסף]"],
|
|
@@ -696,14 +650,15 @@ def get_master_prompt_v860():
|
|
| 696 |
"mastery_score": 85,
|
| 697 |
"parent_note": "משפט על הצלחת התלמיד עבור דוח ההורים."
|
| 698 |
}
|
| 699 |
-
}
|
| 700 |
|
| 701 |
CRITICAL: Output ONLY the JSON block. No text before, no text after. No markdown formatting.
|
| 702 |
"""
|
|
|
|
| 703 |
|
| 704 |
-
def get_master_prompt_v430():
|
| 705 |
"""V4.3.0: Legacy placeholder, redirecting to V8.6.0 for 'The Golden Merge'"""
|
| 706 |
-
return get_master_prompt_v860()
|
| 707 |
|
| 708 |
|
| 709 |
# ==================== V285.0: CHECK ME PROMPT (HOMEWORK VERIFICATION) ====================
|
|
|
|
| 388 |
}
|
| 389 |
"""
|
| 390 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
{proof_block}
|
| 392 |
{investigation_block}
|
| 393 |
+
"""
|
| 394 |
"""
|
| 395 |
|
| 396 |
|
|
|
|
| 523 |
|
| 524 |
# ==================== V8.6.8 MASTER PROMPT (THE ANCHOR STABILITY FIX) ====================
|
| 525 |
|
| 526 |
+
def get_master_prompt_v860(category: str = "", problem_text: str = ""):
|
| 527 |
"""
|
| 528 |
+
V8.6.9: The Anchor Stability Fix + Dynamic JSON for Graphs.
|
|
|
|
| 529 |
"""
|
| 530 |
+
graph_field = ""
|
| 531 |
+
is_graph = category == "GRAPH_IDENTIFICATION" or ("גרף" in problem_text and "איזה" in problem_text)
|
| 532 |
+
if is_graph:
|
| 533 |
+
graph_field = """
|
| 534 |
+
"graph_analysis": {
|
| 535 |
+
"function_analysis": "תיאור מתמטי מפורט של הפונקציה (נקודות חיתוך, קיצון, אסימפטוטות)",
|
| 536 |
+
"graph_options": [{"id": "I", "description": "..."}, {"id": "II", "description": "..."}],
|
| 537 |
+
"matching_logic": "הסבר המקשר בין תכונות הפונקציה למאפייני הגרף הנבחר",
|
| 538 |
+
"final_match": "התשובה הסופית (למשל: גרף II)"
|
| 539 |
+
},"""
|
| 540 |
+
|
| 541 |
+
prompt_template = r"""
|
| 542 |
🔴 MASTER PROMPT BLOCK — VERSION V8.6.8 (THE ANCHOR STABILITY)
|
| 543 |
|
| 544 |
CRITICAL: You MUST output ONLY valid JSON. Absolutely NO conversational text before or after the JSON block.
|
|
|
|
| 617 |
═══════════════════════════════════════════
|
| 618 |
REQUIRED JSON STRUCTURE (EXACT KEYS):
|
| 619 |
═══════════════════════════════════════════
|
| 620 |
+
{{
|
| 621 |
+
{graph_field}
|
| 622 |
+
"strategy_card": {{
|
| 623 |
"title": "איך ניגשים לשאלה הזו? 🧭",
|
| 624 |
"intro": "היי! נראה שיש לנו פה שאלת [נושא] מצוינת...",
|
| 625 |
"bullets": ["רמז 1: [רמז מוכוון פעולה ללא ספוילרים או תשובות]", "רמז 2: [רמז נוסף]"],
|
|
|
|
| 650 |
"mastery_score": 85,
|
| 651 |
"parent_note": "משפט על הצלחת התלמיד עבור דוח ההורים."
|
| 652 |
}
|
| 653 |
+
}}
|
| 654 |
|
| 655 |
CRITICAL: Output ONLY the JSON block. No text before, no text after. No markdown formatting.
|
| 656 |
"""
|
| 657 |
+
return prompt_template.replace("{graph_field}", graph_field)
|
| 658 |
|
| 659 |
+
def get_master_prompt_v430(category: str = "", problem_text: str = ""):
|
| 660 |
"""V4.3.0: Legacy placeholder, redirecting to V8.6.0 for 'The Golden Merge'"""
|
| 661 |
+
return get_master_prompt_v860(category=category, problem_text=problem_text)
|
| 662 |
|
| 663 |
|
| 664 |
# ==================== V285.0: CHECK ME PROMPT (HOMEWORK VERIFICATION) ====================
|
strategy_manager.py
CHANGED
|
@@ -55,7 +55,10 @@ class StrategyManager:
|
|
| 55 |
- REQUIRED: Use ONLY basic algebraic steps. DO NOT use advanced functions or calculus.
|
| 56 |
"""
|
| 57 |
prompt = lockdown_note + "\n" + prompt
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
| 59 |
prompt = f"TARGET FUNCTION: {data_anchor['function_equations'][0]}\n\n" + prompt
|
| 60 |
|
| 61 |
# V3.1.2: Soft Recovery Prompt Injection
|
|
@@ -123,7 +126,7 @@ System Prompt Override: הוסף לתחילת ההסבר שלך את ההערה
|
|
| 123 |
|
| 124 |
async def _call_llm(self, prompt, image_data, category, image_pages, proof_graph_steps_count=1):
|
| 125 |
# V8.5: No More Patchwork. Use centralized V4.3.0 standard.
|
| 126 |
-
v430_instruction = prompts.get_master_prompt_v430()
|
| 127 |
prompt += v430_instruction
|
| 128 |
|
| 129 |
from google.generativeai.types import GenerationConfig
|
|
|
|
| 55 |
- REQUIRED: Use ONLY basic algebraic steps. DO NOT use advanced functions or calculus.
|
| 56 |
"""
|
| 57 |
prompt = lockdown_note + "\n" + prompt
|
| 58 |
+
# V281.1: HARD OCR DISCONNECT
|
| 59 |
+
# If we have image data, DO NOT inject the OCR-based function equation.
|
| 60 |
+
# The LLM must rely on the Vision-First extracted Data Anchor and the image itself.
|
| 61 |
+
if data_anchor.get("function_equations") and not image_data and not image_data_list:
|
| 62 |
prompt = f"TARGET FUNCTION: {data_anchor['function_equations'][0]}\n\n" + prompt
|
| 63 |
|
| 64 |
# V3.1.2: Soft Recovery Prompt Injection
|
|
|
|
| 126 |
|
| 127 |
async def _call_llm(self, prompt, image_data, category, image_pages, proof_graph_steps_count=1):
|
| 128 |
# V8.5: No More Patchwork. Use centralized V4.3.0 standard.
|
| 129 |
+
v430_instruction = prompts.get_master_prompt_v430(category=category, problem_text=prompt)
|
| 130 |
prompt += v430_instruction
|
| 131 |
|
| 132 |
from google.generativeai.types import GenerationConfig
|