Spaces:
Running
Running
| """ | |
| 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 | |