""" Shared domain models for the multi-tenant AI Quiz system. Every request carries school_id + board_id to route to the correct tenant silo. """ from typing import List, Optional, Dict, Any from pydantic import BaseModel, Field, model_validator, ConfigDict class UserProfile(BaseModel): """Unified profile for student, teacher, admin, and super_admin roles.""" model_config = ConfigDict(str_strip_whitespace=True) user_id: str = "student-default" name: str = "Student" school_id: Optional[str] = None board_id: Optional[str] = None role: str = "student" class_id: Optional[str] = None section_id: Optional[str] = "A" subjects: List[str] = Field(default_factory=lambda: ["Science", "Mathematics", "English"]) sub_subject: Optional[str] = "General" class PermissionRequest(BaseModel): user: UserProfile action: str resource: Dict[str, Any] = Field(default_factory=dict) class PermissionDecision(BaseModel): allowed: bool reason: str user_id: str timestamp: Optional[str] = None class TrainingDeleteRequest(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) user: Optional[UserProfile] = None user_id: Optional[str] = None user_name: Optional[str] = None user_role: Optional[str] = None school_id: str board_id: str class_id: str subject: str section_id: Optional[str] = "A" sub_subject: Optional[str] = "General" chapter: Optional[str] = None lesson: Optional[str] = None topic: Optional[str] = None @model_validator(mode="before") @classmethod def sync_user_and_tenant(cls, data: Any) -> Any: if isinstance(data, dict): effective_ch = data.get("chapter") or data.get("lesson") if effective_ch: data.setdefault("chapter", effective_ch) data.setdefault("lesson", effective_ch) user_raw = data.get("user") if isinstance(user_raw, dict): for k in ("school_id", "board_id", "class_id", "section_id", "sub_subject"): if not user_raw.get(k) and data.get(k): user_raw[k] = data[k] return data @property def effective_lesson(self) -> str: return self.lesson or self.chapter or "" class DescriptiveConfigItem(BaseModel): level: str = "medium" # easy, medium, hard format: str = "short" # short, long, comprehensive count: int = 1 class QuizGenerateRequest(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) user: Optional[UserProfile] = None school_id: Optional[str] = None board_id: Optional[str] = None class_id: Optional[str] = None section_id: Optional[str] = "A" subject: str sub_subject: Optional[str] = "General" chapter: Optional[str] = None lesson: Optional[str] = None topic: Optional[str] = None num_questions: int = 5 hf_token: Optional[str] = None difficulty: Optional[str] = "medium" # easy, medium, hard question_format: Optional[str] = "short" # short, long, comprehensive descriptive_distribution: Optional[List[DescriptiveConfigItem]] = None @model_validator(mode="before") @classmethod def sync_tenant_fields(cls, data: Any) -> Any: if isinstance(data, dict): effective_ch = data.get("chapter") or data.get("lesson") if effective_ch: data.setdefault("chapter", effective_ch) data.setdefault("lesson", effective_ch) user_raw = data.get("user") if isinstance(user_raw, dict): for k in ("school_id", "board_id", "class_id", "section_id", "sub_subject"): if not data.get(k) and user_raw.get(k): data[k] = user_raw[k] elif not user_raw.get(k) and data.get(k): user_raw[k] = data[k] elif isinstance(user_raw, UserProfile): for k in ("school_id", "board_id", "class_id", "section_id", "sub_subject"): val = getattr(user_raw, k, None) if not data.get(k) and val: data[k] = val elif user_raw is None: data["user"] = { "user_id": "student-default", "name": "Student", "role": "student", "school_id": data.get("school_id") or "school_default", "board_id": data.get("board_id") or "cbse", "class_id": data.get("class_id") or "10", "section_id": data.get("section_id") or "A", "sub_subject": data.get("sub_subject") or "General", "subjects": [data.get("subject", "Science")] } return data @property def effective_lesson(self) -> str: return self.lesson or self.chapter or "" class QuizEvaluateRequest(BaseModel): user: Optional[UserProfile] = None school_id: Optional[str] = None board_id: Optional[str] = None class_id: Optional[str] = None section_id: Optional[str] = "A" subject: Optional[str] = None sub_subject: Optional[str] = "General" chapter: Optional[str] = None lesson: Optional[str] = None topic: Optional[str] = None question: str selected_option: str correct_option: str hf_token: Optional[str] = None @model_validator(mode="before") @classmethod def sync_tenant_fields(cls, data: Any) -> Any: if isinstance(data, dict): effective_ch = data.get("chapter") or data.get("lesson") if effective_ch: data.setdefault("chapter", effective_ch) data.setdefault("lesson", effective_ch) user_raw = data.get("user") if isinstance(user_raw, dict): for k in ("school_id", "board_id", "class_id", "section_id", "sub_subject"): if not data.get(k) and user_raw.get(k): data[k] = user_raw[k] elif not user_raw.get(k) and data.get(k): user_raw[k] = data[k] elif isinstance(user_raw, UserProfile): for k in ("school_id", "board_id", "class_id", "section_id", "sub_subject"): val = getattr(user_raw, k, None) if not data.get(k) and val: data[k] = val elif user_raw is None: data["user"] = { "user_id": "student-default", "name": "Student", "role": "student", "school_id": data.get("school_id") or "school_default", "board_id": data.get("board_id") or "cbse", "class_id": data.get("class_id") or "10", "section_id": data.get("section_id") or "A", "sub_subject": data.get("sub_subject") or "General", "subjects": [data.get("subject", "Science")] if data.get("subject") else ["Science"] } return data @property def effective_lesson(self) -> str: return self.lesson or self.chapter or "" class MCQOption(BaseModel): id: str text: str class QuizQuestion(BaseModel): id: int question: str options: List[MCQOption] correct_option: str topic: str explanation: str source_context: Optional[str] = None class QuizSet(BaseModel): school_id: str board_id: str class_id: str section_id: Optional[str] = "A" subject: str sub_subject: Optional[str] = "General" chapter: str lesson: Optional[str] = None topic: Optional[str] = None questions: List[QuizQuestion] class EvaluationResult(BaseModel): is_correct: bool selected_option: str correct_option: str explanation: str evidence_quote: str topic: str class TrainingStatus(BaseModel): school_id: str board_id: str chapters: Dict[str, Dict[str, Any]] # ── Student Anti-Cheat Quiz Models ────────────────────────────────────────── class StudentQuizQuestion(BaseModel): id: int question: str options: List[MCQOption] class StudentQuizSet(BaseModel): quiz_id: str school_id: str board_id: str class_id: str section_id: Optional[str] = "A" subject: str sub_subject: Optional[str] = "General" chapter: str lesson: Optional[str] = None topic: Optional[str] = None questions: List[StudentQuizQuestion] class StudentAnswerItem(BaseModel): question_id: int selected_option: str class StudentQuizSubmitRequest(BaseModel): user: Optional[UserProfile] = None quiz_id: str answers: List[StudentAnswerItem] hf_token: Optional[str] = None class StudentQuestionResult(BaseModel): question_id: int question: str selected_option: str correct_option: str is_correct: bool explanation: str evidence_quote: Optional[str] = None class StudentQuizResult(BaseModel): quiz_id: str score: int total: int percentage: float results: List[StudentQuestionResult] # ── Fill-in-the-Blanks Models (Append-Only) ─────────────────────────────────── class BlankQuestion(BaseModel): id: int question: str correct_answer: str acceptable_alternatives: List[str] = Field(default_factory=list) topic: str explanation: str source_context: Optional[str] = None class BlankQuizSet(BaseModel): school_id: str board_id: str class_id: str section_id: Optional[str] = "A" subject: str sub_subject: Optional[str] = "General" chapter: str lesson: Optional[str] = None topic: Optional[str] = None questions: List[BlankQuestion] class StudentBlankQuizQuestion(BaseModel): id: int question: str class StudentBlankQuizSet(BaseModel): quiz_id: str school_id: str board_id: str class_id: str section_id: Optional[str] = "A" subject: str sub_subject: Optional[str] = "General" chapter: str lesson: Optional[str] = None topic: Optional[str] = None questions: List[StudentBlankQuizQuestion] class StudentBlankAnswerItem(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) question_id: int student_answer: str class StudentBlankQuizSubmitRequest(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) user: Optional[UserProfile] = None quiz_id: str answers: List[StudentBlankAnswerItem] hf_token: Optional[str] = None class StudentBlankQuestionResult(BaseModel): question_id: int question: str student_answer: str correct_answer: str is_correct: bool explanation: str evidence_quote: Optional[str] = None class StudentBlankQuizResult(BaseModel): quiz_id: str score: int total: int percentage: float results: List[StudentBlankQuestionResult] # ── True/False Models (Append-Only) ────────────────────────────────────────── class TrueFalseQuestion(BaseModel): id: int question: str options: List[MCQOption] correct_option: str # "A" or "B" topic: Optional[str] = "General" explanation: str source_context: Optional[str] = None class TrueFalseQuizSet(BaseModel): school_id: str board_id: str class_id: str section_id: Optional[str] = "A" subject: str sub_subject: Optional[str] = "General" chapter: str lesson: Optional[str] = None topic: Optional[str] = None questions: List[TrueFalseQuestion] class StudentTrueFalseQuizQuestion(BaseModel): id: int question: str options: List[MCQOption] class StudentTrueFalseQuizSet(BaseModel): quiz_id: str school_id: str board_id: str class_id: str section_id: Optional[str] = "A" subject: str sub_subject: Optional[str] = "General" chapter: str lesson: Optional[str] = None topic: Optional[str] = None questions: List[StudentTrueFalseQuizQuestion] class StudentTrueFalseAnswerItem(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) question_id: int selected_option: str # "A" or "B" class StudentTrueFalseQuizSubmitRequest(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) user: Optional[UserProfile] = None quiz_id: str answers: List[StudentTrueFalseAnswerItem] hf_token: Optional[str] = None class StudentTrueFalseQuestionResult(BaseModel): question_id: int question: str selected_option: str correct_option: str is_correct: bool explanation: str evidence_quote: Optional[str] = None class StudentTrueFalseQuizResult(BaseModel): quiz_id: str score: int total: int percentage: float results: List[StudentTrueFalseQuestionResult] # ── Descriptive Question Models (Append-Only) ───────────────────────────────── class DescriptiveQuestion(BaseModel): id: int question: str model_answer: str key_facts: List[str] = Field(default_factory=list) difficulty: str = "medium" # easy, medium, hard format: str = "short" # short, long, comprehensive max_score: int = 5 topic: Optional[str] = "General" explanation: str source_context: Optional[str] = None class DescriptiveQuizSet(BaseModel): school_id: str board_id: str class_id: str section_id: Optional[str] = "A" subject: str sub_subject: Optional[str] = "General" chapter: str lesson: Optional[str] = None topic: Optional[str] = None questions: List[DescriptiveQuestion] class StudentDescriptiveQuizQuestion(BaseModel): id: int question: str difficulty: str = "medium" format: str = "short" max_score: int = 5 topic: Optional[str] = "General" class StudentDescriptiveQuizSet(BaseModel): quiz_id: str school_id: str board_id: str class_id: str section_id: Optional[str] = "A" subject: str sub_subject: Optional[str] = "General" chapter: str lesson: Optional[str] = None topic: Optional[str] = None questions: List[StudentDescriptiveQuizQuestion] class StudentDescriptiveAnswerItem(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) question_id: int student_answer: str class StudentDescriptiveQuizSubmitRequest(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) user: Optional[UserProfile] = None quiz_id: str answers: List[StudentDescriptiveAnswerItem] hf_token: Optional[str] = None class StudentDescriptiveQuestionResult(BaseModel): question_id: int question: str student_answer: str model_answer: str score: float max_score: int is_correct: bool feedback: str key_points_covered: List[str] = Field(default_factory=list) key_points_missing: List[str] = Field(default_factory=list) evidence_quote: Optional[str] = None explanation: Optional[str] = None class StudentDescriptiveQuizResult(BaseModel): quiz_id: str score: float total: int percentage: float results: List[StudentDescriptiveQuestionResult]