Skip to content

Commit 9f8d086

Browse files
committed
feat: add gRPC server with schema validation and DoclingDocument conversion
Add gRPC service, mapping layer, startup schema validator, and tests aligned with docling-core protobuf parity. Signed-off-by: Kristian Rickert <krickert@gmail.com>
1 parent 52397b8 commit 9f8d086

46 files changed

Lines changed: 11427 additions & 596 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/job-checks.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,40 @@ jobs:
5858
- name: Create the server
5959
run: .venv/bin/python -c 'from docling_serve.app import create_app; create_app()'
6060

61+
grpc-unit-tests:
62+
runs-on: ubuntu-latest
63+
strategy:
64+
matrix:
65+
python-version: ['3.12']
66+
steps:
67+
- uses: actions/checkout@v5
68+
- name: Install uv and set the python version
69+
uses: astral-sh/setup-uv@v6
70+
with:
71+
python-version: ${{ matrix.python-version }}
72+
enable-cache: true
73+
- name: Install dependencies
74+
run: uv sync --frozen --all-extras --no-extra flash-attn
75+
- name: Run gRPC unit tests
76+
run: uv run pytest -m unit -k grpc -v
77+
78+
grpc-integration-tests:
79+
runs-on: ubuntu-latest
80+
strategy:
81+
matrix:
82+
python-version: ['3.12']
83+
steps:
84+
- uses: actions/checkout@v5
85+
- name: Install uv and set the python version
86+
uses: astral-sh/setup-uv@v6
87+
with:
88+
python-version: ${{ matrix.python-version }}
89+
enable-cache: true
90+
- name: Install dependencies
91+
run: uv sync --frozen --all-extras --no-extra flash-attn
92+
- name: Run gRPC integration tests (excluding OCR)
93+
run: uv run pytest -m "integration and not ocr" -k grpc -v
94+
6195
# markdown-lint:
6296
# runs-on: ubuntu-latest
6397
# steps:

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ model_artifacts/
22
scratch/
33
.md-lint
44
actionlint
5+
buf.yaml
56

67
# Created by https://www.toptal.com/developers/gitignore/api/python,macos,virtualenv,pycharm,visualstudiocode,emacs,vim,jupyternotebooks
78
# Edit at https://www.toptal.com/developers/gitignore?templates=python,macos,virtualenv,pycharm,visualstudiocode,emacs,vim,jupyternotebooks
@@ -133,6 +134,7 @@ Temporary Items
133134
# Gradle
134135
.idea/**/gradle.xml
135136
.idea/**/libraries
137+
*.iml
136138

137139
# Gradle and Maven with auto-import
138140
# When using Gradle or Maven with auto-import, you should exclude module files,
@@ -448,4 +450,8 @@ pip-selfcheck.json
448450
cookies.txt
449451

450452
# Examples
451-
/examples/splitted_pdf/*
453+
/examples/splitted_pdf/*
454+
455+
456+
# Cursor workspace
457+
*.code-workspace

CONTRIBUTING.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,32 @@ This creates a Virtual Environment with Python 3.10. For other versions, replace
138138
poetry add NAME
139139
```
140140
141+
## Running tests
142+
143+
Tests are organised with pytest markers so you can run exactly the subset you need:
144+
145+
| Marker | What it covers | Typical runtime |
146+
|---|---|---|
147+
| `unit` | Mapping logic, fake-orchestrator gRPC tests (no models loaded) | ~2 s |
148+
| `integration` | Real pipeline end-to-end through the gRPC server | ~60 s+ |
149+
| `ocr` | Tests that require an OCR engine (easyocr, rapidocr, etc.) | varies |
150+
151+
```bash
152+
# Fast feedback — unit tests only (no models, no network)
153+
uv run pytest -m unit -k grpc -v
154+
155+
# Integration tests excluding OCR (avoids OCR model downloads; other models may still download)
156+
uv run pytest -m "integration and not ocr" -k grpc -v
157+
158+
# Everything, including OCR (requires an OCR extra installed)
159+
uv run pytest -m integration -k grpc -v
160+
161+
# Run a single test file
162+
uv run pytest tests/test_grpc_service_fake.py -v
163+
```
164+
165+
CI runs `unit` and `integration` (excluding `ocr`) as separate jobs so unit failures surface fast.
166+
141167
## Coding style guidelines
142168
143169
We use the following tools to enforce code style:

Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,15 @@ run-docling-rocm72: ## Run the docling-serve container with ROCm 7.2 support and
173173
-e TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1 \
174174
-e DOCLING_SERVE_ENABLE_UI=true \
175175
ghcr.io/docling-project/docling-serve-rocm72:main
176+
177+
.PHONY: test-grpc-unit
178+
test-grpc-unit: ## Run gRPC unit tests (no models)
179+
$(CMD_PREFIX) uv run pytest -m unit -k grpc -v
180+
181+
.PHONY: test-grpc-integration
182+
test-grpc-integration: ## Run gRPC integration tests (excludes OCR)
183+
$(CMD_PREFIX) uv run pytest -m "integration and not ocr" -k grpc -v
184+
185+
.PHONY: test-grpc-ocr
186+
test-grpc-ocr: ## Run gRPC OCR integration tests
187+
$(CMD_PREFIX) uv run pytest -m "integration and ocr" -k grpc -v

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@ The server is available at
3737
- API documentation <http://127.0.0.1:5001/docs>
3838
- UI playground <http://127.0.0.1:5001/ui>
3939

40+
### REST and gRPC
41+
42+
Docling Serve can expose a **REST** API (JSON over HTTP, default port **5001**) or a **gRPC** API (Protocol Buffers over HTTP/2, default port **50051**). The two servers are separate processes; use two containers if you need both. gRPC support is **experimental**; see [gRPC documentation](./docs/grpc/README.md).
43+
44+
REST uses JSON and supports `multipart/form-data` file uploads and the web UI when enabled. gRPC uses protobuf messages; file bytes are sent as **base64** in the `FileSource` message (see the [gRPC docs](./docs/grpc/README.md#file-upload-via-filesource)).
45+
46+
Example running **only** the gRPC server in Docker with a local models directory mounted (same layout as in [Handling models](./docs/models.md)):
47+
48+
```bash
49+
docker run -p 50051:50051 \
50+
-v "$(pwd)/models:/opt/app-root/src/models" \
51+
-e DOCLING_SERVE_ARTIFACTS_PATH="/opt/app-root/src/models" \
52+
quay.io/docling-project/docling-serve -- docling-serve-grpc run --host 0.0.0.0 --port 50051
53+
```
54+
4055
![API documentation](img/fastapi-ui.png)
4156

4257
Try it out with a simple conversion:

docling_serve/grpc/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""gRPC server support for docling-serve."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
from pathlib import Path
7+
8+
_GEN_PATH = Path(__file__).resolve().parent / "gen"
9+
if _GEN_PATH.exists():
10+
sys.path.insert(0, str(_GEN_PATH))

docling_serve/grpc/__main__.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import asyncio
2+
import logging
3+
import platform
4+
import sys
5+
from typing import Annotated
6+
7+
import typer
8+
from rich.console import Console
9+
10+
from docling_serve.settings import docling_serve_settings
11+
12+
from .server import serve
13+
14+
console = Console()
15+
err_console = Console(stderr=True)
16+
app = typer.Typer(no_args_is_help=True, rich_markup_mode="rich")
17+
18+
19+
@app.command()
20+
def run(
21+
host: Annotated[
22+
str,
23+
typer.Option(help="Host to bind the gRPC server."),
24+
] = "0.0.0.0",
25+
port: Annotated[
26+
int,
27+
typer.Option(help="Port to bind the gRPC server."),
28+
] = 50051,
29+
artifacts_path: Annotated[
30+
str | None,
31+
typer.Option(
32+
help=(
33+
"If set to a valid directory, the model weights will be loaded from this path."
34+
)
35+
),
36+
] = None,
37+
) -> None:
38+
if artifacts_path:
39+
docling_serve_settings.artifacts_path = artifacts_path
40+
41+
logging.basicConfig(level=logging.INFO)
42+
console.print("Starting Docling Serve gRPC server 🚀")
43+
console.print(f"Listening on [bold]{host}:{port}[/]")
44+
asyncio.run(serve(host=host, port=port))
45+
46+
47+
@app.command()
48+
def version() -> None:
49+
console.print(
50+
f"Python: {platform.python_version()} ({sys.implementation.cache_tag})"
51+
)
52+
53+
54+
def main() -> None:
55+
app()
56+
57+
58+
if __name__ == "__main__":
59+
main()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from __future__ import annotations
2+
3+
import warnings
4+
5+
from docling_core.proto import docling_document_to_proto as docling_document_to_proto
6+
from docling_core.proto.gen.ai.docling.core.v1 import docling_document_pb2 as pb2
7+
8+
# Re-export for compatibility with existing code in docling-serve
9+
__all__ = ["docling_document_to_proto", "pb2"]

docling_serve/grpc/gen/__init__.py

Whitespace-only changes.

docling_serve/grpc/gen/ai/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)