"""Document and strategy API routes. Endpoints: POST /documents Upload a .docx / .doc / .pdf file POST /documents/{id}/process Run chunking strategies; ?background=true enqueues a job DELETE /documents/{id} Remove document + vectors GET /strategies List available chunking strategies """ from fastapi import APIRouter, BackgroundTasks, File, Query, UploadFile from fastapi.responses import JSONResponse from src.core.exceptions import DocumentProcessingError from src.documents.models import ( DeleteResponse, DocumentResponse, DocumentListResponse, ProcessRequest, ProcessResponse, StrategiesResponse, StrategyInfo, ) from src.documents.parser import SUPPORTED_SUFFIXES from src.documents import service from src.jobs import service as jobs_service from src.jobs.models import JobCreatedResponse from src.storage import sqlite as db router = APIRouter() @router.get("/documents", response_model=DocumentListResponse) async def list_documents(offset: int = 0, limit: int = 50): """List all uploaded documents with pagination.""" result = service.list_documents(offset=offset, limit=limit) return DocumentListResponse( items=[DocumentResponse(**doc) for doc in result["items"]], total=result["total"], offset=result["offset"], limit=result["limit"], ) @router.post("/documents", response_model=DocumentResponse, status_code=201) async def upload_document(file: UploadFile = File(...)): """Upload a document. Parses it, stores the document tree in SQLite.""" if not file.filename: raise DocumentProcessingError("No filename provided") lower = file.filename.lower() if not any(lower.endswith(s) for s in SUPPORTED_SUFFIXES): raise DocumentProcessingError( f"Only {', '.join(SUPPORTED_SUFFIXES)} files are supported" ) content = await file.read() if not content: raise DocumentProcessingError("Empty file") doc = service.upload_document(file.filename, content) return DocumentResponse( id=doc["id"], filename=doc["filename"], paragraph_count=doc.get("paragraph_count", 0), chunk_counts=doc.get("chunk_counts", {}), created_at=doc["created_at"], ) @router.post("/documents/{doc_id}/process") async def process_document( doc_id: str, request: ProcessRequest, background_tasks: BackgroundTasks, background: bool = Query( False, description="Run in background; returns job_id (HTTP 202)" ), ): """Run selected chunking strategies on an uploaded document. Pass ``background=true`` to enqueue and poll ``GET /jobs/{job_id}``. """ if background: if db.get_document(doc_id) is None: raise DocumentProcessingError(f"Document not found: {doc_id}") job = jobs_service.enqueue_process(doc_id, request) background_tasks.add_task(jobs_service.run_process_job, job["id"]) body = JobCreatedResponse( job_id=job["id"], job_type=job["job_type"], status=job["status"], poll_url=f"/jobs/{job['id']}", ) return JSONResponse(status_code=202, content=body.model_dump()) return service.process_document(doc_id, request) @router.delete("/documents/{doc_id}", response_model=DeleteResponse) async def delete_document(doc_id: str): """Delete a document and all its chunk vectors from Qdrant.""" deleted = service.delete_document(doc_id) return DeleteResponse(deleted=deleted, document_id=doc_id) @router.get("/strategies", response_model=StrategiesResponse) async def list_strategies(): """List available chunking strategies with descriptions.""" strategies = service.list_strategies() return StrategiesResponse( strategies=[StrategyInfo(**s) for s in strategies] )