feat(documents): add document parser, service, and API routes

Why:
- Need to parse .docx/.doc files into DocumentTree + markdown
- Need API endpoints for document upload, processing, and deletion

Changes:
- Parser: .doc→.docx conversion via LibreOffice, XML-level extraction for table-heavy docs, heuristic heading detection
- Service: upload, process (delegates to chunking), delete orchestration
- Routes: POST /documents, POST /documents/{id}/process, DELETE /documents/{id}, GET /strategies
This commit is contained in:
2026-07-26 09:37:40 +03:30
parent 4aaaccec49
commit fdc21e3316
5 changed files with 654 additions and 0 deletions

71
src/documents/routes.py Normal file
View File

@@ -0,0 +1,71 @@
"""Document and strategy API routes.
Endpoints:
POST /documents Upload a .docx file
POST /documents/{id}/process Run chunking strategies (stub until Phase 2)
DELETE /documents/{id} Remove document + vectors
GET /strategies List available chunking strategies
"""
from fastapi import APIRouter, File, UploadFile
from src.core.exceptions import DocumentProcessingError
from src.core.models import PaginatedResponse, StrategyName
from src.documents.models import (
DeleteResponse,
DocumentResponse,
ProcessRequest,
ProcessResponse,
StrategiesResponse,
StrategyInfo,
)
from src.documents import service
router = APIRouter()
@router.post("/documents", response_model=DocumentResponse, status_code=201)
async def upload_document(file: UploadFile = File(...)):
"""Upload a .docx file. Parses it, stores the document tree in SQLite."""
if not file.filename:
raise DocumentProcessingError("No filename provided")
if not file.filename.lower().endswith((".docx", ".doc")):
raise DocumentProcessingError("Only .docx and .doc 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", response_model=ProcessResponse)
async def process_document(doc_id: str, request: ProcessRequest):
"""Run selected chunking strategies on an uploaded document.
Currently returns stub results until Phase 2 implements the strategies.
"""
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]
)