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