Spaces:
Running
Running
| """ | |
| main.py β FastAPI entry point for MathMinds AI | |
| """ | |
| import os | |
| import sys | |
| import asyncio | |
| import logging | |
| import uuid | |
| import time | |
| from contextlib import asynccontextmanager | |
| from datetime import datetime, timezone | |
| from typing import Any, Dict, List, Optional | |
| # Windows async fix | |
| if sys.platform == "win32": | |
| asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) | |
| os.environ["DISABLE_MODEL_SOURCE_CHECK"] = "True" | |
| from fastapi import FastAPI, HTTPException, status, Depends, Request | |
| from fastapi.responses import JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from slowapi import _rate_limit_exceeded_handler | |
| from slowapi.errors import RateLimitExceeded | |
| from app.core.limiter import limiter | |
| from app.core.orchestrator import Orchestrator | |
| from app.core.schemas import ( | |
| SolveRequest, | |
| ChatSession, | |
| Message, | |
| SessionRename, | |
| ) | |
| from app.core.logging_config import configure_logging | |
| from app.core.settings import settings | |
| from app.core.errors import AppError, ErrorCodes | |
| from app.api.deps import ( | |
| get_orchestrator, | |
| get_redis_pool, | |
| get_mongo_client, | |
| get_db_manager, | |
| get_redis_client, | |
| ) | |
| from app.core.security import get_current_user | |
| # Logging | |
| configure_logging() | |
| logger = logging.getLogger(__name__) | |
| MAX_IMAGE_SIZE = 5 * 1024 * 1024 # 5MB limit | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LIFESPAN | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def lifespan(app: FastAPI): | |
| logger.info("Starting MathMinds AI") | |
| try: | |
| get_redis_pool() | |
| get_mongo_client() | |
| get_orchestrator() | |
| logger.info("Startup complete") | |
| except Exception as e: | |
| logger.critical(f"Startup failure: {e}") | |
| yield | |
| logger.info("Shutting down MathMinds") | |
| try: | |
| from app.api.deps import close_redis, close_mongo | |
| close_redis() | |
| close_mongo() | |
| logger.info("Shutdown complete") | |
| except Exception as e: | |
| logger.error(f"Shutdown error: {e}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FASTAPI APP | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app = FastAPI( | |
| title="MathMinds AI API", | |
| description="AI-powered math solver API", | |
| version="1.0.0", | |
| lifespan=lifespan, | |
| ) | |
| # Rate limiter | |
| app.state.limiter = limiter | |
| app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) | |
| # CORS | |
| allowed_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=allowed_origins, | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MIDDLEWARE | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def request_id_middleware(request: Request, call_next): | |
| request_id = str(uuid.uuid4()) | |
| request.state.request_id = request_id | |
| start_time = time.time() | |
| logger.info( | |
| "Request started", | |
| extra={ | |
| "request_id": request_id, | |
| "path": request.url.path, | |
| "method": request.method, | |
| }, | |
| ) | |
| response = await call_next(request) | |
| duration = time.time() - start_time | |
| response.headers["X-Request-ID"] = request_id | |
| logger.info( | |
| "Request finished", | |
| extra={ | |
| "request_id": request_id, | |
| "status_code": response.status_code, | |
| "duration": duration, | |
| }, | |
| ) | |
| return response | |
| async def timeout_middleware(request: Request, call_next): | |
| try: | |
| return await asyncio.wait_for(call_next(request), timeout=120) | |
| except asyncio.TimeoutError: | |
| logger.error(f"Timeout: {request.url.path}") | |
| return JSONResponse( | |
| status_code=504, | |
| content={"detail": "Request timed out"}, | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # EXCEPTION HANDLERS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def global_exception_handler(request: Request, exc: Exception): | |
| request_id = getattr(request.state, "request_id", "unknown") | |
| logger.error( | |
| f"[{request_id}] Unhandled error: {exc}", | |
| exc_info=True | |
| ) | |
| return JSONResponse( | |
| status_code=500, | |
| content={ | |
| "status": "error", | |
| "error": "Internal Server Error", | |
| "metadata": { | |
| "request_id": request_id, | |
| "timestamp": datetime.now(timezone.utc).isoformat(), | |
| }, | |
| }, | |
| ) | |
| async def app_error_handler(request: Request, exc: AppError): | |
| mapping = { | |
| ErrorCodes.INPUT_VALIDATION_ERROR: 400, | |
| ErrorCodes.RESOURCE_NOT_FOUND: 404, | |
| ErrorCodes.RATE_LIMIT_EXCEEDED: 429, | |
| ErrorCodes.DEPENDENCY_ERROR: 503, | |
| ErrorCodes.GEMINI_ERROR: 503, | |
| } | |
| status_code = mapping.get(exc.code, 500) | |
| request_id = getattr(request.state, "request_id", "unknown") | |
| return JSONResponse( | |
| status_code=status_code, | |
| content={ | |
| "status": "error", | |
| "error": exc.message, | |
| "error_code": exc.code, | |
| "metadata": { | |
| "request_id": request_id, | |
| "timestamp": datetime.now(timezone.utc).isoformat(), | |
| }, | |
| }, | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # GENERAL ROUTES | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def root(): | |
| return {"message": "MathMinds API running"} | |
| async def version(): | |
| return { | |
| "version": "1.0.0", | |
| "build": os.getenv("BUILD_ID"), | |
| "commit": os.getenv("GIT_SHA"), | |
| } | |
| async def health(): | |
| health: Dict[str, Any] = { | |
| "status": "healthy", | |
| "timestamp": datetime.now(timezone.utc).isoformat(), | |
| "services": {}, | |
| } | |
| try: | |
| # 2s timeout for Redis | |
| ping_task = asyncio.to_thread(get_redis_client().ping) | |
| await asyncio.wait_for(ping_task, timeout=2.0) | |
| health["services"]["redis"] = "healthy" | |
| except Exception as e: | |
| health["services"]["redis"] = str(e) | |
| health["status"] = "degraded" | |
| try: | |
| # 2s timeout for Mongo | |
| mongo_ping = asyncio.to_thread(get_mongo_client().admin.command, "ping") | |
| await asyncio.wait_for(mongo_ping, timeout=2.0) | |
| health["services"]["mongodb"] = "healthy" | |
| except Exception as e: | |
| health["services"]["mongodb"] = str(e) | |
| health["status"] = "degraded" | |
| return health | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SOLVE ROUTE | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def solve_problem( | |
| request: Request, | |
| solve_req: SolveRequest, | |
| orchestrator: Orchestrator = Depends(get_orchestrator), | |
| current_user: dict = Depends(get_current_user), | |
| ): | |
| """ | |
| Solve a math problem and return the result. | |
| """ | |
| request_id = getattr(request.state, "request_id", str(uuid.uuid4())) | |
| if not solve_req.effective_text and not solve_req.image: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Either text or image must be provided", | |
| ) | |
| if solve_req.image and len(solve_req.image) > MAX_IMAGE_SIZE: | |
| raise HTTPException( | |
| status_code=413, | |
| detail="Image too large", | |
| ) | |
| logger.info( | |
| "Solve request received", | |
| extra={ | |
| "request_id": request_id, | |
| "user_id": current_user["uid"], | |
| "session_id": solve_req.session_id, | |
| }, | |
| ) | |
| try: | |
| result = await orchestrator.solve_problem( | |
| query=solve_req.effective_text, | |
| image=solve_req.image, | |
| user_id=current_user["uid"], | |
| session_id=solve_req.session_id, | |
| request_id=request_id, | |
| ) | |
| return JSONResponse(status_code=200, content=result) | |
| except Exception as e: | |
| logger.error(f"Solve error: {e}") | |
| raise HTTPException( | |
| status_code=500, | |
| detail="Internal processing error", | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ANALYZE ROUTE | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def analyze_problem( | |
| request: Request, | |
| solve_req: SolveRequest, | |
| orchestrator: Orchestrator = Depends(get_orchestrator), | |
| current_user: dict = Depends(get_current_user), | |
| ): | |
| """Analyze user's work and grade it.""" | |
| request_id = getattr(request.state, "request_id", str(uuid.uuid4())) | |
| if not solve_req.image: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="An image of the user's work must be provided for analysis.", | |
| ) | |
| if len(solve_req.image) > MAX_IMAGE_SIZE: | |
| raise HTTPException(status_code=413, detail="Image too large") | |
| logger.info("Analyze request received", extra={"request_id": request_id, "user_id": current_user["uid"]}) | |
| try: | |
| result = await orchestrator.solve_problem( | |
| query=solve_req.effective_text, | |
| image=solve_req.image, | |
| user_id=current_user["uid"], | |
| session_id=solve_req.session_id, | |
| request_id=request_id, | |
| mode="analyzer" | |
| ) | |
| return JSONResponse(status_code=200, content=result) | |
| except Exception as e: | |
| logger.error(f"Analyze error: {e}") | |
| raise HTTPException(status_code=500, detail="Internal processing error") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TUTOR ROUTE | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def tutor_problem( | |
| request: Request, | |
| solve_req: SolveRequest, | |
| orchestrator: Orchestrator = Depends(get_orchestrator), | |
| current_user: dict = Depends(get_current_user), | |
| ): | |
| """Socratic tutoring session endpoint.""" | |
| request_id = getattr(request.state, "request_id", str(uuid.uuid4())) | |
| if not solve_req.effective_text and not solve_req.image: | |
| raise HTTPException(status_code=400, detail="Either text or image must be provided") | |
| if solve_req.image and len(solve_req.image) > MAX_IMAGE_SIZE: | |
| raise HTTPException(status_code=413, detail="Image too large") | |
| logger.info("Tutor request received", extra={"request_id": request_id, "user_id": current_user["uid"]}) | |
| try: | |
| result = await orchestrator.solve_problem( | |
| query=solve_req.effective_text, | |
| image=solve_req.image, | |
| user_id=current_user["uid"], | |
| session_id=solve_req.session_id, | |
| request_id=request_id, | |
| mode="tutor" | |
| ) | |
| return JSONResponse(status_code=200, content=result) | |
| except Exception as e: | |
| logger.error(f"Tutor error: {e}") | |
| raise HTTPException(status_code=500, detail="Internal processing error") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CHAT ROUTES | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def list_sessions( | |
| current_user: dict = Depends(get_current_user), | |
| db_manager=Depends(get_db_manager), | |
| ): | |
| return db_manager.list_sessions(current_user["uid"]) | |
| async def create_session( | |
| current_user: dict = Depends(get_current_user), | |
| db_manager=Depends(get_db_manager), | |
| ): | |
| session_id = str(uuid.uuid4()) | |
| title = "New Chat" | |
| if db_manager.create_session(current_user["uid"], session_id, title): | |
| return { | |
| "session_id": session_id, | |
| "title": title, | |
| "created_at": datetime.now(timezone.utc), | |
| } | |
| raise HTTPException(500, "Failed to create session") | |
| async def get_messages( | |
| session_id: str, | |
| current_user: dict = Depends(get_current_user), | |
| db_manager=Depends(get_db_manager), | |
| ): | |
| history = db_manager.get_chat_history(current_user["uid"], session_id) | |
| if history is None: | |
| raise HTTPException(404, "Session not found") | |
| return history | |
| async def rename_session( | |
| session_id: str, | |
| rename_data: SessionRename, | |
| current_user: dict = Depends(get_current_user), | |
| db_manager=Depends(get_db_manager), | |
| ): | |
| if db_manager.rename_session( | |
| current_user["uid"], | |
| session_id, | |
| rename_data.title, | |
| ): | |
| return {"status": "success"} | |
| raise HTTPException(404, "Session not found") | |
| async def delete_session( | |
| session_id: str, | |
| current_user: dict = Depends(get_current_user), | |
| db_manager=Depends(get_db_manager), | |
| ): | |
| if db_manager.delete_session(current_user["uid"], session_id): | |
| return {"status": "success"} | |
| raise HTTPException(404, "Session not found") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ENTRY POINT | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| import uvicorn | |
| port = int(os.environ.get("PORT", 8080)) | |
| uvicorn.run( | |
| app, | |
| host="0.0.0.0", | |
| port=port, | |
| ) |