Spaces:
Runtime error
Runtime error
File size: 4,942 Bytes
1e8bb26 0151e19 1e8bb26 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | """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"))
|