MedRAG / app /api /routes /documents.py
hetsheta's picture
Initial commit
ea50fb7
Raw
History Blame Contribute Delete
5.23 kB
from pathlib import Path
from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import List
from app.core.security import get_current_user_id
from app.db.database import get_db, DocumentRecord, User
from app.schemas.schemas import BatchIngestResponse, DocumentOut
from app.services.ingestion_service import (
create_document_record,
get_conversation_documents,
get_all_documents,
get_document,
schedule_ingestion,
validate_medical_document,
)
from app.db import chat_db
from app.services.qdrant_service import get_qdrant_service
router = APIRouter(prefix="/documents", tags=["documents"])
ALLOWED_EXT = {"pdf", "docx", "txt"}
def _check_ext(filename: str) -> None:
ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
if ext not in ALLOWED_EXT:
raise HTTPException(status_code=400, detail=f"Unsupported file type. Allowed: {', '.join(ALLOWED_EXT)}")
async def _require_admin(user_id: str, db: AsyncSession) -> User:
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user or user.role != "admin":
raise HTTPException(status_code=403, detail="Admin access required")
return user
@router.post("/upload", response_model=BatchIngestResponse, status_code=202)
async def upload_document(
files: List[UploadFile] = File(...),
conversation_id: str = Form(...),
title: str | None = Form(default=None),
source: str | None = Form(default=None),
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
"""Queue one or more medical documents for ingestion."""
conv = await chat_db.get_conversation(user_id, conversation_id)
if not conv:
raise HTTPException(status_code=404, detail="Conversation not found")
pending_documents: List[tuple[str, bytes, str]] = []
for file in files:
filename = file.filename or "unknown"
_check_ext(filename)
raw_bytes = await file.read()
try:
validate_medical_document(filename, raw_bytes)
except ValueError as exc:
raise HTTPException(status_code=400, detail=f"{filename}: {exc}") from exc
doc_title = title.strip() if title and len(files) == 1 else Path(filename).stem.replace("-", " ").replace("_", " ")
pending_documents.append((filename, raw_bytes, doc_title))
document_ids: List[str] = []
for filename, raw_bytes, doc_title in pending_documents:
doc_id = await create_document_record(
filename=filename,
title=doc_title,
source=source,
user_id=user_id,
conversation_id=conversation_id,
db=db,
)
schedule_ingestion(
doc_id=doc_id,
filename=filename,
title=doc_title,
source=source,
raw_bytes=raw_bytes,
)
document_ids.append(doc_id)
noun = "document" if len(document_ids) == 1 else "documents"
return BatchIngestResponse(
document_ids=document_ids,
message=f"Queued {len(document_ids)} {noun} for ingestion",
)
@router.get("", response_model=List[DocumentOut])
async def list_documents(
conversation_id: str | None = None,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
if conversation_id:
conv = await chat_db.get_conversation(user_id, conversation_id)
if not conv:
raise HTTPException(status_code=404, detail="Conversation not found")
docs = await get_conversation_documents(conversation_id, db)
else:
docs = await get_all_documents(db)
return docs
@router.get("/{doc_id}", response_model=DocumentOut)
async def get_document_detail(
doc_id: str,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
doc = await get_document(doc_id, db)
if not doc:
raise HTTPException(status_code=404, detail="Document not found")
return doc
@router.delete("/conversation/{conversation_id}", status_code=204)
async def clear_conversation_documents(
conversation_id: str,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
conv = await chat_db.get_conversation(user_id, conversation_id)
if not conv:
raise HTTPException(status_code=404, detail="Conversation not found")
docs = await get_conversation_documents(conversation_id, db)
qdrant = get_qdrant_service()
for doc in docs:
await qdrant.delete_by_doc_id(doc.id)
await db.delete(doc)
await db.commit()
@router.delete("/{doc_id}", status_code=204)
async def delete_document(
doc_id: str,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
await _require_admin(user_id, db)
doc = await get_document(doc_id, db)
if not doc:
raise HTTPException(status_code=404, detail="Document not found")
qdrant = get_qdrant_service()
await qdrant.delete_by_doc_id(doc_id)
await db.delete(doc)
await db.commit()