diff --git a/docs/docs/Components/bundles-docling.mdx b/docs/docs/Components/bundles-docling.mdx index 16d750d186..df675756a2 100644 --- a/docs/docs/Components/bundles-docling.mdx +++ b/docs/docs/Components/bundles-docling.mdx @@ -25,8 +25,9 @@ The Docling dependency is required to use the Docling components in Langflow. * **Langflow Desktop**: Set `LANGFLOW_DOCLING=True` in your `.env` file to enable Docling dependency installation. For more information, see [Set environment variables for Langflow Desktop](/environment-variables#set-environment-variables-for-langflow-desktop). * **Langflow OSS**: - * If you installed `langflow` (`uv pip install langflow`), Docling is included automatically through bundled extras. + * If you installed `langflow` (`uv pip install langflow`), Docling is included automatically through bundled extras, without Docling's optional chunking dependencies. * If you installed `langflow-base` directly, install Docling with an extra, for example `uv pip install "langflow-base[docling]"`. + * To use the **Chunk DoclingDocument** component, install the chunking extra with `uv pip install "langflow[docling-chunking]"` or `uv pip install "langflow-base[docling-chunking]"`. * **macOS Intel (x86_64)**: Use the [Docling installation guide](https://docling-project.github.io/docling/installation/) to install the Docling dependency. @@ -103,6 +104,8 @@ For more information, see the [Docling serve project repository](https://github. The **Chunk DoclingDocument** component splits `DoclingDocument` objects into chunks. +This component requires Docling's optional chunking dependencies. Install them with `uv pip install "langflow[docling-chunking]"` or `uv pip install "langflow-base[docling-chunking]"`. + It outputs the chunked documents as a [`Table`](/data-types#table). For more information, see the [Docling core project repository](https://github.com/docling-project/docling-core). diff --git a/pyproject.toml b/pyproject.toml index ce4b603b1d..5c7a4fb8d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,7 @@ Documentation = "https://docs.langflow.org" [project.optional-dependencies] docling = [ - "langchain-docling>=1.1.0", + "langflow-base[docling]>=0.9.5", "tesserocr>=2.8.0", "rapidocr-onnxruntime>=1.4.4", "ocrmac>=1.0.0; sys_platform == 'darwin'", @@ -120,6 +120,16 @@ docling = [ "torchvision>=0.21.0", # torchvision 0.21+ is compatible with PyTorch 2.6+ ] +docling-chunking = [ + "langflow-base[docling-chunking]>=0.9.5", +] + +docling-image-description = [ + "langflow-base[docling-image-description]>=0.9.5", + "langchain-docling>=1.1.0", +] + + audio = [ "webrtcvad>=2.0.10", ] diff --git a/src/backend/base/pyproject.toml b/src/backend/base/pyproject.toml index 77ad8cd279..6bb38e0f42 100644 --- a/src/backend/base/pyproject.toml +++ b/src/backend/base/pyproject.toml @@ -355,9 +355,19 @@ openlayer = ["openlayer>=0.9.0,<1.0.0"] # Document processing / OCR docling = [ - "docling-core>=2.36.1,<3.0.0", - "docling>=2.36.1,<3.0.0; sys_platform != 'darwin' or platform_machine != 'x86_64'", + "docling-core>=2.77.0,<3.0.0", + "docling-slim[cli,convert-core,extract-core,feat-ocr-rapidocr,format-latex,format-office,format-pdf,format-web,models-local,service-client]>=2.36.1,<3.0.0; sys_platform != 'darwin' or platform_machine != 'x86_64'", ] +docling-chunking = [ + "langflow-base[docling]", + "docling-core[chunking]>=2.77.0,<3.0.0", + "tiktoken>=0.7.0", +] +docling-image-description = [ + "langflow-base[docling]", + "langchain-docling>=1.1.0", +] + easyocr = ["easyocr>=1.7.2,<2.0.0; sys_platform != 'darwin' or platform_machine != 'x86_64'"] # Additional tools diff --git a/src/backend/tests/unit/components/docling/test_chunk_docling_document_component.py b/src/backend/tests/unit/components/docling/test_chunk_docling_document_component.py index ca2928f7b0..5d13fa805b 100644 --- a/src/backend/tests/unit/components/docling/test_chunk_docling_document_component.py +++ b/src/backend/tests/unit/components/docling/test_chunk_docling_document_component.py @@ -1,14 +1,14 @@ """Tests for ChunkDoclingDocumentComponent HybridChunker parameters.""" +import builtins import sys import types import pytest - -pytest.importorskip("tiktoken") -pytest.importorskip("docling_core") - -from lfx.components.docling.chunk_docling_document import ChunkDoclingDocumentComponent +from lfx.components.docling.chunk_docling_document import ( + ChunkDoclingDocumentComponent, + _load_docling_chunker_dependencies, +) def _base_build_config(): @@ -95,6 +95,11 @@ class TestChunkDoclingDocumentComponentHybridChunker: captured["max_tokens"] = max_tokens return "tokenizer" + class DummyDocMeta: + @classmethod + def model_validate(cls, meta): + return meta + hybrid_chunker_module = types.ModuleType("docling_core.transforms.chunker.hybrid_chunker") hybrid_chunker_module.HybridChunker = DummyHybridChunker monkeypatch.setitem(sys.modules, "docling_core.transforms.chunker.hybrid_chunker", hybrid_chunker_module) @@ -110,8 +115,8 @@ class TestChunkDoclingDocumentComponentHybridChunker: huggingface_tokenizer_module, ) monkeypatch.setattr( - "lfx.components.docling.chunk_docling_document.HierarchicalChunker", - DummyHierarchicalChunker, + "lfx.components.docling.chunk_docling_document._load_docling_chunker_dependencies", + lambda: (DummyDocMeta, DummyHierarchicalChunker), ) monkeypatch.setattr( "lfx.components.docling.chunk_docling_document.extract_docling_documents", @@ -182,3 +187,20 @@ class TestChunkDoclingDocumentComponentHybridChunker: always_emit_headings_input=True, ) assert captured["hierarchical_called"] is True + + def test_missing_chunking_extra_has_actionable_error(self, monkeypatch): + original_import = builtins.__import__ + + def fake_import(name, globals_=None, locals_=None, fromlist=(), level=0): + if name in { + "docling_core.transforms.chunker.doc_chunk", + "docling_core.transforms.chunker.hierarchical_chunker", + }: + msg = "Module requires 'chunking' extra" + raise RuntimeError(msg) + return original_import(name, globals_, locals_, fromlist, level) + + monkeypatch.setattr(builtins, "__import__", fake_import) + + with pytest.raises(ImportError, match=r"langflow-base\[docling-chunking\]"): + _load_docling_chunker_dependencies() diff --git a/src/lfx/src/lfx/base/data/docling_utils.py b/src/lfx/src/lfx/base/data/docling_utils.py index 7a96e3a61e..9f9fd814b7 100644 --- a/src/lfx/src/lfx/base/data/docling_utils.py +++ b/src/lfx/src/lfx/base/data/docling_utils.py @@ -292,7 +292,6 @@ def docling_worker( from docling.document_converter import DocumentConverter, FormatOption, PdfFormatOption # noqa: F401 from docling.models.factories import get_ocr_factory # noqa: F401 from docling.pipeline.vlm_pipeline import VlmPipeline # noqa: F401 - from langchain_docling.picture_description import PictureDescriptionLangChainOptions # noqa: F401 # Check for shutdown after imports check_shutdown() @@ -332,7 +331,6 @@ def docling_worker( from docling.datamodel.pipeline_options import PdfPipelineOptions from docling.document_converter import DocumentConverter, FormatOption, PdfFormatOption from docling.models.factories import get_ocr_factory - from langchain_docling.picture_description import PictureDescriptionLangChainOptions pipeline_options = PdfPipelineOptions() pipeline_options.do_ocr = ocr_engine not in {"", "None"} @@ -342,6 +340,14 @@ def docling_worker( pipeline_options.ocr_options = ocr_options pipeline_options.do_picture_classification = do_picture_classification + try: + from langchain_docling.picture_description import PictureDescriptionLangChainOptions + except ImportError as e: + msg = ( + "langchain-docling is not installed. Please install it with `pip install langchain-docling` " + "or `pip install 'langflow[docling-image-description]'`." + ) + raise ImportError(msg) from e pic_desc_llm = _deserialize_pydantic_model(pic_desc_config) logger.info("Docling enabling the picture description stage.") pipeline_options.do_picture_description = True diff --git a/src/lfx/src/lfx/components/docling/chunk_docling_document.py b/src/lfx/src/lfx/components/docling/chunk_docling_document.py index 4137bf75f3..e569d82000 100644 --- a/src/lfx/src/lfx/components/docling/chunk_docling_document.py +++ b/src/lfx/src/lfx/components/docling/chunk_docling_document.py @@ -1,14 +1,27 @@ import json - -import tiktoken -from docling_core.transforms.chunker import BaseChunker, DocMeta -from docling_core.transforms.chunker.hierarchical_chunker import HierarchicalChunker +from typing import Any from lfx.base.data.docling_utils import extract_docling_documents from lfx.custom import Component from lfx.io import BoolInput, DropdownInput, HandleInput, IntInput, MessageTextInput, Output, StrInput from lfx.schema import Data, DataFrame +_CHUNKING_INSTALL_HINT = ( + "Install them with `uv pip install 'langflow[docling-chunking]'`, " + "`uv pip install 'langflow-base[docling-chunking]'`, or " + "`uv pip install 'docling-core[chunking]' tiktoken`." +) + + +def _load_docling_chunker_dependencies() -> tuple[type[Any], type[Any]]: + try: + from docling_core.transforms.chunker.doc_chunk import DocMeta as DocMetaCls + from docling_core.transforms.chunker.hierarchical_chunker import HierarchicalChunker as HierarchicalChunkerCls + except (ImportError, RuntimeError) as e: + msg = f"Docling chunking dependencies are not installed. {_CHUNKING_INSTALL_HINT}" + raise ImportError(msg) from e + return DocMetaCls, HierarchicalChunkerCls + class ChunkDoclingDocumentComponent(Component): display_name: str = "Chunk DoclingDocument" @@ -144,25 +157,20 @@ class ChunkDoclingDocumentComponent(Component): if warning: self.status = warning - chunker: BaseChunker + doc_meta_cls, hierarchical_chunker_cls = _load_docling_chunker_dependencies() + chunker: Any if self.chunker == "HybridChunker": try: from docling_core.transforms.chunker.hybrid_chunker import HybridChunker - except ImportError as e: - msg = ( - "HybridChunker is not installed. Please install it with `uv pip install docling-core[chunking] " - "or `uv pip install transformers`" - ) + except (ImportError, RuntimeError) as e: + msg = f"HybridChunker is not installed. {_CHUNKING_INSTALL_HINT}" raise ImportError(msg) from e max_tokens: int | None = self.max_tokens if self.max_tokens else None if self.provider == "Hugging Face": try: from docling_core.transforms.chunker.tokenizer.huggingface import HuggingFaceTokenizer - except ImportError as e: - msg = ( - "HuggingFaceTokenizer is not installed." - " Please install it with `uv pip install docling-core[chunking]`" - ) + except (ImportError, RuntimeError) as e: + msg = f"HuggingFaceTokenizer is not installed. {_CHUNKING_INSTALL_HINT}" raise ImportError(msg) from e tokenizer = HuggingFaceTokenizer.from_pretrained( model_name=self.hf_model_name, @@ -170,13 +178,10 @@ class ChunkDoclingDocumentComponent(Component): ) elif self.provider == "OpenAI": try: + import tiktoken from docling_core.transforms.chunker.tokenizer.openai import OpenAITokenizer - except ImportError as e: - msg = ( - "OpenAITokenizer is not installed." - " Please install it with `uv pip install docling-core[chunking]`" - " or `uv pip install transformers`" - ) + except (ImportError, RuntimeError) as e: + msg = f"OpenAITokenizer is not installed. {_CHUNKING_INSTALL_HINT}" raise ImportError(msg) from e if max_tokens is None: max_tokens = 128 * 1024 # context window length required for OpenAI tokenizers @@ -190,7 +195,7 @@ class ChunkDoclingDocumentComponent(Component): ) elif self.chunker == "HierarchicalChunker": - chunker = HierarchicalChunker() + chunker = hierarchical_chunker_cls() else: msg = f"Unknown chunker: {self.chunker}" raise ValueError(msg) @@ -200,7 +205,7 @@ class ChunkDoclingDocumentComponent(Component): for doc in documents: for chunk in chunker.chunk(dl_doc=doc): enriched_text = chunker.contextualize(chunk=chunk) - meta = DocMeta.model_validate(chunk.meta) + meta = doc_meta_cls.model_validate(chunk.meta) results.append( Data( diff --git a/src/lfx/src/lfx/components/docling/docling_inline.py b/src/lfx/src/lfx/components/docling/docling_inline.py index ffcbaa5e02..d1b5e50f45 100644 --- a/src/lfx/src/lfx/components/docling/docling_inline.py +++ b/src/lfx/src/lfx/components/docling/docling_inline.py @@ -140,9 +140,18 @@ class DoclingInlineComponent(BaseFileComponent): try: import importlib from pydantic import TypeAdapter - from langchain_docling.picture_description import ( - PictureDescriptionLangChainOptions, - ) + try: + from langchain_docling.picture_description import PictureDescriptionLangChainOptions + except ImportError as e: + print(json.dumps({ + "ok": False, + "error": ( + "langchain-docling is not installed. Please install it with " + "`pip install langchain-docling` or " + "`pip install 'langflow[docling-image-description]'`." + ) + })) + return mod_name, cls_name = pic_desc_config["__class_path__"].rsplit(".", 1) mod = importlib.import_module(mod_name) cls = getattr(mod, cls_name) @@ -157,6 +166,7 @@ class DoclingInlineComponent(BaseFileComponent): print(json.dumps({"ok": False, "error": f"Picture description setup failed: {e}"})) return + if pipeline == "vlm": try: from docling.datamodel.pipeline_options import VlmPipelineOptions