from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from ..database import get_db from ..models import Student, Syllabus, TestPlan, TestPaper, TestAttempt, AnalysisReport, TokenUsage from ..services import ai_service from ..services.graph_service import generate_all_graphs import uuid router = APIRouter(prefix="/api/analysis", tags=["Analysis"]) def gen_id(): return str(uuid.uuid4()) async def _gather_attempts(student_id: str, syllabus_id: str, db: AsyncSession) -> list[dict]: plan_result = await db.execute(select(TestPlan).where(TestPlan.syllabus_id == syllabus_id)) plan = plan_result.scalar_one_or_none() if not plan: return [] papers_result = await db.execute(select(TestPaper).where(TestPaper.test_plan_id == plan.id)) paper_ids = {p.id: p for p in papers_result.scalars().all()} attempts_result = await db.execute( select(TestAttempt).where( TestAttempt.student_id == student_id, TestAttempt.test_paper_id.in_(list(paper_ids.keys())) ).order_by(TestAttempt.attempted_at) ) attempts = attempts_result.scalars().all() return [ { "test_title": paper_ids[a.test_paper_id].title, "percentage": a.percentage, "score": a.score, "total_marks": a.total_marks, "is_targeted": paper_ids[a.test_paper_id].is_targeted, "evaluation": a.evaluation, "attempted_at": a.attempted_at.isoformat(), } for a in attempts ] @router.post("/generate/{student_id}/{syllabus_id}") async def generate_analysis(student_id: str, syllabus_id: str, db: AsyncSession = Depends(get_db)): student_result = await db.execute(select(Student).where(Student.id == student_id)) student = student_result.scalar_one_or_none() if not student: raise HTTPException(404, "Student not found") syllabus_result = await db.execute(select(Syllabus).where(Syllabus.id == syllabus_id)) syllabus = syllabus_result.scalar_one_or_none() if not syllabus: raise HTTPException(404, "Syllabus not found") attempts = await _gather_attempts(student_id, syllabus_id, db) if not attempts: raise HTTPException(400, "No test attempts found for this student/syllabus combination") existing_result = await db.execute( select(AnalysisReport).where( AnalysisReport.student_id == student_id, AnalysisReport.syllabus_id == syllabus_id ).order_by(AnalysisReport.version.desc()) ) existing = existing_result.scalars().first() prev_report = None if existing: prev_report = { "version": existing.version, "overall_score": existing.overall_score, "weaknesses": existing.weaknesses, "strengths": existing.strengths, } analysis, usage = await ai_service.analyze_performance( student_name=student.name, subject=syllabus.subject, grade=syllabus.grade, syllabus_topics=syllabus.topics, all_attempts=attempts, previous_report=prev_report, ) version = (existing.version + 1) if existing else 1 report = AnalysisReport( id=gen_id(), student_id=student_id, syllabus_id=syllabus_id, topic_performance=analysis["topic_performance"], overall_score=analysis["overall_score"], strengths=analysis["strengths"], weaknesses=analysis["weaknesses"], recommendations=analysis["recommendations"], narrative=analysis["narrative"], version=version, tests_analyzed=len(attempts), ) db.add(report) db.add(TokenUsage( id=gen_id(), event_type="performance_analysis", student_id=student_id, student_name=student.name, syllabus_id=syllabus_id, subject=syllabus.subject, grade=syllabus.grade, input_tokens=usage["input_tokens"], output_tokens=usage["output_tokens"], total_tokens=usage["total_tokens"], model=usage["model"], extra_info={"tests_analyzed": len(attempts), "report_version": version}, )) await db.commit() return { "report_id": report.id, "version": version, "overall_score": analysis["overall_score"], "topic_performance": analysis["topic_performance"], "strengths": analysis["strengths"], "weaknesses": analysis["weaknesses"], "recommendations": analysis["recommendations"], "narrative": analysis["narrative"], "tests_analyzed": len(attempts), } @router.get("/{student_id}/{syllabus_id}") async def get_analysis(student_id: str, syllabus_id: str, db: AsyncSession = Depends(get_db)): result = await db.execute( select(AnalysisReport).where( AnalysisReport.student_id == student_id, AnalysisReport.syllabus_id == syllabus_id ).order_by(AnalysisReport.version.desc()) ) report = result.scalars().first() if not report: raise HTTPException(404, "No analysis report found. Generate one first.") return { "report_id": report.id, "version": report.version, "overall_score": report.overall_score, "topic_performance": report.topic_performance, "strengths": report.strengths, "weaknesses": report.weaknesses, "recommendations": report.recommendations, "narrative": report.narrative, "tests_analyzed": report.tests_analyzed, "updated_at": report.updated_at.isoformat(), } @router.get("/graphs/{student_id}/{syllabus_id}") async def get_graphs(student_id: str, syllabus_id: str, db: AsyncSession = Depends(get_db)): report_result = await db.execute( select(AnalysisReport).where( AnalysisReport.student_id == student_id, AnalysisReport.syllabus_id == syllabus_id ).order_by(AnalysisReport.version.desc()) ) report = report_result.scalars().first() if not report: raise HTTPException(404, "Generate analysis report first") attempts = await _gather_attempts(student_id, syllabus_id, db) graphs = generate_all_graphs(report.topic_performance, attempts) return { "student_id": student_id, "syllabus_id": syllabus_id, "graphs": graphs, } @router.post("/targeted-test/{student_id}/{syllabus_id}") async def generate_targeted_test(student_id: str, syllabus_id: str, db: AsyncSession = Depends(get_db)): student_result = await db.execute(select(Student).where(Student.id == student_id)) student = student_result.scalar_one_or_none() if not student: raise HTTPException(404, "Student not found") syllabus_result = await db.execute(select(Syllabus).where(Syllabus.id == syllabus_id)) syllabus = syllabus_result.scalar_one_or_none() if not syllabus: raise HTTPException(404, "Syllabus not found") report_result = await db.execute( select(AnalysisReport).where( AnalysisReport.student_id == student_id, AnalysisReport.syllabus_id == syllabus_id ).order_by(AnalysisReport.version.desc()) ) report = report_result.scalars().first() if not report: raise HTTPException(400, "Generate analysis report first before creating targeted tests") weak_topics = [] weak_subtopics = [] for tp in report.topic_performance: if tp.get("overall_score") is not None and tp["overall_score"] < 60: weak_topics.append(tp["topic"]) for st in tp.get("subtopic_scores", []): if st.get("score") is not None and st["score"] < 60: weak_subtopics.append(st["name"]) if not weak_topics and not weak_subtopics: weak_topics = [tp["topic"] for tp in report.topic_performance[:2]] paper_data, usage = await ai_service.generate_targeted_test( subject=syllabus.subject, grade=syllabus.grade, student_name=student.name, weak_topics=weak_topics, weak_subtopics=weak_subtopics, topic_details=syllabus.topics, ) plan_result = await db.execute(select(TestPlan).where(TestPlan.syllabus_id == syllabus_id)) plan = plan_result.scalar_one_or_none() paper = TestPaper( id=gen_id(), test_plan_id=plan.id, sequence_number=0, title=paper_data["title"], questions=paper_data["questions"], total_marks=paper_data["total_marks"], duration_minutes=paper_data["duration_minutes"], topics_covered=paper_data["topics_covered"], is_targeted=True, target_student_id=student_id, ) db.add(paper) db.add(TokenUsage( id=gen_id(), event_type="targeted_test_generation", student_id=student_id, student_name=student.name, syllabus_id=syllabus_id, subject=syllabus.subject, grade=syllabus.grade, input_tokens=usage["input_tokens"], output_tokens=usage["output_tokens"], total_tokens=usage["total_tokens"], model=usage["model"], extra_info={"weak_topics": weak_topics, "question_count": len(paper_data["questions"])}, )) await db.commit() return { "test_paper_id": paper.id, "title": paper.title, "total_marks": paper.total_marks, "duration_minutes": paper.duration_minutes, "topics_covered": paper.topics_covered, "focused_weak_areas": weak_topics + weak_subtopics, "questions_count": len(paper.questions), }