dashhdata's picture
Upload folder using huggingface_hub
0151e19 verified
Raw
History Blame Contribute Delete
4.94 kB
"""FastAPI application: upload documents, list/manage them, and chat over them."""
from __future__ import annotations
import uuid
from pathlib import Path
from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from . import db, vector_store
from .config import get_settings
from .embeddings import embed_texts
from .ingestion import process_document
from .models import ChatRequest, ChatResponse, DocumentSummary, IngestResponse
app = FastAPI(
title="Document Intelligence & Chat Assistant",
description="RAG over your uploaded documents: OCR + MongoDB + Qdrant + Claude.",
version="1.0.0",
)
STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
@app.on_event("startup")
def _startup() -> None:
# Best-effort: create the Qdrant collection up front. Never block startup —
# if Qdrant is briefly unreachable the collection is created lazily on the
# first upload instead, so the server still binds and the UI loads.
try:
vector_store.ensure_collection()
except Exception as exc: # noqa: BLE001
print(f"[startup] Qdrant not ready yet, will retry on first upload: {exc}")
def _to_summary(doc: dict) -> DocumentSummary:
return DocumentSummary(
id=doc["_id"],
filename=doc["filename"],
content_type=doc["content_type"],
num_chunks=doc["num_chunks"],
num_chars=doc["num_chars"],
ocr_used=doc["ocr_used"],
created_at=doc["created_at"],
)
@app.get("/health")
def health() -> dict:
status = {"status": "ok"}
try:
db.ping()
status["mongodb"] = "ok"
except Exception as exc: # noqa: BLE001
status["mongodb"] = f"error: {exc}"
try:
vector_store.get_client().get_collections()
status["qdrant"] = "ok"
except Exception as exc: # noqa: BLE001
status["qdrant"] = f"error: {exc}"
status["llm_key_configured"] = bool(get_settings().openrouter_api_key)
return status
@app.post("/documents/upload", response_model=IngestResponse)
async def upload_document(file: UploadFile = File(...)) -> IngestResponse:
settings = get_settings()
data = await file.read()
if not data:
raise HTTPException(status_code=400, detail="Uploaded file is empty.")
chunks, ocr_used, num_chars = process_document(
file.filename, file.content_type or "", data,
settings.chunk_size, settings.chunk_overlap,
)
if not chunks:
raise HTTPException(
status_code=422,
detail="No text could be extracted from this document.",
)
document_id = str(uuid.uuid4())
vectors = embed_texts(chunks)
vector_store.ensure_collection() # lazy create if startup couldn't reach Qdrant
vector_store.upsert_chunks(document_id, file.filename, chunks, vectors)
doc = db.save_document(
document_id=document_id,
filename=file.filename,
content_type=file.content_type or "",
num_chunks=len(chunks),
num_chars=num_chars,
ocr_used=ocr_used,
)
return IngestResponse(
document=_to_summary(doc),
message=f"Indexed {len(chunks)} chunks" + (" (OCR used)" if ocr_used else ""),
)
@app.get("/documents", response_model=list[DocumentSummary])
def list_documents() -> list[DocumentSummary]:
return [_to_summary(d) for d in db.list_documents()]
@app.get("/documents/{document_id}", response_model=DocumentSummary)
def get_document(document_id: str) -> DocumentSummary:
doc = db.get_document(document_id)
if not doc:
raise HTTPException(status_code=404, detail="Document not found.")
return _to_summary(doc)
@app.delete("/documents/{document_id}")
def delete_document(document_id: str) -> dict:
if not db.get_document(document_id):
raise HTTPException(status_code=404, detail="Document not found.")
vector_store.delete_document(document_id)
db.delete_document(document_id)
return {"deleted": document_id}
@app.post("/chat", response_model=ChatResponse)
def chat(request: ChatRequest) -> ChatResponse:
from . import rag
try:
return rag.answer_question(
question=request.question,
document_ids=request.document_ids,
top_k=request.top_k,
)
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc))
except Exception as exc: # noqa: BLE001 — always return JSON so the UI can show it
import traceback
traceback.print_exc()
raise HTTPException(status_code=502, detail=f"{type(exc).__name__}: {exc}")
# --- Minimal web UI (served at /) ---
if STATIC_DIR.exists():
app.mount("/ui", StaticFiles(directory=str(STATIC_DIR), html=True), name="ui")
@app.get("/")
def root() -> FileResponse:
return FileResponse(str(STATIC_DIR / "index.html"))