Skip to content

Commit 11e4214

Browse files
author
Peter Gustafsson
committed
fix(file-processor): Preserve content in async Docling uploads.
Pass in-memory DocumentStream objects to both async Docling service operations so the original filename and complete file content are uploaded. Fixes #6393 Signed-off-by: Peter Gustafsson <peter.gustafsson6@gmail.com>
1 parent 0443aa6 commit 11e4214

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)