Spaces:
Running
Running
File size: 4,930 Bytes
1959397 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | """
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
|