""" SlideMentor Backend — FastAPI + Groq ===================================== Endpoints: POST /explain-slide → Explain slide text or image POST /explain-selection → Explain highlighted text POST /chat → Contextual follow-up chat Security: - Input sanitization and prompt injection defense - Rate limiting (in-memory, suitable for demo scale) - CORS locked to extension origin - No raw user content in system prompts """ from fastapi import FastAPI, Request, HTTPException, Depends from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from contextlib import asynccontextmanager from routers.pdf import router as pdf_router import time import logging from dotenv import load_dotenv load_dotenv() from routers.explain import router as explain_router from routers.chat import router as chat_router from middleware.rate_limiter import RateLimiter from middleware.sanitizer import sanitize_input # ─── Logging ──────────────────────────────────────────────────────────────── logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", ) logger = logging.getLogger("slidementor") # ─── Lifespan ──────────────────────────────────────────────────────────────── @asynccontextmanager async def lifespan(app: FastAPI): logger.info("SlideMentor API starting up") yield logger.info("SlideMentor API shutting down") # ─── App ───────────────────────────────────────────────────────────────────── app = FastAPI( title="SlideMentor API", version="1.0.0", description="AI-powered slide explanation backend using Groq LLaMA 3", docs_url="/docs", redoc_url=None, lifespan=lifespan, ) # ─── CORS ───────────────────────────────────────────────────────────────────── app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) # ─── Rate Limiter ───────────────────────────────────────────────────────────── rate_limiter = RateLimiter(max_requests=30, window_seconds=60) @app.middleware("http") async def rate_limit_middleware(request: Request, call_next): if request.method == "POST": client_ip = request.client.host if request.client else "unknown" allowed, retry_after = rate_limiter.check(client_ip) if not allowed: return JSONResponse( status_code=429, content={ "error": "rate_limit_exceeded", "detail": f"Too many requests. Retry after {retry_after}s.", "retry_after": retry_after, }, ) return await call_next(request) # ─── Routers ───────────────────────────────────────────────────────────────── app.include_router(explain_router, prefix="", tags=["Explain"]) app.include_router(chat_router, prefix="", tags=["Chat"]) app.include_router(pdf_router, prefix="", tags=["PDF"]) # ─── Health ─────────────────────────────────────────────────────────────────── @app.get("/health") async def health(): return {"status": "ok", "service": "slidementor", "version": "1.0.0"} # ─── Global Error Handler ───────────────────────────────────────────────────── @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): logger.error(f"Unhandled error on {request.url.path}: {exc}", exc_info=True) return JSONResponse( status_code=500, content={"error": "internal_error", "detail": "An unexpected error occurred."}, )