Spaces:
Runtime error
Runtime error
| """ | |
| DermIQ API Server | |
| FastAPI backend β port binds instantly, engine loads in background thread | |
| """ | |
| import sys | |
| import os | |
| import logging | |
| import threading | |
| from contextlib import asynccontextmanager | |
| from pathlib import Path | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse, JSONResponse | |
| from pydantic import BaseModel, Field, field_validator | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| from rag_engine import DermIQEngine | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s | %(levelname)s | %(message)s", | |
| ) | |
| logger = logging.getLogger("dermiq-api") | |
| # ββ Singleton engine ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| engine = DermIQEngine() | |
| def _init_engine_background(): | |
| """ | |
| Runs in a daemon thread so uvicorn binds the port immediately. | |
| Queries return 503 until this finishes β frontend shows Initializing status. | |
| """ | |
| logger.info("Background thread: starting engine initialization...") | |
| try: | |
| engine.initialize(force_rebuild=False) | |
| logger.info("Background thread: engine ready") | |
| except Exception as exc: | |
| logger.error(f"Background thread: engine init failed β {exc}", exc_info=True) | |
| # ββ Lifespan ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def lifespan(app: FastAPI): | |
| logger.info("DermIQ server starting β launching engine in background thread") | |
| t = threading.Thread(target=_init_engine_background, daemon=True) | |
| t.start() | |
| yield | |
| logger.info("DermIQ server shutting down") | |
| # ββ App βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app = FastAPI( | |
| title="DermIQ β Skin Disease RAG API", | |
| description="RAG API for skin disease classification. LangGraph + Cohere + FAISS.", | |
| version="1.0.0", | |
| lifespan=lifespan, | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ββ Frontend ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| frontend_path = Path(__file__).parent / "frontend" | |
| if frontend_path.exists(): | |
| app.mount("/static", StaticFiles(directory=str(frontend_path)), name="static") | |
| logger.info(f"Frontend mounted from: {frontend_path}") | |
| # ββ Pydantic models βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class QueryRequest(BaseModel): | |
| question: str = Field(..., min_length=3, max_length=1000) | |
| def sanitize_question(cls, v: str) -> str: | |
| return v.strip() | |
| class QueryResponse(BaseModel): | |
| question: str | |
| answer: str | |
| sources: list[str] | |
| confidence: str | |
| processing_time: float | |
| docs_retrieved: int | |
| status: str = "success" | |
| class RebuildRequest(BaseModel): | |
| confirm: bool = Field(..., description="Must be true to trigger rebuild") | |
| # ββ Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def serve_frontend(): | |
| index_file = frontend_path / "index.html" | |
| if index_file.exists(): | |
| return FileResponse(str(index_file)) | |
| return JSONResponse({ | |
| "message": "DermIQ API is running", | |
| "status": "ready" if engine.is_ready else "initializing", | |
| "docs": "/docs", | |
| "health": "/health", | |
| }) | |
| async def health_check(): | |
| stats = engine.get_stats() | |
| return { | |
| "status": "healthy" if engine.is_ready else "initializing", | |
| "service": "DermIQ RAG API", | |
| "version": "1.0.0", | |
| "engine": stats, | |
| } | |
| async def run_query(request: QueryRequest): | |
| if not engine.is_ready: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Engine is still initializing β please wait ~2 minutes and retry.", | |
| ) | |
| try: | |
| result = engine.query(request.question) | |
| return QueryResponse(**result) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| except RuntimeError as exc: | |
| raise HTTPException(status_code=503, detail=str(exc)) | |
| except Exception as exc: | |
| logger.error(f"Query error: {exc}", exc_info=True) | |
| raise HTTPException(status_code=500, detail="Internal error. Please retry.") | |
| async def rebuild_vector_index(request: RebuildRequest): | |
| if not request.confirm: | |
| raise HTTPException(status_code=400, detail="Set confirm: true to proceed.") | |
| try: | |
| engine.build_vectorstore(force_rebuild=True) | |
| return {"status": "success", "stats": engine.get_stats()} | |
| except Exception as exc: | |
| raise HTTPException(status_code=500, detail=str(exc)) | |
| async def get_engine_stats(): | |
| return engine.get_stats() | |
| # ββ Entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| import uvicorn | |
| port = int(os.environ.get("PORT", 7860)) | |
| uvicorn.run( | |
| "server:app", | |
| host="0.0.0.0", | |
| port=port, | |
| reload=False, | |
| log_level="info", | |
| ) |