VigilantRAG / app.py
Aryan
Implement production security hardening (bcrypt, CORS, rate limits, upload filters, local loopback bindings)
2be4436
Raw
History Blame Contribute Delete
6.97 kB
import os
import uvicorn
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
from typing import Dict, Any, List, Optional
from src.engine import VigilantRAGEngine
from src.config import config
from src.parser import parse_file
# Initialize FastAPI app
app = FastAPI(
title="VigilantRAG API",
description="Backend API for the Self-Correcting Multi-Stage RAG Engine",
version="1.0.0"
)
# Enable CORS for development
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5000"], # Allow Node.js backend proxy
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Initialize global engine
engine = VigilantRAGEngine()
# Request/Response schemas
class QueryRequest(BaseModel):
query: str = Field(..., example="What are the remote work guidelines?")
use_reranking: Optional[bool] = Field(default=True)
use_nli_guard: Optional[bool] = Field(default=True)
limit_document_id: Optional[str] = Field(default=None)
relevance_threshold: Optional[float] = Field(default=None, ge=0.0, le=1.0)
nli_threshold: Optional[float] = Field(default=None, ge=0.0, le=1.0)
temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0)
class IngestRequest(BaseModel):
doc_id: str = Field(..., example="doc_remote_policy")
title: str = Field(..., example="Remote Work Policy 2026")
text: str = Field(..., example="Employees can work remotely up to 3 days per week with manager approval...")
metadata: Dict[str, Any] = Field(default_factory=dict)
# API Endpoints
@app.post("/api/query")
def run_query(payload: QueryRequest):
"""Executes a search query through the self-correcting RAG pipeline."""
try:
# Build overrides configuration mapping
overrides = {}
if payload.relevance_threshold is not None:
overrides["relevance_threshold"] = payload.relevance_threshold
if payload.nli_threshold is not None:
overrides["nli_threshold"] = payload.nli_threshold
if payload.temperature is not None:
overrides["temperature_default"] = payload.temperature
# Plan-based capability limits overrides passed from Gateway
overrides["use_reranking"] = payload.use_reranking
overrides["use_nli_guard"] = payload.use_nli_guard
overrides["limit_document_id"] = payload.limit_document_id
# Execute query passing the overrides mapping
result = engine.query(payload.query, config_overrides=overrides)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/ingest")
def ingest_document(payload: IngestRequest):
"""Ingests a text document, chunks it, and rebuilds search indexes."""
try:
chunk_count = engine.retriever.ingest_document(
doc_id=payload.doc_id,
title=payload.title,
text=payload.text,
metadata=payload.metadata
)
return {
"status": "success",
"message": f"Document '{payload.title}' ingested successfully.",
"doc_id": payload.doc_id,
"chunks_created": chunk_count
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/ingest-file")
async def ingest_file(
doc_id: str = Form(...),
title: str = Form(...),
file: UploadFile = File(...)
):
"""Parses and ingests an uploaded file (.pdf, .docx, .xlsx, .txt)."""
try:
file_bytes = await file.read()
parsed_text = parse_file(file.filename, file_bytes)
chunk_count = engine.retriever.ingest_document(
doc_id=doc_id,
title=title,
text=parsed_text,
metadata={"filename": file.filename}
)
return {
"status": "success",
"message": f"File '{file.filename}' parsed and ingested successfully.",
"doc_id": doc_id,
"chunks_created": chunk_count,
"text_length": len(parsed_text)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"File ingestion failed: {str(e)}")
@app.get("/api/documents")
def list_documents():
"""Lists currently ingested documents and chunk details."""
try:
docs_summary = []
for doc_id, doc in engine.retriever.documents.items():
# Count chunks for this doc
doc_chunks = [c for c in engine.retriever.chunks if c.doc_id == doc_id]
docs_summary.append({
"id": doc_id,
"title": doc.title,
"text_length": len(doc.text),
"chunks_count": len(doc_chunks),
"metadata": doc.metadata
})
return docs_summary
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/documents/{doc_id}")
def get_document(doc_id: str):
"""Retrieves full text and details of a specific document."""
try:
if doc_id in engine.retriever.documents:
doc = engine.retriever.documents[doc_id]
return {
"id": doc.id,
"title": doc.title,
"text": doc.text,
"metadata": doc.metadata
}
else:
raise HTTPException(status_code=404, detail="Document not found")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/api/documents/{doc_id}")
def delete_document(doc_id: str):
"""Deletes a document and its chunks from the retriever index."""
try:
success = engine.retriever.delete_document(doc_id)
if not success:
raise HTTPException(status_code=404, detail=f"Document with ID '{doc_id}' not found.")
return {
"status": "success",
"message": f"Document '{doc_id}' deleted successfully."
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# Setup Static File Serving
static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
os.makedirs(static_dir, exist_ok=True)
# Mount static folder
# app.mount("/static", StaticFiles(directory=static_dir), name="static")
@app.get("/")
def read_root():
"""Serves the frontend dashboard landing page."""
index_path = os.path.join(static_dir, "index.html")
if os.path.exists(index_path):
return FileResponse(index_path)
return {"message": "VigilantRAG API is running. Build the Web UI static files to access the frontend dashboard at static/index.html."}
if __name__ == "__main__":
uvicorn.run("app:app", host="127.0.0.1", port=8000, reload=True)