# app/main.py from fastapi import FastAPI, Depends, HTTPException, status from fastapi.middleware.cors import CORSMiddleware from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from contextlib import asynccontextmanager import uvicorn from app.database import engine, Base from app.routers import auth, sync, content, analytics, grading from app.core.config import settings from app.ml.model_manager import ModelManager import logging # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Initialize ML models on startup @asynccontextmanager async def lifespan(app: FastAPI): # Startup logger.info("Starting AI Tutor Backend...") # Create database tables Base.metadata.create_all(bind=engine) # Initialize ML models model_manager = ModelManager() await model_manager.load_models() # Store model manager in app state app.state.model_manager = model_manager logger.info("Backend startup complete") yield # Shutdown logger.info("Shutting down AI Tutor Backend...") app = FastAPI( title="AI Tutor Backend", description="Adaptive Multilingual Offline-First AI Tutor API", version="1.0.0", lifespan=lifespan ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.ALLOWED_HOSTS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Include routers app.include_router(auth.router, prefix="/api/v1/auth", tags=["authentication"]) app.include_router(sync.router, prefix="/api/v1/sync", tags=["synchronization"]) app.include_router(content.router, prefix="/api/v1/content", tags=["content"]) app.include_router(analytics.router, prefix="/api/v1/analytics", tags=["analytics"]) app.include_router(grading.router, prefix="/api/v1/grading", tags=["grading"]) @app.get("/") async def root(): return {"message": "AI Tutor Backend API", "version": "1.0.0"} @app.get("/health") async def health_check(): return {"status": "healthy", "version": "1.0.0"} if __name__ == "__main__": uvicorn.run( "app.main:app", host=settings.HOST, port=settings.PORT, reload=settings.DEBUG, log_level="info" ) # app/core/config.py from pydantic_settings import BaseSettings from typing import List class Settings(BaseSettings): # Basic settings APP_NAME: str = "AI Tutor Backend" DEBUG: bool = True HOST: str = "0.0.0.0" PORT: int = 8000 # Database DATABASE_URL: str = "postgresql://postgres:password@localhost/ai_tutor" # Security SECRET_KEY: str = "your-secret-key-change-in-production" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 * 24 * 60 # 30 days # CORS ALLOWED_HOSTS: List[str] = ["*"] # Redis (for caching and task queue) REDIS_URL: str = "redis://localhost:6379" # ML Models MODEL_PATH: str = "./models" HUGGINGFACE_CACHE: str = "./cache" class Config: env_file = ".env" settings = Settings() # app/database.py from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from app.core.config import settings engine = create_engine( settings.DATABASE_URL, pool_pre_ping=True, pool_recycle=300, ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() def get_db(): db = SessionLocal() try: yield db finally: db.close() # app/models/database.py from sqlalchemy import Column, Integer, String, Float, DateTime, Text, Boolean, ForeignKey, JSON from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.database import Base import uuid class Student(Base): __tablename__ = "students" id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) name = Column(String, nullable=False) grade = Column(Integer, nullable=False) preferred_language = Column(String, nullable=False, default="urdu") skill_mastery = Column(JSON, default=dict) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) device_id = Column(String) # Relationships learning_sessions = relationship("LearningSession", back_populates="student") student_responses = relationship("StudentResponse", back_populates="student") class Question(Base): __tablename__ = "questions" id = Column(String, primary_key=True) subject = Column(String, nullable=False) topic = Column(String, nullable=False) grade = Column(Integer, nullable=False) skill_tags = Column(JSON, default=list) difficulty_estimate = Column(Float, nullable=False) prompt = Column(JSON, nullable=False) # Multi-language prompts options = Column(JSON, default=list) solution_steps = Column(JSON, default=list) hints = Column(JSON, default=dict) answer_patterns = Column(JSON, nullable=False) explanation = Column(JSON, default=dict) content_version = Column(String, default="1.0") created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) # Relationships student_responses = relationship("StudentResponse", back_populates="question") class LearningSession(Base): __tablename__ = "learning_sessions" id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) student_id = Column(String, ForeignKey("students.id"), nullable=False) subject = Column(String, nullable=False) start_time = Column(DateTime(timezone=True), nullable=False) end_time = Column(DateTime(timezone=True)) skills_updated = Column(JSON, default=dict) synced_at = Column(DateTime(timezone=True)) device_id = Column(String) # Relationships student = relationship("Student", back_populates="learning_sessions") responses = relationship("StudentResponse", back_populates="session") class StudentResponse(Base): __tablename__ = "student_responses" id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) student_id = Column(String, ForeignKey("students.id"), nullable=False) question_id = Column(String, ForeignKey("questions.id"), nullable=False) session_id = Column(String, ForeignKey("learning_sessions.id"), nullable=False) response = Column(Text, nullable=False) response_type = Column(String, nullable=False) # 'voice' or 'text' is_correct = Column(Boolean, nullable=False) partial_credit = Column(Float, nullable=False, default=0.0) confidence = Column(Float, nullable=False, default=0.0) timestamp = Column(DateTime(timezone=True), nullable=False) synced_at = Column(DateTime(timezone=True)) # Relationships student = relationship("Student", back_populates="student_responses") question = relationship("Question", back_populates="student_responses") session = relationship("LearningSession", back_populates="responses") class Teacher(Base): __tablename__ = "teachers" id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) email = Column(String, unique=True, nullable=False) hashed_password = Column(String, nullable=False) name = Column(String, nullable=False) school = Column(String) subjects = Column(JSON, default=list) grades = Column(JSON, default=list) is_active = Column(Boolean, default=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) class ContentBundle(Base): __tablename__ = "content_bundles" id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) version = Column(String, nullable=False) grade = Column(Integer, nullable=False) subject = Column(String, nullable=False) language = Column(String, nullable=False) file_path = Column(String, nullable=False) file_size = Column(Integer, nullable=False) checksum = Column(String, nullable=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) # app/schemas/sync.py from pydantic import BaseModel from typing import List, Dict, Optional, Any from datetime import datetime class StudentResponseSchema(BaseModel): id: str student_id: str question_id: str session_id: str response: str response_type: str is_correct: bool partial_credit: float confidence: float timestamp: datetime class LearningSessionSchema(BaseModel): id: str student_id: str subject: str start_time: datetime end_time: Optional[datetime] = None skills_updated: Dict[str, float] = {} class SyncPayload(BaseModel): device_id: str sessions: List[LearningSessionSchema] responses: List[StudentResponseSchema] last_sync_time: datetime class SyncResponse(BaseModel): success: bool synced_sessions: int synced_responses: int new_content_available: bool content_version: Optional[str] = None # app/schemas/student.py from pydantic import BaseModel from typing import Dict, Optional from datetime import datetime class StudentCreate(BaseModel): name: str grade: int preferred_language: str = "urdu" class StudentResponse(BaseModel): id: str name: str grade: int preferred_language: str skill_mastery: Dict[str, float] created_at: datetime updated_at: datetime class Config: from_attributes = True # app/routers/auth.py from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy.orm import Session from datetime import datetime, timedelta from typing import Optional import jwt from passlib.context import CryptContext from app.database import get_db from app.models.database import Teacher from app.core.config import settings from pydantic import BaseModel router = APIRouter() security = HTTPBearer() pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") class LoginRequest(BaseModel): email: str password: str class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password): return pwd_context.hash(password) def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256") return encoded_jwt def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): try: payload = jwt.decode(credentials.credentials, settings.SECRET_KEY, algorithms=["HS256"]) email: str = payload.get("sub") if email is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials", headers={"WWW-Authenticate": "Bearer"}, ) return email except jwt.PyJWTError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials", headers={"WWW-Authenticate": "Bearer"}, ) @router.post("/login", response_model=TokenResponse) async def login(request: LoginRequest, db: Session = Depends(get_db)): teacher = db.query(Teacher).filter(Teacher.email == request.email).first() if not teacher or not verify_password(request.password, teacher.hashed_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", headers={"WWW-Authenticate": "Bearer"}, ) access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": teacher.email}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"} @router.get("/me") async def get_current_user(email: str = Depends(verify_token), db: Session = Depends(get_db)): teacher = db.query(Teacher).filter(Teacher.email == email).first() if not teacher: raise HTTPException(status_code=404, detail="Teacher not found") return { "id": teacher.id, "email": teacher.email, "name": teacher.name, "school": teacher.school, "subjects": teacher.subjects, "grades": teacher.grades } # app/routers/sync.py from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from sqlalchemy import and_ from datetime import datetime from typing import List from app.database import get_db from app.models.database import Student, LearningSession, StudentResponse, Question from app.schemas.sync import SyncPayload, SyncResponse from app.routers.auth import verify_token router = APIRouter() @router.post("/push", response_model=SyncResponse) async def sync_push( payload: SyncPayload, db: Session = Depends(get_db), current_user: str = Depends(verify_token) ): """ Receive and process synced data from mobile devices """ try: synced_sessions = 0 synced_responses = 0 # Process learning sessions for session_data in payload.sessions: # Check if session already exists existing_session = db.query(LearningSession).filter( LearningSession.id == session_data.id ).first() if not existing_session: # Create new session new_session = LearningSession( id=session_data.id, student_id=session_data.student_id, subject=session_data.subject, start_time=session_data.start_time, end_time=session_data.end_time, skills_updated=session_data.skills_updated, device_id=payload.device_id, synced_at=datetime.utcnow() ) db.add(new_session) synced_sessions += 1 # Process student responses for response_data in payload.responses: # Check if response already exists existing_response = db.query(StudentResponse).filter( StudentResponse.id == response_data.id ).first() if not existing_response: # Create new response new_response = StudentResponse( id=response_data.id, student_id=response_data.student_id, question_id=response_data.question_id, session_id=response_data.session_id, response=response_data.response, response_type=response_data.response_type, is_correct=response_data.is_correct, partial_credit=response_data.partial_credit, confidence=response_data.confidence, timestamp=response_data.timestamp, synced_at=datetime.utcnow() ) db.add(new_response) synced_responses += 1 # Commit all changes db.commit() # Check for new content # For now, assume no new content available new_content_available = False content_version = None return SyncResponse( success=True, synced_sessions=synced_sessions, synced_responses=synced_responses, new_content_available=new_content_available, content_version=content_version ) except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=f"Sync failed: {str(e)}") @router.get("/status/{device_id}") async def sync_status( device_id: str, db: Session = Depends(get_db) ): """ Get sync status for a device """ # Count unsynced items for this device unsynced_sessions = db.query(LearningSession).filter( and_( LearningSession.device_id == device_id, LearningSession.synced_at.is_(None) ) ).count() unsynced_responses = db.query(StudentResponse).filter( and_( StudentResponse.synced_at.is_(None) ) ).count() # Note: StudentResponse doesn't have device_id, so checking all return { "device_id": device_id, "unsynced_sessions": unsynced_sessions, "unsynced_responses": unsynced_responses, "last_sync": datetime.utcnow().isoformat() } # app/routers/content.py from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from typing import List, Optional from app.database import get_db from app.models.database import Question, ContentBundle from app.routers.auth import verify_token router = APIRouter() @router.get("/questions") async def get_questions( grade: Optional[int] = Query(None, description="Grade level"), subject: Optional[str] = Query(None, description="Subject"), language: Optional[str] = Query("urdu", description="Language"), limit: int = Query(50, description="Number of questions to return"), offset: int = Query(0, description="Offset for pagination"), db: Session = Depends(get_db) ): """ Get questions filtered by grade, subject, and language """ query = db.query(Question) if grade: query = query.filter(Question.grade == grade) if subject: query = query.filter(Question.subject == subject) questions = query.offset(offset).limit(limit).all() # Filter questions that have content in the requested language filtered_questions = [] for question in questions: if language in question.prompt: question_data = { "id": question.id, "subject": question.subject, "topic": question.topic, "grade": question.grade, "skill_tags": question.skill_tags, "difficulty_estimate": question.difficulty_estimate, "prompt": question.prompt.get(language, question.prompt.get('english', '')), "options": question.options, "solution_steps": question.solution_steps, "hints": question.hints.get(language, question.hints.get('english', [])), "answer_patterns": question.answer_patterns, "explanation": question.explanation.get(language, question.explanation.get('english', '')) } filtered_questions.append(question_data) return { "questions": filtered_questions, "total": len(filtered_questions), "grade": grade, "subject": subject, "language": language } @router.get("/bundle") async def get_content_bundle( version: Optional[str] = Query("latest", description="Content version"), grade: Optional[int] = Query(None, description="Grade level"), subject: Optional[str] = Query(None, description="Subject"), language: str = Query("urdu", description="Language"), db: Session = Depends(get_db) ): """ Get content bundle for offline use """ query = db.query(ContentBundle) if version != "latest": query = query.filter(ContentBundle.version == version) if grade: query = query.filter(ContentBundle.grade == grade) if subject: query = query.filter(ContentBundle.subject == subject) if language: query = query.filter(ContentBundle.language == language) # Get the latest version if "latest" is requested if version == "latest": bundle = query.order_by(ContentBundle.created_at.desc()).first() else: bundle = query.first() if not bundle: raise HTTPException(status_code=404, detail="Content bundle not found") return { "id": bundle.id, "version": bundle.version, "grade": bundle.grade, "subject": bundle.subject, "language": bundle.language, "file_path": bundle.file_path, "file_size": bundle.file_size, "checksum": bundle.checksum, "created_at": bundle.created_at } @router.post("/questions") async def create_question( question_data: dict, current_user: str = Depends(verify_token), db: Session = Depends(get_db) ): """ Create a new question (teacher only) """ try: new_question = Question(**question_data) db.add(new_question) db.commit() db.refresh(new_question) return {"message": "Question created successfully", "id": new_question.id} except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=f"Failed to create question: {str(e)}") # app/routers/grading.py from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from typing import Dict, Any from app.ml.grading_engine import GradingEngine router = APIRouter() class GradingRequest(BaseModel): student_response: str question_id: str answer_patterns: list response_type: str = "text" language: str = "urdu" class GradingResult(BaseModel): is_correct: bool partial_credit: float confidence: float feedback: str @router.post("/grade", response_model=GradingResult) async def grade_response(request: GradingRequest): """ Grade a student response using ML models """ try: grading_engine = GradingEngine() result = await grading_engine.grade_response( response=request.student_response, answer_patterns=request.answer_patterns, response_type=request.response_type, language=request.language ) return GradingResult( is_correct=result["is_correct"], partial_credit=result["partial_credit"], confidence=result["confidence"], feedback=result.get("feedback", "") ) except Exception as e: raise HTTPException(status_code=500, detail=f"Grading failed: {str(e)}") # app/routers/analytics.py from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import func, and_ from datetime import datetime, timedelta from typing import Optional, List from app.database import get_db from app.models.database import Student, StudentResponse, LearningSession, Question from app.routers.auth import verify_token router = APIRouter() @router.get("/student/{student_id}/progress") async def get_student_progress( student_id: str, days: int = Query(30, description="Number of days to analyze"), db: Session = Depends(get_db), current_user: str = Depends(verify_token) ): """ Get detailed progress analytics for a student """ # Get student info student = db.query(Student).filter(Student.id == student_id).first() if not student: raise HTTPException(status_code=404, detail="Student not found") # Date range end_date = datetime.utcnow() start_date = end_date - timedelta(days=days) # Get responses in date range responses = db.query(StudentResponse).filter( and_( StudentResponse.student_id == student_id, StudentResponse.timestamp >= start_date, StudentResponse.timestamp <= end_date ) ).all() # Calculate metrics total_responses = len(responses) correct_responses = sum(1 for r in responses if r.is_correct) accuracy = correct_responses / total_responses if total_responses > 0 else 0 # Subject-wise breakdown subject_stats = {} for response in responses: question = db.query(Question).filter(Question.id == response.question_id).first() if question: subject = question.subject if subject not in subject_stats: subject_stats[subject] = {"total": 0, "correct": 0, "partial_credit": 0} subject_stats[subject]["total"] += 1 if response.is_correct: subject_stats[subject]["correct"] += 1 subject_stats[subject]["partial_credit"] += response.partial_credit # Calculate subject accuracies for subject in subject_stats: stats = subject_stats[subject] stats["accuracy"] = stats["correct"] / stats["total"] if stats["total"] > 0 else 0 stats["avg_partial_credit"] = stats["partial_credit"] / stats["total"] if stats["total"] > 0 else 0 # Skill mastery trends skill_mastery = student.skill_mastery or {} # Recent sessions recent_sessions = db.query(LearningSession).filter( and_( LearningSession.student_id == student_id, LearningSession.start_time >= start_date ) ).order_by(LearningSession.start_time.desc()).limit(10).all() session_data = [] for session in recent_sessions: session_responses = [r for r in responses if r.session_id == session.id] session_accuracy = sum(1 for r in session_responses if r.is_correct) / len(session_responses) if session_responses else 0 session_data.append({ "id": session.id, "subject": session.subject, "start_time": session.start_time, "end_time": session.end_time, "responses": len(session_responses), "accuracy": session_accuracy }) return { "student": { "id": student.id, "name": student.name, "grade": student.grade, "preferred_language": student.preferred_language }, "period": { "days": days, "start_date": start_date, "end_date": end_date }, "overall": { "total_responses": total_responses, "correct_responses": correct_responses, "accuracy": accuracy, "sessions": len(recent_sessions) }, "by_subject": subject_stats, "skill_mastery": skill_mastery, "recent_sessions": session_data } @router.get("/class/overview") async def get_class_overview( grade: Optional[int] = Query(None, description="Grade to filter"), days: int = Query(7, description="Number of days to analyze"), db: Session = Depends(get_db), current_user: str = Depends(verify_token) ): """ Get class-level analytics overview """ # Date range end_date = datetime.utcnow() start_date = end_date - timedelta(days=days) # Get students students_query = db.query(Student) if grade: students_query = students_query.filter(Student.grade == grade) students = students_query.all() # Get recent responses for these students student_ids = [s.id for s in students] responses = db.query(StudentResponse).filter( and_( StudentResponse.student_id.in_(student_ids), StudentResponse.timestamp >= start_date ) ).all() # Overall statistics total_students = len(students) active_students = len(set(r.student_id for r in responses)) total_responses = len(responses) correct_responses = sum(1 for r in responses if r.is_correct) overall_accuracy = correct_responses / total_responses if total_responses > 0 else 0 # Student performance breakdown student_performance = [] for student in students: student_responses = [r for r in responses if r.student_id == student.id] if student_responses: accuracy = sum(1 for r in student_responses if r.is_correct) / len(student_responses) student_performance.appen