Update app/main.py
Browse files- app/main.py +166 -0
app/main.py
CHANGED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/main.py
|
| 2 |
+
from fastapi import FastAPI, Depends, HTTPException, status
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from fastapi.responses import FileResponse
|
| 5 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 6 |
+
from sqlalchemy.orm import Session
|
| 7 |
+
from app.db.database import Base, engine, SessionLocal
|
| 8 |
+
from app.db import crud
|
| 9 |
+
from app.models.request_models import GenerateRequest, ChatRequest, AuthRequest
|
| 10 |
+
from app.services.orchestrator.build_question_set import build_question_set
|
| 11 |
+
from app.services.scheduler import start_scheduler
|
| 12 |
+
from app.services.chatbot_service import ask_chatbot
|
| 13 |
+
from app.utils import auth as auth_utils
|
| 14 |
+
from app.db import models as dbmodels
|
| 15 |
+
import json, datetime, os
|
| 16 |
+
|
| 17 |
+
# create DB tables
|
| 18 |
+
Base.metadata.create_all(bind=engine)
|
| 19 |
+
|
| 20 |
+
app = FastAPI(title="GATE Chemical Engineering Multi-Model")
|
| 21 |
+
|
| 22 |
+
# serve frontend static
|
| 23 |
+
app.mount("/static", StaticFiles(directory="frontend"), name="static")
|
| 24 |
+
|
| 25 |
+
def get_db():
|
| 26 |
+
db = SessionLocal()
|
| 27 |
+
try:
|
| 28 |
+
yield db
|
| 29 |
+
finally:
|
| 30 |
+
db.close()
|
| 31 |
+
|
| 32 |
+
bearer = HTTPBearer()
|
| 33 |
+
|
| 34 |
+
def get_current_user(creds: HTTPAuthorizationCredentials = Depends(bearer), db: Session = Depends(get_db)):
|
| 35 |
+
token = creds.credentials
|
| 36 |
+
payload = auth_utils.decode_access_token(token)
|
| 37 |
+
if not payload:
|
| 38 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
| 39 |
+
username = payload.get("username")
|
| 40 |
+
uid = payload.get("user_id")
|
| 41 |
+
user = crud.get_user_by_username(db, username)
|
| 42 |
+
if not user or user.id != uid:
|
| 43 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
| 44 |
+
return user
|
| 45 |
+
|
| 46 |
+
@app.get("/")
|
| 47 |
+
def ui():
|
| 48 |
+
return FileResponse("frontend/index.html")
|
| 49 |
+
|
| 50 |
+
# Auth
|
| 51 |
+
@app.post("/register")
|
| 52 |
+
def register(payload: dict, db: Session = Depends(get_db)):
|
| 53 |
+
username = payload.get("username")
|
| 54 |
+
email = payload.get("email")
|
| 55 |
+
password = payload.get("password")
|
| 56 |
+
if not username or not email or not password:
|
| 57 |
+
raise HTTPException(status_code=400, detail="username/email/password required")
|
| 58 |
+
if crud.get_user_by_username(db, username):
|
| 59 |
+
raise HTTPException(status_code=400, detail="username taken")
|
| 60 |
+
if crud.get_user_by_email(db, email):
|
| 61 |
+
raise HTTPException(status_code=400, detail="email taken")
|
| 62 |
+
user = crud.create_user(db, username, email, password)
|
| 63 |
+
token = auth_utils.create_access_token({"user_id": user.id, "username": user.username})
|
| 64 |
+
return {"access_token": token, "token_type": "bearer", "user": {"id": user.id, "username": user.username}}
|
| 65 |
+
|
| 66 |
+
@app.post("/login")
|
| 67 |
+
def login(payload: dict, db: Session = Depends(get_db)):
|
| 68 |
+
username = payload.get("username")
|
| 69 |
+
password = payload.get("password")
|
| 70 |
+
if not username or not password:
|
| 71 |
+
raise HTTPException(status_code=400, detail="username/password required")
|
| 72 |
+
user = crud.get_user_by_username(db, username)
|
| 73 |
+
if not user or not crud.verify_password(password, user.hashed_password):
|
| 74 |
+
raise HTTPException(status_code=401, detail="invalid credentials")
|
| 75 |
+
token = auth_utils.create_access_token({"user_id": user.id, "username": user.username})
|
| 76 |
+
return {"access_token": token, "token_type": "bearer", "user": {"id": user.id, "username": user.username}}
|
| 77 |
+
|
| 78 |
+
# Generate questions (multi-model orchestrator)
|
| 79 |
+
@app.post("/generate")
|
| 80 |
+
async def generate(req: GenerateRequest):
|
| 81 |
+
topic = getattr(req, "topic", "General")
|
| 82 |
+
num = getattr(req, "num_questions", 10)
|
| 83 |
+
qset = await build_question_set(topic=topic, num=num)
|
| 84 |
+
return qset
|
| 85 |
+
|
| 86 |
+
@app.get("/daily")
|
| 87 |
+
def get_daily():
|
| 88 |
+
path = "app/data/daily_questions.json"
|
| 89 |
+
if os.path.exists(path):
|
| 90 |
+
with open(path, "r") as f:
|
| 91 |
+
return json.load(f)
|
| 92 |
+
return {"error": "Daily not ready"}
|
| 93 |
+
|
| 94 |
+
# Save quiz (auth required)
|
| 95 |
+
@app.post("/save-quiz")
|
| 96 |
+
def save_quiz(payload: dict, user: dbmodels.User = Depends(get_current_user), db: Session = Depends(get_db)):
|
| 97 |
+
attempt_id = crud.save_quiz_attempt(db, user.id, payload)
|
| 98 |
+
return {"status": "saved", "attempt_id": attempt_id}
|
| 99 |
+
|
| 100 |
+
@app.get("/attempts")
|
| 101 |
+
def list_attempts(subject: str = None, start: str = None, end: str = None, db: Session = Depends(get_db), user: dbmodels.User = Depends(get_current_user)):
|
| 102 |
+
s, e = None, None
|
| 103 |
+
if start:
|
| 104 |
+
s = datetime.datetime.fromisoformat(start)
|
| 105 |
+
if end:
|
| 106 |
+
e = datetime.datetime.fromisoformat(end)
|
| 107 |
+
rows = crud.get_user_attempts(db, user.id, subject=subject, start=s, end=e)
|
| 108 |
+
out = []
|
| 109 |
+
for r in rows:
|
| 110 |
+
out.append({
|
| 111 |
+
"id": r.id,
|
| 112 |
+
"subject": r.subject,
|
| 113 |
+
"topic": r.topic,
|
| 114 |
+
"total_questions": r.total_questions,
|
| 115 |
+
"correct_answers": r.correct_answers,
|
| 116 |
+
"percentage": r.percentage,
|
| 117 |
+
"timestamp": r.timestamp.isoformat()
|
| 118 |
+
})
|
| 119 |
+
return out
|
| 120 |
+
|
| 121 |
+
@app.get("/attempts/{attempt_id}")
|
| 122 |
+
def attempt_details(attempt_id: int, db: Session = Depends(get_db), user: dbmodels.User = Depends(get_current_user)):
|
| 123 |
+
att = db.query(dbmodels.QuizAttempt).filter(dbmodels.QuizAttempt.id == attempt_id, dbmodels.QuizAttempt.user_id == user.id).first()
|
| 124 |
+
if not att:
|
| 125 |
+
raise HTTPException(status_code=404, detail="Attempt not found")
|
| 126 |
+
answers = crud.get_attempt_details(db, attempt_id)
|
| 127 |
+
out = []
|
| 128 |
+
for a in answers:
|
| 129 |
+
out.append({
|
| 130 |
+
"question_number": a.question_number,
|
| 131 |
+
"question_text": a.question_text,
|
| 132 |
+
"user_answer": a.user_answer,
|
| 133 |
+
"correct_answer": a.correct_answer,
|
| 134 |
+
"explanation": a.explanation,
|
| 135 |
+
"detailed_solution": a.detailed_solution,
|
| 136 |
+
"is_correct": bool(a.is_correct)
|
| 137 |
+
})
|
| 138 |
+
return {"attempt": {
|
| 139 |
+
"id": att.id,
|
| 140 |
+
"subject": att.subject,
|
| 141 |
+
"topic": att.topic,
|
| 142 |
+
"total_questions": att.total_questions,
|
| 143 |
+
"correct_answers": att.correct_answers,
|
| 144 |
+
"percentage": att.percentage,
|
| 145 |
+
"timestamp": att.timestamp.isoformat()
|
| 146 |
+
}, "answers": out}
|
| 147 |
+
|
| 148 |
+
# Analytics
|
| 149 |
+
@app.get("/analytics/score-trend")
|
| 150 |
+
def score_trend(subject: str = None, limit: int = 100, db: Session = Depends(get_db), user: dbmodels.User = Depends(get_current_user)):
|
| 151 |
+
rows = crud.get_score_trend(db, user.id, subject=subject, limit=limit)
|
| 152 |
+
return [{"timestamp": r.timestamp.isoformat(), "percentage": r.percentage, "topic": r.topic} for r in rows]
|
| 153 |
+
|
| 154 |
+
# Chatbot
|
| 155 |
+
@app.post("/chat")
|
| 156 |
+
async def chat(req: ChatRequest):
|
| 157 |
+
ans = await ask_chatbot(req.question)
|
| 158 |
+
return {"answer": ans}
|
| 159 |
+
|
| 160 |
+
# Start scheduler on startup (best-effort)
|
| 161 |
+
@app.on_event("startup")
|
| 162 |
+
def startup_event():
|
| 163 |
+
try:
|
| 164 |
+
start_scheduler()
|
| 165 |
+
except Exception:
|
| 166 |
+
pass
|