Spaces:
Running
Running
| """ | |
| Variance Engine for Quiz Battle Questions. | |
| Applies per-session variance techniques via DeepSeek, | |
| with pure-Python fallback for choice shuffling. | |
| """ | |
| import json | |
| import random | |
| import re | |
| from typing import List, Dict | |
| from services.ai_client import get_deepseek_client, CHAT_MODEL | |
| from services.question_bank_service import get_cached_session, cache_session_questions | |
| def _fallback_shuffle(questions: List[Dict], seed: int) -> List[Dict]: | |
| """ | |
| Pure-Python fallback: shuffle choices deterministically. | |
| """ | |
| rng = random.Random(seed) | |
| for q in questions: | |
| choices = q["choices"].copy() | |
| correct_letter = q["correct_answer"] | |
| correct_index = ord(correct_letter) - ord("A") | |
| correct_text = choices[correct_index] | |
| rng.shuffle(choices) | |
| q["choices"] = choices | |
| q["correct_answer"] = chr(ord("A") + choices.index(correct_text)) | |
| q["variance_applied"] = ["choice_shuffle"] | |
| return questions | |
| async def apply_variance(questions: List[Dict], session_id: str) -> List[Dict]: | |
| """ | |
| Apply per-session variance to a list of questions. | |
| 1. Check 24h Firestore cache first | |
| 2. Call DeepSeek with variance prompt | |
| 3. Parse JSON response | |
| 4. Fall back to pure-Python shuffle if DeepSeek fails | |
| 5. Cache result for 24 hours | |
| """ | |
| # 1. Check cache | |
| cached = await get_cached_session(session_id) | |
| if cached: | |
| return cached | |
| # 2. Generate deterministic seed from session_id | |
| seed = hash(session_id) % (2**32) | |
| # 3. Call DeepSeek | |
| client = get_deepseek_client() | |
| system_prompt = ( | |
| "You are a math quiz variance engine for MathPulse AI, an educational platform for " | |
| "Filipino high school students following the DepEd K-12 curriculum. " | |
| "Your job is to make quiz questions feel fresh each session WITHOUT changing the " | |
| "correct answer or difficulty level." | |
| ) | |
| user_prompt = f"""Given these {len(questions)} quiz battle questions as JSON: | |
| {json.dumps(questions, indent=2)} | |
| Apply the following variance techniques. Use session_seed={seed} for deterministic but varied output: | |
| PARAPHRASE (30% chance per question): Reword the question stem using different phrasing, synonyms, or sentence structure. Do NOT change the math or the answer. | |
| CHOICE SHUFFLE (always): Randomize the order of answer choices A/B/C/D. Update "correct_answer" to reflect the new position. | |
| DISTRACTOR REFRESH (20% chance per question): Replace 1-2 wrong choices with new plausible-but-incorrect distractors that represent common student misconceptions for this topic. Keep the correct answer unchanged. | |
| CONTEXT SWAP (10% chance per question): Replace real-world context variables (names, objects, currencies) with Filipino-localized equivalents (e.g., "pesos", "jeepney", "barangay") to increase cultural relevance. | |
| NUMERIC SCALING (10% chance, only for computation problems): Scale numbers by a small integer factor (2x or 3x) so the method remains the same but the answer changes. Recompute the correct answer and all distractors accordingly. | |
| Return the full modified questions array as valid JSON only. Keep all original fields. | |
| Add a "variance_applied": ["paraphrase", "distractor_refresh", ...] field per question. | |
| Do NOT change "topic", "difficulty", "grade_level", or "source_chunk_id".""" | |
| try: | |
| response = client.chat.completions.create( | |
| model=CHAT_MODEL, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt}, | |
| ], | |
| temperature=0.5, | |
| max_tokens=4000, | |
| ) | |
| content = response.choices[0].message.content.strip() | |
| # Strip markdown code fences | |
| content = re.sub(r"^```json\s*", "", content) | |
| content = re.sub(r"\s*```$", "", content) | |
| varied_questions = json.loads(content) | |
| if not isinstance(varied_questions, list) or len(varied_questions) != len(questions): | |
| raise ValueError("Invalid response format from DeepSeek") | |
| # Validate required fields | |
| for q in varied_questions: | |
| if not all(k in q for k in ("question", "choices", "correct_answer", "variance_applied")): | |
| raise ValueError("Missing required fields in varied question") | |
| except Exception as e: | |
| print(f"[variance_engine] DeepSeek variance failed, falling back to shuffle: {e}") | |
| varied_questions = _fallback_shuffle(questions, seed) | |
| # 4. Cache for 24 hours | |
| # Extract player_ids, grade_level, topic from original questions if available | |
| player_ids = [] | |
| grade_level = questions[0].get("grade_level", 11) if questions else 11 | |
| topic = questions[0].get("topic", "general_mathematics") if questions else "general_mathematics" | |
| await cache_session_questions(session_id, varied_questions, player_ids, grade_level, topic) | |
| return varied_questions | |