Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,51 @@ loader = DoclingLoader(file_path=FILE_PATH)
docs = loader.load()
```

### Docling Serve or Managed Docling

Pass Docling's existing `DoclingServiceClient` as the loader's converter to run
conversion remotely. The same client works with
[Docling for IBM watsonx](https://www.ibm.com/products/docling) and a self-hosted
Docling Serve endpoint:

```python
import os

from docling.datamodel.service.options import ConvertDocumentsOptions
from docling.service_client import DoclingServiceClient

from langchain_docling import DoclingLoader

service_url = os.environ["DOCLING_SERVICE_URL"]
api_key = os.environ["DOCLING_API_KEY"]
options = ConvertDocumentsOptions(
do_ocr=True,
table_mode="accurate",
)

with DoclingServiceClient(url=service_url, api_key=api_key) as client:
loader = DoclingLoader(
file_path=["https://arxiv.org/pdf/2408.09869"],
converter=client,
convert_kwargs={"options": options},
)
docs = loader.load()
```

The loader does not take ownership of a supplied client. Keep the client open
until `load()` completes or until a `lazy_load()` iterator has been fully
consumed. For a self-hosted endpoint that does not require authentication, omit
`api_key`.

### Advanced usage

When initializing a `DoclingLoader`, you can use the following parameters:

- `file_path`: source as single str (URL or local file) or iterable thereof
- `converter` (optional): any specific Docling converter instance to use
- `convert_kwargs` (optional): any specific kwargs for conversion execution
- `converter` (optional): a local `DocumentConverter`, remote
`DoclingServiceClient`, or compatible converter
- `convert_kwargs` (optional): backend-specific kwargs for conversion execution,
such as `{"options": ConvertDocumentsOptions(...)}` for the service client
- `export_type` (optional): export mode to use: `ExportType.DOC_CHUNKS` (default) or
`ExportType.MARKDOWN`
- `md_export_kwargs` (optional): any specific Markdown export kwargs (for Markdown mode)
Expand Down
3 changes: 2 additions & 1 deletion langchain_docling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
#
"""Docling LangChain package."""

from langchain_docling.loader import DoclingLoader
from langchain_docling.loader import ConversionBackend as ConversionBackend
from langchain_docling.loader import DoclingLoader as DoclingLoader
39 changes: 26 additions & 13 deletions langchain_docling/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
"""Docling LangChain loader module."""

from abc import ABC, abstractmethod
from collections.abc import Callable, Iterable, Iterator
from enum import Enum
from typing import Any, Dict, Iterable, Iterator, Optional, Union
from typing import Any, Protocol

from docling.chunking import BaseChunk, BaseChunker, HybridChunker
from docling.datamodel.document import DoclingDocument
from docling.datamodel.document import ConversionResult, DoclingDocument
from docling.document_converter import DocumentConverter
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
Expand All @@ -23,6 +24,15 @@ class ExportType(str, Enum):
DOC_CHUNKS = "doc_chunks"


class ConversionBackend(Protocol):
"""Structural interface for local and service-backed Docling conversion."""

@property
def convert(self) -> Callable[..., ConversionResult]:
"""Return the backend-specific conversion callable."""
...


class BaseMetaExtractor(ABC):
"""BaseMetaExtractor."""

Expand Down Expand Up @@ -61,24 +71,27 @@ class DoclingLoader(BaseLoader):

def __init__(
self,
file_path: Union[str, Iterable[str]],
file_path: str | Iterable[str],
*,
converter: Optional[DocumentConverter] = None,
convert_kwargs: Optional[Dict[str, Any]] = None,
converter: ConversionBackend | None = None,
convert_kwargs: dict[str, Any] | None = None,
export_type: ExportType = ExportType.DOC_CHUNKS,
md_export_kwargs: Optional[dict[str, Any]] = None,
chunker: Optional[BaseChunker] = None,
meta_extractor: Optional[BaseMetaExtractor] = None,
md_export_kwargs: dict[str, Any] | None = None,
chunker: BaseChunker | None = None,
meta_extractor: BaseMetaExtractor | None = None,
):
"""Initialize with a file path.

Args:
file_path: File source as single str (URL or local file) or Iterable
thereof.
converter: Any specific `DocumentConverter` to use. Defaults to `None` (i.e.
converter defined internally).
convert_kwargs: Any specific kwargs to pass to conversion invocation.
Defaults to `None` (i.e. behavior defined internally).
converter: A local `DocumentConverter`, remote `DoclingServiceClient`, or
compatible conversion backend. Defaults to `None` (i.e. a local
`DocumentConverter` defined internally).
convert_kwargs: Any backend-specific kwargs to pass to the conversion
invocation. For example, pass `options` when using a
`DoclingServiceClient`. Defaults to `None` (i.e. behavior defined
internally).
export_type: The type to export to: either `ExportType.MARKDOWN` (outputs
Markdown of whole input file) or `ExportType.DOC_CHUNKS` (outputs chunks
based on chunker).
Expand All @@ -97,7 +110,7 @@ def __init__(
else [file_path]
)

self._converter: DocumentConverter = converter or DocumentConverter()
self._converter: ConversionBackend = converter or DocumentConverter()
self._convert_kwargs = convert_kwargs if convert_kwargs is not None else {}
self._export_type = export_type
self._md_export_kwargs = (
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ classifiers = [
requires-python = ">=3.10,<4" # syntax without tilde to prevent uv warning
dependencies = [
"langchain-core~=1.0",
"docling~=2.26",
"docling~=2.103",
]

[project.urls]
Expand Down
33 changes: 33 additions & 0 deletions test/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import pytest
from docling.chunking import HierarchicalChunker
from docling.datamodel.document import DoclingDocument
from docling.datamodel.service.options import ConvertDocumentsOptions
from docling.service_client import DoclingServiceClient

from langchain_docling.loader import DoclingLoader, ExportType

Expand Down Expand Up @@ -79,3 +81,34 @@ def test_load_as_doc_chunks(monkeypatch: pytest.MonkeyPatch) -> None:
with open(exp_file, encoding="utf-8") as f:
exp_data = json.load(f)
assert act_data == exp_data


def test_service_client_load_preserves_chunk_metadata(
monkeypatch: pytest.MonkeyPatch,
) -> None:
file_path = "https://example.com/foo.pdf"
options = ConvertDocumentsOptions(do_ocr=False)
mock_dl_doc = DoclingDocument.load_from_json("test/data/input/dl_doc_1.json")
mock_response = MagicMock()
mock_response.document = mock_dl_doc

with DoclingServiceClient(url="https://docling.example.com") as client:
convert = MagicMock(return_value=mock_response)
monkeypatch.setattr(client, "convert", convert)
loader = DoclingLoader(
file_path=file_path,
converter=client,
convert_kwargs={"options": options},
export_type=ExportType.DOC_CHUNKS,
chunker=HierarchicalChunker(),
)

documents = loader.load()

convert.assert_called_once_with(source=file_path, options=options)
assert len(documents) == 2
assert all(document.metadata["source"] == file_path for document in documents)
assert all("dl_meta" in document.metadata for document in documents)
assert [document.page_content for document in documents] == [
chunk.text for chunk in HierarchicalChunker().chunk(mock_dl_doc)
]
Loading
Loading