smart-advisor / src /utils /schemas.py
sajaahmed5
Smart Advisor deployment
74e8a7b
Raw
History Blame Contribute Delete
4.11 kB
"""
Pydantic models:
- StudentProfile: structured student data collected during onboarding
- API request/response schemas used by the FastAPI routes
"""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, Field
class StudentProfile(BaseModel):
"""Stores structured information about the student, collected during onboarding.
Used to personalise query rewriting and LLM answers."""
# Admission eligibility
gpa: float | None = Field(None, description="Tawjihi GPA as a percentage (0-100)", ge=0, le=100)
academic_track: Literal["ุนู„ู…ูŠ", "ุตู†ุงุนูŠ", "ุชูƒู†ูˆู„ูˆุฌูŠุง ู…ุนู„ูˆู…ุงุช", "ุฃุฏุจูŠ", "ุชุฌุงุฑูŠ", "ุฃุฎุฑู‰"] | None = Field(
None, description="High school academic track"
)
# Interests
likes_math: bool | None = Field(None, description="Enjoys mathematics and statistics")
has_programming_background: bool | None = Field(None, description="Has prior programming experience")
interest_areas: list[Literal[
"ุนู„ู… ุงู„ุจูŠุงู†ุงุช", "ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ", "ุฃู…ู† ุงู„ู…ุนู„ูˆู…ุงุช",
"ุดุจูƒุงุช ุงู„ุญุงุณูˆุจ", "ู‡ู†ุฏุณุฉ ุงู„ุญุงุณูˆุจ", "ุบูŠุฑ ู…ุญุฏุฏ"
]] = Field(default_factory=list, description="Areas of academic or professional interest")
# Academic goals
degree_preference: Literal["ุจูƒุงู„ูˆุฑูŠูˆุณ", "ุฏุจู„ูˆู…", "ุบูŠุฑ ู…ุญุฏุฏ"] | None = Field(
None, description="Preferred degree level"
)
# Priority fields that drive onboarding โ€” ordered by importance
_PRIORITY = ["gpa", "academic_track", "likes_math", "interest_areas", "degree_preference"]
def missing_fields(self) -> list[str]:
"""Return priority fields still None (or empty list for interest_areas)."""
result = []
for f in self._PRIORITY:
v = getattr(self, f)
if v is None or (isinstance(v, list) and len(v) == 0):
result.append(f)
return result
def is_complete(self) -> bool:
return len(self.missing_fields()) == 0
def to_context_string(self) -> str:
"""Arabic summary injected into prompts."""
parts = []
if self.gpa is not None:
parts.append(f"ุงู„ู…ุนุฏู„ ููŠ ุงู„ุซุงู†ูˆูŠุฉ: {self.gpa}%")
if self.academic_track:
parts.append(f"ุงู„ูุฑุน ุงู„ุฏุฑุงุณูŠ: {self.academic_track}")
# if self.enrollment_status:
# parts.append(f"ุงู„ุญุงู„ุฉ: {self.enrollment_status}")
if self.likes_math is not None:
parts.append("ูŠุณุชู…ุชุน ุจุงู„ุฑูŠุงุถูŠุงุช" if self.likes_math else "ู„ุง ูŠูุถู„ ุงู„ุฑูŠุงุถูŠุงุช")
# if self.has_programming_background is not None:
# parts.append("ู„ุฏูŠู‡ ุฎู„ููŠุฉ ุจุฑู…ุฌูŠุฉ" if self.has_programming_background else "ู„ุง ุชูˆุฌุฏ ุฎู„ููŠุฉ ุจุฑู…ุฌูŠุฉ")
if self.interest_areas:
parts.append(f"ุงู‡ุชู…ุงู…ุงุชู‡: {', '.join(self.interest_areas)}")
if self.degree_preference:
parts.append(f"ูŠุฑูŠุฏ: {self.degree_preference}")
# if self.needs_financial_aid:
# parts.append("ู…ู‡ุชู… ุจุงู„ู…ู†ุญ ุงู„ุฏุฑุงุณูŠุฉ")
return " | ".join(parts) if parts else "ู„ุง ุชูˆุฌุฏ ู…ุนู„ูˆู…ุงุช ุจุนุฏ"
class StudentContact(BaseModel):
"""Contact info collected only when a fallback to a human advisor is needed."""
name: str | None = None
email: str | None = None
phone: str | None = None
def missing(self) -> str | None:
if not self.name:
return "name"
if not self.email and not self.phone:
return "contact"
return None
# โ”€โ”€ API I/O schemas โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class ChatMessage(BaseModel):
role: Literal["user", "assistant"]
content: str
class ChatRequest(BaseModel):
session_id: str
message: str
class ChatResponse(BaseModel):
session_id: str
reply: str
onboarding: bool
awaiting_info: bool