feat(documents): add text pdf ingestion with heading reconstruction

Why:
- Support Text PDFs in the same DocumentTree/markdown contract as DOCX.

Changes:
- PyMuPDF parser, text-layer gate, shared heading heuristics, upload dispatch for .pdf.

Impact:
- Scanned/image PDFs are rejected at upload; requires pymupdf installed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-02 16:59:42 +03:30
parent afc20200f7
commit 55907a8dec
8 changed files with 418 additions and 131 deletions

View File

@@ -1,8 +1,8 @@
"""Document and strategy API routes.
Endpoints:
POST /documents Upload a .docx file
POST /documents/{id}/process Run chunking strategies (stub until Phase 2)
POST /documents Upload a .docx / .doc / .pdf file
POST /documents/{id}/process Run chunking strategies
DELETE /documents/{id} Remove document + vectors
GET /strategies List available chunking strategies
"""
@@ -10,7 +10,6 @@ Endpoints:
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,
@@ -20,6 +19,7 @@ from src.documents.models import (
StrategiesResponse,
StrategyInfo,
)
from src.documents.parser import SUPPORTED_SUFFIXES
from src.documents import service
router = APIRouter()
@@ -39,11 +39,15 @@ async def list_documents(offset: int = 0, limit: int = 50):
@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."""
"""Upload a document. 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")
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:
@@ -61,10 +65,7 @@ async def upload_document(file: UploadFile = File(...)):
@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.
"""
"""Run selected chunking strategies on an uploaded document."""
return service.process_document(doc_id, request)