mathpulse-api-v3test / services /question_bank_service.py
github-actions[bot]
🚀 Auto-deploy backend from GitHub (93e7c2a)
92bfe31
"""
Question Bank Service for Quiz Battle.
Handles querying the question bank with random ordering,
caching session questions, and 24-hour debounce for variance results.
"""
import os
import random
from datetime import datetime, timezone, timedelta
from typing import List, Dict, Optional
from google.cloud import firestore
DEFAULT_FIREBASE_PROJECT = os.getenv("FIREBASE_AUTH_PROJECT_ID", "mathpulse-ai-2026")
def _get_db() -> firestore.Client:
"""Get Firestore client."""
return firestore.Client(project=DEFAULT_FIREBASE_PROJECT)
async def get_questions_for_battle(
grade_level: int,
topic: str,
count: int = 10,
) -> List[Dict]:
"""
Fetch random questions from the question bank for a battle session.
Uses Firestore random_seed field for pseudo-random ordering.
If fewer than `count` questions exist, returns all available.
"""
db = _get_db()
collection_path = f"question_bank/{grade_level}/{topic}/questions"
collection_ref = db.collection(collection_path)
# Pseudo-random query using random_seed >= random threshold
threshold = random.random()
query = (
collection_ref
.where("random_seed", ">=", threshold)
.order_by("random_seed")
.limit(count)
)
docs = list(query.stream())
# If we didn't get enough, query from the start to fill shortfall
if len(docs) < count:
remaining = count - len(docs)
fallback_query = (
collection_ref
.where("random_seed", "<", threshold)
.order_by("random_seed")
.limit(remaining)
)
docs.extend(list(fallback_query.stream()))
questions = [doc.to_dict() for doc in docs]
# Ensure all required fields are present
valid_questions = []
for q in questions:
if q and all(k in q for k in ("question", "choices", "correct_answer", "difficulty")):
valid_questions.append(q)
return valid_questions
async def cache_session_questions(
session_id: str,
questions: List[Dict],
player_ids: List[str],
grade_level: int,
topic: str,
) -> None:
"""Cache varied questions for a battle session with 24-hour TTL."""
db = _get_db()
session_ref = db.collection("quiz_battle_sessions").document(session_id)
session_ref.set({
"player_ids": player_ids,
"grade_level": grade_level,
"topic": topic,
"created_at": firestore.SERVER_TIMESTAMP,
"variance_cached_until": datetime.now(timezone.utc) + timedelta(hours=24),
})
# Write questions to subcollection
batch = db.batch()
for idx, q in enumerate(questions):
q_ref = session_ref.collection("questions").document(str(idx))
batch.set(q_ref, q)
batch.commit()
async def get_cached_session(session_id: str) -> Optional[List[Dict]]:
"""
Check if a session has cached varied questions within 24 hours.
Returns the cached questions if valid, otherwise None.
"""
db = _get_db()
session_doc = db.collection("quiz_battle_sessions").document(session_id).get()
if not session_doc.exists:
return None
data = session_doc.to_dict()
cached_until = data.get("variance_cached_until")
if cached_until:
if isinstance(cached_until, datetime):
if cached_until.tzinfo is None:
cached_until = cached_until.replace(tzinfo=timezone.utc)
elif hasattr(cached_until, 'timestamp'):
# Firestore Timestamp object
cached_until = datetime.fromtimestamp(cached_until.timestamp(), tz=timezone.utc)
if cached_until > datetime.now(timezone.utc):
# Return cached questions
q_docs = db.collection("quiz_battle_sessions").document(session_id).collection("questions").stream()
questions = [doc.to_dict() for doc in q_docs]
return questions if questions else None
return None