expt8 / app.py
Enayut's picture
Deploy expt8 Gradio app
33f0428 verified
Raw History Blame Contribute Delete
3.24 kB
"""FastAPI service exposing the Customer Support RAG pipeline."""
import logging
import time
import uuid
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel, field_validator
from support_rag import BackendError, SupportRAG
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")
log = logging.getLogger("api")
MAX_QUESTION_CHARS = 1000
state = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# Heavy objects (embedding model, FAISS index, graph) are built once at startup.
t0 = time.perf_counter()
state["rag"] = SupportRAG()
log.info("startup complete in %.0f ms", (time.perf_counter() - t0) * 1000)
yield
state.clear()
app = FastAPI(title="Customer Support RAG API", version="1.0", lifespan=lifespan)
class AskRequest(BaseModel):
question: str
@field_validator("question")
@classmethod
def check_question(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("question must not be empty")
if len(v) > MAX_QUESTION_CHARS:
raise ValueError(f"question must be at most {MAX_QUESTION_CHARS} characters")
return v
class AskResponse(BaseModel):
answer: str
intent: str | None
sources: list[str]
retries: int
fallback: bool
latency_ms: float
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
# Only field + message are returned (no echo of the submitted input).
errors = [{"field": ".".join(str(p) for p in e["loc"][1:]) or "body",
"message": e["msg"].removeprefix("Value error, ")} for e in exc.errors()]
log.warning("rejected request: invalid input (%d error(s))", len(errors))
return JSONResponse(status_code=422, content={"detail": errors})
@app.get("/health")
def health():
return {"status": "ok", "rag_loaded": "rag" in state}
@app.post("/ask", response_model=AskResponse)
def ask(req: AskRequest):
req_id = uuid.uuid4().hex[:8]
start = time.perf_counter()
try:
result = state["rag"].ask(req.question)
except BackendError as e:
latency = (time.perf_counter() - start) * 1000
log.error("req=%s status=failure reason=%s latency_ms=%.0f", req_id, e, latency)
return JSONResponse(status_code=503, content={
"detail": "The answer service is temporarily unavailable. Please try again later.",
"request_id": req_id})
except Exception:
latency = (time.perf_counter() - start) * 1000
log.exception("req=%s status=failure reason=unexpected latency_ms=%.0f", req_id, latency)
return JSONResponse(status_code=500, content={
"detail": "Internal server error.", "request_id": req_id})
latency = (time.perf_counter() - start) * 1000
log.info("req=%s status=success intent=%s fallback=%s question_chars=%d latency_ms=%.0f",
req_id, result["intent"], result["fallback"], len(req.question), latency)
return AskResponse(**result, latency_ms=round(latency, 1))