Skip to content

Commit 9add4db

Browse files
pgustafsPeter Gustafsson
andauthored
fix(file-processor): Preserve content in async Docling uploads (#6394)
# What does this PR do? This fixes async uploads to Docling Serve. OGX wrote the document to a temporary file and sent its path before the data was flushed. Small documents could therefore be uploaded as empty files. The temporary filename also hid the original filename and file type. This change sends the document as an in-memory DocumentStream instead. It keeps the original filename and sends all file bytes. The fix is used for both async conversion and async chunking. Fixes #6393 ## Test Plan - Run: uv run pytest -q tests/unit/providers/file_processor/test_docling_serve.py - Result: 23 passed. - Run pre-commit on the two changed files. - Result: all checks passed, including Ruff and mypy. - Run all file-processor unit tests. - Result: 87 passed and 2 skipped. Four unrelated auto-processor tests failed because the local environment detected their fake DOCX and XLSX data as an unknown MIME type. The full pre-commit run was also checked. All applicable checks passed except two API coverage hooks because oasdiff is not installed locally. Signed-off-by: Peter Gustafsson <peter.gustafsson6@gmail.com> Co-authored-by: Peter Gustafsson <peter.gustafsson6@gmail.com>
1 parent 2830b82 commit 9add4db

2 files changed

Lines changed: 67 additions & 65 deletions

File tree

src/ogx/providers/remote/file_processor/docling_serve/docling_serve.py

Lines changed: 57 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
# the root directory of this source tree.
66

77
import os
8-
import tempfile
98
import time
109
import uuid
11-
from pathlib import Path
10+
from io import BytesIO
1211
from typing import Any
1312

1413
import httpx
1514
from docling.datamodel.base_models import OutputFormat
1615
from docling.datamodel.service.options import ConvertDocumentsOptions
1716
from docling.service_client import AsyncDoclingServiceClient, ChunkerKind
17+
from docling_core.types.io import DocumentStream
1818
from fastapi import UploadFile
1919

2020
from ogx.log import get_logger
@@ -222,37 +222,33 @@ async def _convert_no_chunk_async(
222222
document_metadata: dict[str, Any],
223223
) -> list[Chunk]:
224224
"""Convert file using async endpoints with AsyncDoclingServiceClient."""
225-
# AsyncDoclingServiceClient requires a file path via temp file
226-
with tempfile.NamedTemporaryFile() as tmp:
227-
tmp.write(content)
228-
tmp_path = Path(tmp.name)
229-
230-
async with AsyncDoclingServiceClient(
231-
url=self.config.base_url,
232-
api_key=self.config.api_key.get_secret_value() if self.config.api_key else "",
233-
job_timeout=300.0,
234-
) as client:
235-
job = await client.submit(
236-
source=tmp_path,
237-
options=ConvertDocumentsOptions(to_formats=[OutputFormat.MARKDOWN]),
238-
)
239-
result = await job.result()
240-
241-
# Handle both local docling-serve (ConversionResult with .document)
242-
# and IBM SaaS (PresignedUrlConvertResponse with .documents and presigned URLs)
243-
md_content = ""
244-
if hasattr(result, "documents"):
245-
# IBM SaaS: PresignedUrlConvertResponse with presigned URLs
246-
if result.documents and result.documents[0].artifacts:
247-
artifact = result.documents[0].artifacts[0]
248-
# Download markdown from presigned URL
249-
async with httpx.AsyncClient() as http_client:
250-
response = await http_client.get(str(artifact.uri))
251-
response.raise_for_status()
252-
md_content = response.text
253-
elif hasattr(result, "document"):
254-
# Local docling-serve: ConversionResult with direct document
255-
md_content = result.document.export_to_markdown() if result.document else ""
225+
source = DocumentStream(name=filename, stream=BytesIO(content))
226+
async with AsyncDoclingServiceClient(
227+
url=self.config.base_url,
228+
api_key=self.config.api_key.get_secret_value() if self.config.api_key else "",
229+
job_timeout=300.0,
230+
) as client:
231+
job = await client.submit(
232+
source=source,
233+
options=ConvertDocumentsOptions(to_formats=[OutputFormat.MARKDOWN]),
234+
)
235+
result = await job.result()
236+
237+
# Handle both local docling-serve (ConversionResult with .document)
238+
# and IBM SaaS (PresignedUrlConvertResponse with .documents and presigned URLs)
239+
md_content = ""
240+
if hasattr(result, "documents"):
241+
# IBM SaaS: PresignedUrlConvertResponse with presigned URLs
242+
if result.documents and result.documents[0].artifacts:
243+
artifact = result.documents[0].artifacts[0]
244+
# Download markdown from presigned URL
245+
async with httpx.AsyncClient() as http_client:
246+
response = await http_client.get(str(artifact.uri))
247+
response.raise_for_status()
248+
md_content = response.text
249+
elif hasattr(result, "document"):
250+
# Local docling-serve: ConversionResult with direct document
251+
md_content = result.document.export_to_markdown() if result.document else ""
256252

257253
if not md_content or not md_content.strip():
258254
return []
@@ -374,39 +370,35 @@ async def _convert_and_chunk_async(
374370
document_metadata: dict[str, Any],
375371
) -> list[Chunk]:
376372
"""Convert and chunk file using async endpoints with AsyncDoclingServiceClient."""
377-
# AsyncDoclingServiceClient requires a file path via temp file
378-
with tempfile.NamedTemporaryFile() as tmp:
379-
tmp.write(content)
380-
tmp_path = Path(tmp.name)
381-
382-
async with AsyncDoclingServiceClient(
383-
url=self.config.base_url,
384-
api_key=self.config.api_key.get_secret_value() if self.config.api_key else "",
385-
job_timeout=300.0,
386-
) as client:
387-
try:
388-
job = await client.submit_chunk(
389-
source=tmp_path,
390-
chunker=ChunkerKind.HYBRID,
391-
options=ConvertDocumentsOptions(),
392-
)
393-
response = await job.result()
394-
except httpx.HTTPStatusError as e:
395-
# Chunking endpoint not supported (e.g., IBM Docling SaaS)
396-
if e.response.status_code in (404, 405):
397-
raise InvalidParameterError(
398-
param_name="chunking_strategy",
399-
value=chunking_strategy.model_dump() if chunking_strategy else None,
400-
constraint=(
401-
"Chunking is not supported by this Docling instance. "
402-
"This is a known limitation of IBM Docling SaaS. "
403-
"Either remove 'chunking_strategy' from your request, "
404-
"or configure OGX to use local docling-serve for chunking support."
405-
),
406-
) from e
407-
raise
373+
source = DocumentStream(name=filename, stream=BytesIO(content))
374+
async with AsyncDoclingServiceClient(
375+
url=self.config.base_url,
376+
api_key=self.config.api_key.get_secret_value() if self.config.api_key else "",
377+
job_timeout=300.0,
378+
) as client:
379+
try:
380+
job = await client.submit_chunk(
381+
source=source,
382+
chunker=ChunkerKind.HYBRID,
383+
options=ConvertDocumentsOptions(),
384+
)
385+
response = await job.result()
386+
except httpx.HTTPStatusError as e:
387+
# Chunking endpoint not supported (e.g., IBM Docling SaaS)
388+
if e.response.status_code in (404, 405):
389+
raise InvalidParameterError(
390+
param_name="chunking_strategy",
391+
value=chunking_strategy.model_dump() if chunking_strategy else None,
392+
constraint=(
393+
"Chunking is not supported by this Docling instance. "
394+
"This is a known limitation of IBM Docling SaaS. "
395+
"Either remove 'chunking_strategy' from your request, "
396+
"or configure OGX to use local docling-serve for chunking support."
397+
),
398+
) from e
399+
raise
408400

409-
raw_chunks = response.chunks if response.chunks else []
401+
raw_chunks = response.chunks if response.chunks else []
410402

411403
if not raw_chunks:
412404
return []

tests/unit/providers/file_processor/test_docling_serve.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import httpx
1313
import pytest
14+
from docling_core.types.io import DocumentStream
1415
from fastapi import UploadFile
1516
from pydantic import SecretStr
1617

@@ -448,6 +449,10 @@ async def test_ibm_saas_allows_conversion_without_chunking(
448449
assert result.chunks is not None
449450
assert len(result.chunks) > 0
450451
assert result.metadata["conversion_method"] == "async"
452+
source = mock_instance.submit.await_args.kwargs["source"]
453+
assert isinstance(source, DocumentStream)
454+
assert source.name == "test.pdf"
455+
assert source.stream.getvalue() == b"%PDF-fake-content"
451456

452457
async def test_local_docker_allows_chunking(self, upload_file: UploadFile):
453458
"""Local docling-serve should allow chunking (successful response)."""
@@ -481,6 +486,11 @@ async def test_local_docker_allows_chunking(self, upload_file: UploadFile):
481486
# Should succeed without raising InvalidParameterError
482487
result = await processor.process_file(request, file=upload_file)
483488

489+
submit_kwargs = mock_instance.submit_chunk.await_args.kwargs
490+
source = submit_kwargs["source"]
491+
assert isinstance(source, DocumentStream)
492+
assert source.name == "test.pdf"
493+
assert source.stream.getvalue() == b"%PDF-fake-content"
484494
assert result.chunks is not None
485495
assert len(result.chunks) > 0
486496
assert result.metadata["conversion_method"] == "async"

0 commit comments

Comments
 (0)