Spaces:
Sleeping
Sleeping
| """ | |
| FastAPI entrypoint — the "API + Safety layer" in the architecture diagram. | |
| POST /query -> runs guardrails, invokes the LangGraph agentic core, | |
| runs output guardrails, returns the final answer + sources. | |
| GET /health -> liveness/readiness probe for Cloud Run. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import os | |
| import time | |
| from dotenv import load_dotenv | |
| from fastapi import FastAPI, HTTPException, UploadFile, File | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pathlib import Path | |
| import shutil | |
| from api.schemas import ChunkResponse, HealthResponse, QueryRequest, QueryResponse | |
| from core.graph import run_query | |
| from guardrails.filters import run_input_guardrails, run_output_guardrails | |
| from observability.logging_config import configure_logging | |
| load_dotenv() | |
| configure_logging() | |
| logger = logging.getLogger("api.main") | |
| app = FastAPI( | |
| title="Production-Grade Advanced RAG API", | |
| description="Cyclic LangGraph RAG system with guardrails, LLM gateway, and evaluation support.", | |
| version="1.0.0", | |
| ) | |
| cors_origins = os.getenv("CORS_ORIGINS", "http://localhost:8501").split(",") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=cors_origins, | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| def startup_event(): | |
| logger.info("Pre-warming model dependencies (embeddings and reranker)...") | |
| try: | |
| from ingestion.embedding import get_embedder | |
| from core.retriever import _get_reranker | |
| get_embedder() | |
| _get_reranker() | |
| logger.info("Model pre-warming complete. Ready to serve requests!") | |
| except Exception as e: | |
| logger.warning("Failed to pre-warm models: %s", e) | |
| GUARDRAILS_ENABLED = os.getenv("GUARDRAILS_ENABLED", "true").lower() == "true" | |
| def health() -> HealthResponse: | |
| return HealthResponse(status="ok", vector_backend=os.getenv("VECTOR_BACKEND", "qdrant")) | |
| def ingest(file: UploadFile = File(...)): | |
| logger.info("Ingestion request received for file: %s", file.filename) | |
| temp_dir = Path("data/temp_upload") | |
| temp_dir.mkdir(parents=True, exist_ok=True) | |
| file_path = temp_dir / file.filename | |
| try: | |
| with open(file_path, "wb") as buffer: | |
| shutil.copyfileobj(file.file, buffer) | |
| from ingestion.pipeline import run as run_ingestion | |
| run_ingestion(source_dir=str(temp_dir), gcs_bucket=None, gcs_prefix="") | |
| return {"status": "success", "message": f"Successfully ingested {file.filename}"} | |
| except Exception as e: | |
| logger.exception("Ingestion failed for %s", file.filename) | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| finally: | |
| if file_path.exists(): | |
| file_path.unlink() | |
| try: | |
| temp_dir.rmdir() | |
| except Exception: | |
| pass | |
| def query(request: QueryRequest) -> QueryResponse: | |
| start = time.perf_counter() | |
| if GUARDRAILS_ENABLED: | |
| input_check = run_input_guardrails(request.query) | |
| if not input_check.allowed: | |
| logger.warning("Blocked request thread_id=%s reason=%s", request.thread_id, input_check.reason) | |
| raise HTTPException(status_code=400, detail=f"Request blocked by guardrails: {input_check.reason}") | |
| sanitized_query = input_check.sanitized_text | |
| else: | |
| sanitized_query = request.query | |
| try: | |
| state = run_query(sanitized_query, thread_id=request.thread_id) | |
| except Exception as exc: # noqa: BLE001 | |
| logger.exception("Graph execution failed for thread_id=%s", request.thread_id) | |
| raise HTTPException(status_code=502, detail=f"RAG pipeline error: {exc}") from exc | |
| answer = state.get("final_answer", "") | |
| if GUARDRAILS_ENABLED: | |
| output_check = run_output_guardrails(answer) | |
| answer = output_check.sanitized_text | |
| elapsed_ms = (time.perf_counter() - start) * 1000 | |
| logger.info( | |
| "query_served thread_id=%s refine_count=%d satisfactory=%s latency_ms=%.1f", | |
| request.thread_id, state.get("refine_count", 0), state.get("is_satisfactory"), elapsed_ms, | |
| ) | |
| sources = [ | |
| ChunkResponse(chunk_id=c["chunk_id"], score=c["score"], text_preview=c["text"][:200]) | |
| for c in state.get("reranked_chunks", []) | |
| ] | |
| return QueryResponse( | |
| answer=answer, | |
| standalone_query=state.get("standalone_query", sanitized_query), | |
| refine_count=state.get("refine_count", 0), | |
| is_satisfactory=state.get("is_satisfactory", True), | |
| sources=sources, | |
| trace=state.get("trace"), | |
| ) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run("api.main:app", host=os.getenv("API_HOST", "0.0.0.0"), port=int(os.getenv("API_PORT", "8000"))) | |