Skip to content

feat: setup pdf parser service using docling#15

Merged
ayushpahwa merged 86 commits into
trunkfrom
feat/pdf-parser-setup
Apr 15, 2026
Merged

feat: setup pdf parser service using docling#15
ayushpahwa merged 86 commits into
trunkfrom
feat/pdf-parser-setup

Conversation

@ayushpahwa

@ayushpahwa ayushpahwa commented Dec 17, 2025

Copy link
Copy Markdown
Member

Summary

  • Set up the pdf-parse Python service using FastAPI + Docling for extracting structured data from health report PDFs
  • Built a full extraction pipeline: PDF → Docling → typed Pydantic models → page-grouped JSON output
  • Created typed models for all output: section headers, groups (KV and list via discriminated union), tables with row-level bounding boxes
  • Added generic ApiResponse[T] wrapper with unified exception handling across all endpoints
  • Migrated from raw dict parsing to Docling's typed objects for type safety
  • Set up CI workflow, linting (ruff), type checking (pyright), and dependency management (uv)

Key files

  • pdf-parse/app/services/docling.py — extraction provider with parse_section_headers, parse_groups, parse_tables, normalise_extraction
  • pdf-parse/app/schemas/extract.py — Pydantic models: Page, ParsedTable, TableRow, ParseSectionHeader, ParsedGroups, KVGroup | ListGroup
  • pdf-parse/app/schemas/api_response.py — generic ApiResponse[T] response wrapper
  • pdf-parse/app/exception_handlers.py — unified exception handler for HTTPException, validation errors, and unhandled exceptions
  • pdf-parse/app/schemas/docling.py — type aliases bridging Docling union types

Known Docling artifacts (deferred to LLM normalization step)

  • Method text merges into test names (e.g., "Haemoglobin (HB) Method: Photometry")
  • Cross-page row contamination on first row of continuation pages
  • Colon prefix on group values (: Ayush Pahwa)
  • Patient info duplicated across pages
  • OCR spacing artifacts (DEPARTME N T)

Test plan

  • Upload 3-page haematology PDF → verify all 29 test values extracted correctly
  • Upload non-PDF file → verify 400 error wrapped in ApiResponse format
  • Upload file > 5MB → verify 413 error wrapped in ApiResponse format
  • Verify null fields excluded from JSON response
  • Verify section headers, groups, and tables are page-grouped correctly

🤖 Generated with Claude Code

@ayushpahwa ayushpahwa self-assigned this Dec 17, 2025
@ayushpahwa ayushpahwa marked this pull request as ready for review December 17, 2025 07:11
@ayushpahwa ayushpahwa changed the title Feat/pdf parser setup feat: setup pdf parser service using docling Dec 17, 2025

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a Python-based PDF parsing microservice to the monorepo using Docling and FastAPI. The service enables local extraction of health metrics from PDF reports without sending sensitive data to cloud services, addressing privacy requirements for the health data management application.

Key Changes:

  • New PDF parsing service with provider pattern for swappable extraction backends
  • FastAPI endpoints for health checks and PDF extraction
  • Integration with pnpm workspace via postinstall hooks and custom scripts
  • Documentation updates for setup and development workflows

Reviewed changes

Copilot reviewed 14 out of 20 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
src/pdf-parse/pyproject.toml Python project configuration with dependencies for Docling, FastAPI, and PyTorch
src/pdf-parse/app/main.py FastAPI application entry point with router configuration
src/pdf-parse/app/routers/health.py Health check endpoint returning service status
src/pdf-parse/app/routers/extract.py PDF extraction endpoint with file upload handling
src/pdf-parse/app/services/provider.py Abstract provider interface for extraction backends
src/pdf-parse/app/services/docling.py Docling-based implementation of extraction provider
src/pdf-parse/app/schemas/extract.py Pydantic models for API response schemas
src/pdf-parse/.python-version Python version requirement (3.13)
src/pdf-parse/README.md Service-specific documentation with setup and usage instructions
src/package.json Added postinstall hook for Python deps and PDF service scripts
src/AGENTS.md Updated architecture documentation to include PDF service
docs/setup.md Added Python/uv prerequisites and PDF service endpoints
.gitignore Added Python-specific ignore patterns

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/pdf-parse/README.md
Comment thread src/pdf-parse/app/main.py Outdated
Comment thread src/pdf-parse/app/main.py
Comment on lines +4 to +8
app = FastAPI(
title="PDF Service",
description="PDF extraction service for My Health Log",
version="0.1.0",
)

Copilot AI Dec 17, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing CORS configuration. Since this is a microservice that will be called from a web application (the client app), it needs CORS headers configured to allow requests from the frontend. Without proper CORS middleware, the browser will block cross-origin requests to this API. Consider adding FastAPI's CORSMiddleware with appropriate allowed origins.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

Comment thread src/pdf-parse/app/routers/extract.py Outdated
Comment thread src/pdf-parse/app/services/docling.py Outdated
Comment thread src/pdf-parse/app/routers/extract.py Outdated
Comment thread src/pdf-parse/app/routers/extract.py Outdated
Comment thread src/pdf-parse/app/routers/extract.py Outdated
Comment on lines +12 to +14
if not file.filename.endswith(".pdf"):
raise HTTPException(400, "Only PDF files supported")

Copilot AI Dec 17, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file validation only checks the extension, which is insufficient for security. An attacker could upload a malicious file with a .pdf extension. Consider adding MIME type validation and/or file content validation to ensure the uploaded file is actually a PDF. For example, verify the file starts with the PDF magic bytes "%PDF".

Suggested change
if not file.filename.endswith(".pdf"):
raise HTTPException(400, "Only PDF files supported")
# Basic filename check for user convenience
if not file.filename.lower().endswith(".pdf"):
raise HTTPException(400, "Only PDF files supported")
# Validate reported MIME type
if file.content_type != "application/pdf":
raise HTTPException(400, "Invalid content type; only PDF files supported")
# Validate file signature (magic bytes) to ensure it is actually a PDF
header = await file.read(4)
if header != b"%PDF":
raise HTTPException(400, "Uploaded file is not a valid PDF")
# Reset file pointer so downstream code reads from the beginning
await file.seek(0)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docling itself validates the PDF structure during conversion and will raise on invalid files. Magic byte validation is not needed here.

Comment thread src/pdf-parse/app/routers/extract.py Outdated
Comment thread src/pdf-parse/app/services/docling.py Outdated
Comment on lines +15 to +18
async def extract(self, file: UploadFile) -> dict:
content = await file.read()
stream = DocumentStream(name=file.filename, stream=io.BytesIO(content))
result = self.converter.convert(stream)

Copilot AI Dec 17, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extract method is defined as async but calls synchronous blocking operations (file.read() and converter.convert()). The DocumentConverter.convert() call likely performs CPU-intensive ML operations that will block the event loop, degrading performance for concurrent requests. Consider using run_in_executor to run the blocking conversion in a thread pool, or verify if Docling provides async APIs.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ayushpahwa yo check this

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. This is a single-user local service for now. Will move to run_in_executor if concurrency becomes a concern.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #16 to track this. Added a TODO in code linking to the issue.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed and closed #16. Also added pyright and tsc type checking to CI pipeline. Test coverage tracked as #17.

@DPirate

DPirate commented Dec 18, 2025

Copy link
Copy Markdown
Collaborator

@ayushpahwa , I think if we are enabling AI reviews then rhe author should resolve the ai comments before human review. It will increase clarity about what is already being addressed and also cleanup the review view of github.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 21 changed files in this pull request and generated 11 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/pdf-parse/app/services/docling.py Outdated
async def extract(self, file: UploadFile) -> dict:
if file.size is None or file.filename is None:
raise HTTPException(404, "Please use a valid file")
print(file.size)

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug print statement should be removed or replaced with proper logging. This will clutter the console output in production and doesn't follow the logging pattern established elsewhere in the application (using the logger from app.state).

Suggested change
print(file.size)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — replaced all print statements with logger.error() using the uvicorn logger.

Comment thread src/pdf-parse/app/services/docling.py Outdated
# TODO: replace return type from dict to ExtractionResponse once mapping is fixed
async def extract(self, file: UploadFile) -> dict:
if file.size is None or file.filename is None:
raise HTTPException(404, "Please use a valid file")

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message clarity issue: The message "Please use a valid file" is vague and doesn't explain what makes a file valid. This will be unhelpful to API consumers trying to debug upload failures. Consider a more descriptive message like "File upload is invalid: missing file size or filename".

Suggested change
raise HTTPException(404, "Please use a valid file")
raise HTTPException(404, "File upload is invalid: missing file size or filename.")

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now routed through the unified exception handler which returns a structured ApiResponse with the error details.

Comment thread docs/setup.md
Comment on lines +44 to +47
| `pnpm dev` | Start server, client, and PDF service in parallel |
| `pnpm dev:server` | Start server only |
| `pnpm dev:client` | Start client only |
| `pnpm dev:pdf-parse` | Start PDF parsing service (dev mode) |

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation references script name "pnpm dev:pdf-parse" but earlier in the same document (line 9) it shows "pnpm dev" running "server + client + PDF service in parallel". However, looking at package.json, the "dev" script doesn't actually include the PDF service automatically. The documentation should clarify that users need to run "pnpm dev:pdf-parse" separately, or the package.json dev script should be updated to include it.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dev script on line 9 runs all services in parallel including pdf-parse. The separate dev:pdf-parse script is for running the PDF service standalone. This is documented correctly.

Comment thread src/pdf-parse/app/services/docling.py Outdated
# TODO: replace return type from dict to ExtractionResponse once mapping is fixed
async def extract(self, file: UploadFile) -> dict:
if file.size is None or file.filename is None:
raise HTTPException(404, "Please use a valid file")

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP status code 404 (Not Found) is semantically incorrect for invalid file uploads. A 404 status code indicates that the requested resource doesn't exist on the server. For file validation failures, use 400 (Bad Request) instead to indicate that the client sent an invalid request.

Suggested change
raise HTTPException(404, "Please use a valid file")
raise HTTPException(400, "Please use a valid file")

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — changed to HTTPException(400).

Comment thread src/pdf-parse/app/main.py
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing HTTP methods validation in CORS configuration. The CORSMiddleware is configured without specifying allowed HTTP methods (allow_methods) or headers (allow_headers). This could cause CORS preflight requests to fail when the frontend tries to POST files to /extract. Add allow_methods=[""] or a specific list of methods, and allow_headers=[""] or specific headers needed for file uploads.

Suggested change
allow_credentials=True,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added allow_methods=[""] and allow_headers=[""].

Comment thread src/package.json
"scripts": {
"dev": "pnpm --parallel --filter '*' dev",
"postinstall": "[ \"$CI\" = 'true' ] || (cd pdf-parse && uv sync)",
"dev": "pnpm --parallel --filter '*' dev & pnpm dev:pdf-parse",

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "&" operator for running parallel commands is not cross-platform compatible. It works on Unix-like systems (Linux/macOS) but will fail on Windows. Since the repository appears to be using pnpm's built-in parallel execution, use "pnpm --parallel" instead, or use a cross-platform tool like "concurrently" or "npm-run-all".

Copilot uses AI. Check for mistakes.
Comment thread src/AGENTS.md Outdated
pnpm dev # Run server + client in parallel
pnpm dev:server # Server only
pnpm dev:client # Client only
pnpm pdf:dev # PDF parsing service (dev mode)

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation references script name "pnpm pdf:dev" which doesn't exist in package.json. The actual script name is "pnpm dev:pdf-parse". This inconsistency will confuse users following the documentation. Update this to match the actual script name in package.json.

Suggested change
pnpm pdf:dev # PDF parsing service (dev mode)
pnpm dev:pdf-parse # PDF parsing service (dev mode)

Copilot uses AI. Check for mistakes.
Comment thread src/AGENTS.md Outdated
pnpm dev:server # Server only
pnpm dev:client # Client only
pnpm pdf:dev # PDF parsing service (dev mode)
pnpm pdf:start # PDF parsing service (production)

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation references script name "pnpm pdf:start" which doesn't exist in package.json. The actual script name is "pnpm start:pdf-parse". This inconsistency will confuse users following the documentation. Update this to match the actual script name in package.json.

Suggested change
pnpm pdf:start # PDF parsing service (production)
pnpm start:pdf-parse # PDF parsing service (production)

Copilot uses AI. Check for mistakes.
# TODO: replace return type from dict to ExtractionResponse once mapping is fixed
# async def extract(self, file: UploadFile) -> ExtractionResponse:
async def extract(self, file: UploadFile) -> dict:
pass

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The abstract method uses "pass" but doesn't raise NotImplementedError. While Python's ABC will prevent instantiation of classes that don't implement abstract methods, it's a best practice to include "raise NotImplementedError" in the abstract method body for clarity and to catch potential issues if the method is called via super().

Suggested change
pass
raise NotImplementedError("Subclasses of ExtractionProvider must implement the 'extract' method.")

Copilot uses AI. Check for mistakes.
Comment thread src/pdf-parse/app/schemas/extract.py Outdated
class Element(BaseModel):
text: str
bbox: BoundingBox
type: str # "text", "table", "heading"

Copilot AI Dec 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type field in Element uses a string literal type with specific expected values ("text", "table", "heading") indicated in the comment, but the actual type annotation is just 'str'. This allows any string value, which could lead to inconsistent data. Consider using a Literal type or Enum to enforce the allowed values: type: Literal["text", "table", "heading"].

Copilot uses AI. Check for mistakes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 27 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/pdf-parse/app/services/docling.py Outdated
top=bbox.t,
bottom=bbox.b,
right=bbox.r,
coord_origin=CoordOriginEnum(value=bbox.coord_origin.value),

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CoordOriginEnum is being constructed incorrectly: CoordOriginEnum(value=...) will raise TypeError because enums don’t accept a value= keyword. This will cause section header parsing to fail and be silently swallowed by the surrounding try/except. Use CoordOriginEnum(bbox.coord_origin.value) (or map the docling origin to your enum) instead.

Suggested change
coord_origin=CoordOriginEnum(value=bbox.coord_origin.value),
coord_origin=CoordOriginEnum(bbox.coord_origin.value),

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works correctly — Python StrEnum accepts value= as a keyword argument. We've verified the output multiple times with correct coord_origin values. Dismissing.

Comment thread src/AGENTS.md Outdated
Comment on lines +47 to +48
pnpm pdf:dev # PDF parsing service (dev mode)
pnpm pdf:start # PDF parsing service (production)

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commands pnpm pdf:dev and pnpm pdf:start referenced here don’t exist in src/package.json (the scripts are dev:pdf-parse / start:pdf-parse). This will mislead contributors following the setup instructions. Update the docs to the actual script names (or rename the scripts to match the docs).

Suggested change
pnpm pdf:dev # PDF parsing service (dev mode)
pnpm pdf:start # PDF parsing service (production)
pnpm dev:pdf-parse # PDF parsing service (dev mode)
pnpm start:pdf-parse # PDF parsing service (production)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — updated AGENTS.md to use the correct script names (dev:pdf-parse, start:pdf-parse).

@DPirate DPirate left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@ayushpahwa ayushpahwa merged commit c58a74c into trunk Apr 15, 2026
1 check passed
@ayushpahwa ayushpahwa deleted the feat/pdf-parser-setup branch April 15, 2026 04:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants