docs-qa-backend / app /main.py
capcut8128's picture
Upload 12 files
0a19322 verified
Raw History Blame Contribute Delete
3.51 kB
import logging
import threading
from contextlib import asynccontextmanager
from fastapi import BackgroundTasks, FastAPI, File, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from . import ingest, store
from .bm25 import bm25
from .config import settings
from .rag import NoReadyDocuments, answer_question
from .registry import get_registry, public
try:
from groq import RateLimitError
except Exception: # pragma: no cover
RateLimitError = () # type: ignore
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
log = logging.getLogger("rag.main")
_warm = threading.Event()
def _warm_up() -> None:
"""Load models in the background so the server starts instantly."""
try:
store.get_embedder()
store.get_qdrant()
ingest.recover_interrupted()
store.get_reranker()
bm25.ensure_fresh()
except Exception:
log.exception("Warm-up failed (the app will retry lazily on first use)")
finally:
_warm.set()
@asynccontextmanager
async def lifespan(app: FastAPI):
threading.Thread(target=_warm_up, daemon=True).start()
yield
app = FastAPI(title="RAG Backend", version="0.3.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=[o.strip() for o in settings.cors_origins.split(",") if o.strip()],
allow_methods=["*"],
allow_headers=["*"],
)
class Turn(BaseModel):
role: str = Field(pattern="^(user|assistant)$")
content: str
class AskRequest(BaseModel):
question: str
doc_ids: list[str] | None = None # None = search every ready document
history: list[Turn] = []
@app.get("/health")
def health():
return {"status": "ok", "llm_model": settings.llm_model, "models_loaded": _warm.is_set()}
@app.get("/documents")
def documents():
return [public(d) for d in get_registry().all()]
@app.post("/documents")
def upload_document(background: BackgroundTasks, file: UploadFile = File(...)):
data = file.file.read()
try:
entry, duplicate = ingest.submit_document(file.filename or "upload", data)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
if not duplicate:
background.add_task(ingest.process_document, entry["doc_id"], entry["filename"], data)
return {**public(entry), "duplicate": duplicate}
@app.delete("/documents/{doc_id}")
def delete_document(doc_id: str):
if not ingest.delete_document(doc_id):
raise HTTPException(status_code=404, detail="Document not found.")
return {"deleted": doc_id}
@app.post("/ask")
def ask(req: AskRequest):
if not req.question.strip():
raise HTTPException(status_code=400, detail="Question is empty.")
if not settings.groq_api_key:
raise HTTPException(status_code=500, detail="GROQ_API_KEY is not set in .env")
try:
return answer_question(req.question.strip(), req.doc_ids, [t.model_dump() for t in req.history])
except NoReadyDocuments:
raise HTTPException(status_code=409, detail="No document is ready yet. Upload one and wait until it finishes processing.")
except RateLimitError:
raise HTTPException(status_code=429, detail="Groq free-tier rate limit reached. Wait a few seconds and try again.")
except Exception as e: # bad key, retired model, network...
log.exception("ask failed")
raise HTTPException(status_code=502, detail=f"LLM error: {e}")