Spaces:
Runtime error
Runtime error
| """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" | |
| 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"], | |
| ) | |
| 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 | |
| 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 ""), | |
| ) | |
| def list_documents() -> list[DocumentSummary]: | |
| return [_to_summary(d) for d in db.list_documents()] | |
| 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) | |
| 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} | |
| 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") | |
| def root() -> FileResponse: | |
| return FileResponse(str(STATIC_DIR / "index.html")) | |