From caa10b8577068b2e1e06b8a5bbb885e20b0ba4f2 Mon Sep 17 00:00:00 2001 From: anakin87 Date: Thu, 14 Aug 2025 11:25:04 +0200 Subject: [PATCH 1/9] draft --- integrations/cohere/pyproject.toml | 6 +- .../components/embedders/cohere/__init__.py | 3 +- .../embedders/cohere/document_embedder.py | 2 +- .../cohere/document_image_embedder.py | 270 ++++++++++++++++++ .../embedders/cohere/text_embedder.py | 2 +- .../cohere/tests/test_files/apple.jpg | Bin 0 -> 69286 bytes 6 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py create mode 100644 integrations/cohere/tests/test_files/apple.jpg diff --git a/integrations/cohere/pyproject.toml b/integrations/cohere/pyproject.toml index cb3a9ecc6a..353d63c38a 100644 --- a/integrations/cohere/pyproject.toml +++ b/integrations/cohere/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] -dependencies = ["haystack-ai>=2.15.1", "cohere>=5.16.0"] +dependencies = ["haystack-ai>=2.16.1", "cohere>=5.16.0"] [project.urls] Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/cohere#readme" @@ -57,7 +57,9 @@ dependencies = [ "pytest-cov", "pytest-rerunfailures", "mypy", - "pip" + "pip", + "pillow", # image support + "pypdfium2" # image support ] [tool.hatch.envs.test.scripts] diff --git a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/__init__.py b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/__init__.py index 73a863a738..82da826f4d 100644 --- a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/__init__.py +++ b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/__init__.py @@ -3,5 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 from .document_embedder import CohereDocumentEmbedder from .text_embedder import CohereTextEmbedder +from .document_image_embedder import CohereDocumentImageEmbedder -__all__ = ["CohereDocumentEmbedder", "CohereTextEmbedder"] +__all__ = ["CohereDocumentEmbedder", "CohereTextEmbedder", "CohereDocumentImageEmbedder"] diff --git a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_embedder.py b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_embedder.py index 03869b276f..ea98849817 100644 --- a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_embedder.py +++ b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_embedder.py @@ -22,7 +22,7 @@ class CohereDocumentEmbedder: Usage example: ```python from haystack import Document - from cohere_haystack.embedders.document_embedder import CohereDocumentEmbedder + from haystack_integrations.components.embedders.cohere import CohereDocumentEmbedder doc = Document(content="I love pizza!") diff --git a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py new file mode 100644 index 0000000000..6fdb4ced8c --- /dev/null +++ b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py @@ -0,0 +1,270 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import replace +from typing import Any, Literal, Optional, Tuple +from tqdm import tqdm + +from haystack.dataclasses import ByteStream +from haystack import Document, component, default_from_dict, default_to_dict, logging +from haystack.components.converters.image.image_utils import ( + _batch_convert_pdf_pages_to_images, + _extract_image_sources_info, + _PDFPageInfo, + _encode_image_to_base64 +) +from haystack.lazy_imports import LazyImport +from haystack.utils.auth import Secret, deserialize_secrets_inplace +from haystack.utils.device import ComponentDevice +from haystack.utils.hf import deserialize_hf_model_kwargs, serialize_hf_model_kwargs + + +from cohere import AsyncClientV2, ClientV2 + +from .embedding_types import EmbeddingTypes +from .utils import get_async_response, get_response + +# PDF is not officially supported, but we convert PDFs to JPEG images +SUPPORTED_IMAGE_MIME_TYPES = ["image/jpeg", "image/png", "application/pdf"] + +logger = logging.getLogger(__name__) + +@component +class CohereDocumentImageEmbedder: + """ + A component for computing Document embeddings based on images using Cohere models. + + The embedding of each Document is stored in the `embedding` field of the Document. + + ### Usage example + ```python + from haystack import Document + from haystack_integrations.components.embedders.cohere import CohereDocumentImageEmbedder + + embedder = CohereDocumentImageEmbedder(model="embed-v4.0") + + documents = [ + Document(content="A photo of a cat", meta={"file_path": "cat.jpg"}), + Document(content="A photo of a dog", meta={"file_path": "dog.jpg"}), + ] + + result = embedder.run(documents=documents) + documents_with_embeddings = result["documents"] + print(documents_with_embeddings) + + # [Document(id=..., + # content='A photo of a cat', + # meta={'file_path': 'cat.jpg', + # 'embedding_source': {'type': 'image', 'file_path_meta_field': 'file_path'}}, + # embedding=vector of size 1536), + # ...] + ``` + """ + + def __init__( + self, + *, + file_path_meta_field: str = "file_path", + root_path: Optional[str] = None, + image_size: Optional[Tuple[int, int]] = None, + api_key: Secret = Secret.from_env_var(["COHERE_API_KEY", "CO_API_KEY"]), + model: str = "embed-v4.0", + api_base_url: str = "https://api.cohere.com", + timeout: int = 120, + embedding_dimension: Optional[int] = None, + embedding_type: EmbeddingTypes = EmbeddingTypes.FLOAT, + progress_bar: bool = True, + ) -> None: + """ + Creates a CohereDocumentImageEmbedder component. + + :param file_path_meta_field: + The metadata field in the Document that contains the file path to the image or PDF. + :param root_path: + The root directory path where document files are located. If provided, file paths in + document metadata will be resolved relative to this path. If None, file paths are treated as absolute paths. + :param image_size: + If provided, resizes the image to fit within the specified dimensions (width, height) while + maintaining aspect ratio. This reduces file size, memory usage, and processing time, which is beneficial + when working with models that have resolution constraints or when transmitting images to remote services. + :param api_key: + The Cohere API key. + :param model: + The Cohere model to use for calculating embeddings. + Read [Cohere documentation](https://docs.cohere.com/docs/models#embed) for a list of all supported models. + :param api_base_url: + The Cohere API base URL. + :param timeout: + Request timeout in seconds. + :param embedding_dimension: + The dimension of the embeddings to return. Only valid for v4 and newer models. + Read [Cohere API reference](https://docs.cohere.com/reference/embed) for a list possible values and + supported models. + :param embedding_type: + The type of embeddings to return. Defaults to float embeddings. + Note that int8, uint8, binary, and ubinary are only valid for v3 models. + :param progress_bar: + Whether to show a progress bar or not. Can be helpful to disable in production deployments + to keep the logs clean. + """ + + self.file_path_meta_field = file_path_meta_field + self.root_path = root_path or "" + self.image_size = image_size + self.model = model + self.embedding_dimension = embedding_dimension + self.embedding_type = embedding_type + self.progress_bar = progress_bar + + self._api_key = api_key + self._api_base_url = api_base_url + self._timeout = timeout + + self._client = ClientV2( + api_key=self._api_key.resolve_value(), + base_url=self._api_base_url, + timeout=self._timeout, + client_name="haystack", + ) + self._async_client = AsyncClientV2( + api_key=self._api_key.resolve_value(), + base_url=self._api_base_url, + timeout=self._timeout, + client_name="haystack", + ) + + def to_dict(self) -> dict[str, Any]: + """ + Serializes the component to a dictionary. + + :returns: + Dictionary with serialized data. + """ + serialization_dict = default_to_dict( + self, + file_path_meta_field=self.file_path_meta_field, + root_path=self.root_path, + image_size=self.image_size, + model=self.model, + progress_bar=self.progress_bar, + api_key=self._api_key.to_dict(), + api_base_url=self._api_base_url, + timeout=self._timeout, + embedding_dimension=self.embedding_dimension, + embedding_type=self.embedding_type.value, + ) + return serialization_dict + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "CohereDocumentImageEmbedder": + """ + Deserializes the component from a dictionary. + + :param data: + Dictionary to deserialize from. + :returns: + Deserialized component. + """ + deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"]) + return default_from_dict(cls, data) + + + @component.output_types(documents=list[Document]) + def run(self, documents: list[Document]) -> dict[str, list[Document]]: + """ + Embed a list of documents. + + :param documents: + Documents to embed. + + :returns: + A dictionary with the following keys: + - `documents`: Documents with embeddings. + + :raises TypeError: + If the input is not a list of `Documents`. + :raises ValueError: + If the input contains unsupported image MIME types. + """ + if not isinstance(documents, list) or (documents and not isinstance(documents[0], Document)): + msg = ( + "CohereDocumentImageEmbedder expects a list of Documents as input. " + "In case you want to embed a string, please use the CohereTextEmbedder." + ) + raise TypeError(msg) + + images_source_info = _extract_image_sources_info( + documents=documents, file_path_meta_field=self.file_path_meta_field, root_path=self.root_path + ) + + for img_info in images_source_info: + if img_info["mime_type"] not in SUPPORTED_IMAGE_MIME_TYPES: + msg = (f"Unsupported image MIME type: {img_info['mime_type']}. " + f"Supported types are: {', '.join(SUPPORTED_IMAGE_MIME_TYPES)}") + raise ValueError(msg) + + images_to_embed: list[Optional[str]] = [None] * len(documents) + pdf_page_infos: list[_PDFPageInfo] = [] + + for doc_idx, image_source_info in enumerate(images_source_info): + if image_source_info["mime_type"] == "application/pdf": + # Store PDF documents for later processing + page_number = image_source_info.get("page_number") + assert page_number is not None # checked in _extract_image_sources_info but mypy doesn't know that + pdf_page_info: _PDFPageInfo = { + "doc_idx": doc_idx, + "path": image_source_info["path"], + "page_number": page_number, + } + pdf_page_infos.append(pdf_page_info) + else: + # Process images directly + image_byte_stream = ByteStream.from_file_path(filepath=image_source_info["path"], + mime_type=image_source_info["mime_type"]) + mime_type, base64_image = _encode_image_to_base64(bytestream=image_byte_stream, size=self.image_size) + images_to_embed[doc_idx] = f"data:{mime_type};base64,{base64_image}" + + base64_jpeg_images_by_doc_idx = _batch_convert_pdf_pages_to_images(pdf_page_infos=pdf_page_infos, return_base64=True, + size=self.image_size) + for doc_idx, base64_jpeg_image in base64_jpeg_images_by_doc_idx.items(): + images_to_embed[doc_idx] = f"data:image/jpeg;base64,{base64_jpeg_image}" + + none_images_doc_ids = [documents[doc_idx].id for doc_idx, image in enumerate(images_to_embed) if image is None] + if none_images_doc_ids: + msg = f"Conversion failed for some documents. Document IDs: {none_images_doc_ids}." + raise RuntimeError(msg) + + embeddings = [] + + # The Cohere API only supports passing one image at a time + for image in tqdm(images_to_embed, desc="Embedding images", disable=not self.progress_bar): + + try: + response = self._client.embed( + model=self.model, + images=[image], + input_type="image", + output_dimension=self.embedding_dimension, + embedding_types=[self.embedding_type.value], + ) + + embedding = getattr(response.embeddings, self.embedding_type.value)[0] + except Exception as e: + msg = f"Error embedding image: {e}" + logger.warning(msg) + embedding = None + + embeddings.append(embedding) + + docs_with_embeddings = [] + for doc, emb in zip(documents, embeddings): + # we store this information for later inspection + new_meta = { + **doc.meta, + "embedding_source": {"type": "image", "file_path_meta_field": self.file_path_meta_field}, + } + new_doc = replace(doc, meta=new_meta, embedding=emb) + docs_with_embeddings.append(new_doc) + + return {"documents": docs_with_embeddings} \ No newline at end of file diff --git a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/text_embedder.py b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/text_embedder.py index b159c234fc..e593589817 100644 --- a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/text_embedder.py +++ b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/text_embedder.py @@ -19,7 +19,7 @@ class CohereTextEmbedder: Usage example: ```python - from haystack_integrations.components.embedders.cohere import CohereDocumentEmbedder + from haystack_integrations.components.embedders.cohere import CohereTextEmbedder text_to_embed = "I love pizza!" diff --git a/integrations/cohere/tests/test_files/apple.jpg b/integrations/cohere/tests/test_files/apple.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f9023fea2cf935f2786880010e662074495b3eec GIT binary patch literal 69286 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

3q(kEeHIL}j@>1~3Iwh(vpa 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 Date: Thu, 14 Aug 2025 13:10:27 +0200 Subject: [PATCH 2/9] improvements + tests --- .../components/embedders/cohere/__init__.py | 4 +- .../cohere/document_image_embedder.py | 55 ++-- integrations/cohere/tests/conftest.py | 8 + .../tests/test_document_image_embedder.py | 286 ++++++++++++++++++ .../cohere/tests/test_files/sample_pdf_1.pdf | Bin 0 -> 44524 bytes 5 files changed, 325 insertions(+), 28 deletions(-) create mode 100644 integrations/cohere/tests/conftest.py create mode 100644 integrations/cohere/tests/test_document_image_embedder.py create mode 100644 integrations/cohere/tests/test_files/sample_pdf_1.pdf diff --git a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/__init__.py b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/__init__.py index 82da826f4d..ebedea6012 100644 --- a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/__init__.py +++ b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/__init__.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 from .document_embedder import CohereDocumentEmbedder -from .text_embedder import CohereTextEmbedder from .document_image_embedder import CohereDocumentImageEmbedder +from .text_embedder import CohereTextEmbedder -__all__ = ["CohereDocumentEmbedder", "CohereTextEmbedder", "CohereDocumentImageEmbedder"] +__all__ = ["CohereDocumentEmbedder", "CohereDocumentImageEmbedder", "CohereTextEmbedder"] diff --git a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py index 6fdb4ced8c..58d7af6370 100644 --- a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py +++ b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py @@ -3,33 +3,30 @@ # SPDX-License-Identifier: Apache-2.0 from dataclasses import replace -from typing import Any, Literal, Optional, Tuple -from tqdm import tqdm +from typing import Any, Optional, Tuple -from haystack.dataclasses import ByteStream from haystack import Document, component, default_from_dict, default_to_dict, logging from haystack.components.converters.image.image_utils import ( _batch_convert_pdf_pages_to_images, + _encode_image_to_base64, _extract_image_sources_info, _PDFPageInfo, - _encode_image_to_base64 ) -from haystack.lazy_imports import LazyImport +from haystack.dataclasses import ByteStream from haystack.utils.auth import Secret, deserialize_secrets_inplace -from haystack.utils.device import ComponentDevice -from haystack.utils.hf import deserialize_hf_model_kwargs, serialize_hf_model_kwargs - +from tqdm import tqdm from cohere import AsyncClientV2, ClientV2 from .embedding_types import EmbeddingTypes -from .utils import get_async_response, get_response # PDF is not officially supported, but we convert PDFs to JPEG images SUPPORTED_IMAGE_MIME_TYPES = ["image/jpeg", "image/png", "application/pdf"] + logger = logging.getLogger(__name__) + @component class CohereDocumentImageEmbedder: """ @@ -70,8 +67,8 @@ def __init__( image_size: Optional[Tuple[int, int]] = None, api_key: Secret = Secret.from_env_var(["COHERE_API_KEY", "CO_API_KEY"]), model: str = "embed-v4.0", - api_base_url: str = "https://api.cohere.com", - timeout: int = 120, + api_base_url: str = "https://api.cohere.com", + timeout: int = 120, embedding_dimension: Optional[int] = None, embedding_type: EmbeddingTypes = EmbeddingTypes.FLOAT, progress_bar: bool = True, @@ -87,7 +84,7 @@ def __init__( :param image_size: If provided, resizes the image to fit within the specified dimensions (width, height) while maintaining aspect ratio. This reduces file size, memory usage, and processing time, which is beneficial - when working with models that have resolution constraints or when transmitting images to remote services. + when working with models that have resolution constraints or when transmitting images to remote services. :param api_key: The Cohere API key. :param model: @@ -110,7 +107,7 @@ def __init__( """ self.file_path_meta_field = file_path_meta_field - self.root_path = root_path or "" + self.root_path = root_path or "" self.image_size = image_size self.model = model self.embedding_dimension = embedding_dimension @@ -166,9 +163,11 @@ def from_dict(cls, data: dict[str, Any]) -> "CohereDocumentImageEmbedder": :returns: Deserialized component. """ - deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"]) - return default_from_dict(cls, data) + init_params = data["init_parameters"] + deserialize_secrets_inplace(init_params, keys=["api_key"]) + init_params["embedding_type"] = EmbeddingTypes.from_str(init_params["embedding_type"]) + return default_from_dict(cls, data) @component.output_types(documents=list[Document]) def run(self, documents: list[Document]) -> dict[str, list[Document]]: @@ -200,8 +199,10 @@ def run(self, documents: list[Document]) -> dict[str, list[Document]]: for img_info in images_source_info: if img_info["mime_type"] not in SUPPORTED_IMAGE_MIME_TYPES: - msg = (f"Unsupported image MIME type: {img_info['mime_type']}. " - f"Supported types are: {', '.join(SUPPORTED_IMAGE_MIME_TYPES)}") + msg = ( + f"Unsupported image MIME type: {img_info['mime_type']}. " + f"Supported types are: {', '.join(SUPPORTED_IMAGE_MIME_TYPES)}" + ) raise ValueError(msg) images_to_embed: list[Optional[str]] = [None] * len(documents) @@ -220,13 +221,15 @@ def run(self, documents: list[Document]) -> dict[str, list[Document]]: pdf_page_infos.append(pdf_page_info) else: # Process images directly - image_byte_stream = ByteStream.from_file_path(filepath=image_source_info["path"], - mime_type=image_source_info["mime_type"]) + image_byte_stream = ByteStream.from_file_path( + filepath=image_source_info["path"], mime_type=image_source_info["mime_type"] + ) mime_type, base64_image = _encode_image_to_base64(bytestream=image_byte_stream, size=self.image_size) images_to_embed[doc_idx] = f"data:{mime_type};base64,{base64_image}" - base64_jpeg_images_by_doc_idx = _batch_convert_pdf_pages_to_images(pdf_page_infos=pdf_page_infos, return_base64=True, - size=self.image_size) + base64_jpeg_images_by_doc_idx = _batch_convert_pdf_pages_to_images( + pdf_page_infos=pdf_page_infos, return_base64=True, size=self.image_size + ) for doc_idx, base64_jpeg_image in base64_jpeg_images_by_doc_idx.items(): images_to_embed[doc_idx] = f"data:image/jpeg;base64,{base64_jpeg_image}" @@ -238,12 +241,12 @@ def run(self, documents: list[Document]) -> dict[str, list[Document]]: embeddings = [] # The Cohere API only supports passing one image at a time - for image in tqdm(images_to_embed, desc="Embedding images", disable=not self.progress_bar): - + for doc, image in tqdm(zip(documents, images_to_embed), desc="Embedding images", disable=not self.progress_bar): try: response = self._client.embed( model=self.model, - images=[image], + # tested above that image is not None, but mypy doesn't know that + images=[image], # type: ignore[list-item] input_type="image", output_dimension=self.embedding_dimension, embedding_types=[self.embedding_type.value], @@ -251,7 +254,7 @@ def run(self, documents: list[Document]) -> dict[str, list[Document]]: embedding = getattr(response.embeddings, self.embedding_type.value)[0] except Exception as e: - msg = f"Error embedding image: {e}" + msg = f"Error embedding Document {doc.id}. The Document will be skipped. \nException: {e}" logger.warning(msg) embedding = None @@ -267,4 +270,4 @@ def run(self, documents: list[Document]) -> dict[str, list[Document]]: new_doc = replace(doc, meta=new_meta, embedding=emb) docs_with_embeddings.append(new_doc) - return {"documents": docs_with_embeddings} \ No newline at end of file + return {"documents": docs_with_embeddings} diff --git a/integrations/cohere/tests/conftest.py b/integrations/cohere/tests/conftest.py new file mode 100644 index 0000000000..ed10024090 --- /dev/null +++ b/integrations/cohere/tests/conftest.py @@ -0,0 +1,8 @@ +from pathlib import Path + +import pytest + + +@pytest.fixture() +def test_files_path(): + return Path(__file__).parent / "test_files" diff --git a/integrations/cohere/tests/test_document_image_embedder.py b/integrations/cohere/tests/test_document_image_embedder.py new file mode 100644 index 0000000000..8bc1bf7569 --- /dev/null +++ b/integrations/cohere/tests/test_document_image_embedder.py @@ -0,0 +1,286 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +import glob +import logging +import os +import random +from unittest.mock import MagicMock, patch + +import pytest +from cohere.types import EmbedByTypeResponse, EmbedByTypeResponseEmbeddings +from haystack import Document + +from haystack_integrations.components.embedders.cohere.document_image_embedder import ( + CohereDocumentImageEmbedder, +) +from haystack_integrations.components.embedders.cohere.embedding_types import EmbeddingTypes + +IMPORT_PATH = "haystack_integrations.components.embedders.cohere.document_image_embedder" + + +class TestCohereDocumentImageEmbedder: + def test_init_default(self, monkeypatch): + monkeypatch.setenv("COHERE_API_KEY", "test-api-key") + embedder = CohereDocumentImageEmbedder() + + assert embedder.file_path_meta_field == "file_path" + assert embedder.root_path == "" + assert embedder.model == "embed-v4.0" + assert embedder.image_size is None + assert embedder.progress_bar is True + assert embedder.embedding_dimension is None + assert embedder.embedding_type == EmbeddingTypes.FLOAT + + assert embedder._api_base_url == "https://api.cohere.com" + assert embedder._timeout == 120 + assert embedder._api_key.resolve_value() == "test-api-key" + assert embedder._client is not None + assert embedder._async_client is not None + + def test_init_with_parameters(self, monkeypatch): + monkeypatch.setenv("COHERE_API_KEY", "test-api-key") + embedder = CohereDocumentImageEmbedder( + file_path_meta_field="custom_file_path", + root_path="root_path", + image_size=(100, 100), + model="model", + api_base_url="https://api.cohere.com/v1", + timeout=300, + progress_bar=False, + embedding_dimension=256, + embedding_type=EmbeddingTypes.INT8, + ) + assert embedder.file_path_meta_field == "custom_file_path" + assert embedder.root_path == "root_path" + assert embedder.model == "model" + assert embedder.image_size == (100, 100) + assert embedder.progress_bar is False + assert embedder.embedding_dimension == 256 + assert embedder.embedding_type == EmbeddingTypes.INT8 + assert embedder._api_base_url == "https://api.cohere.com/v1" + assert embedder._timeout == 300 + assert embedder._api_key.resolve_value() == "test-api-key" + assert embedder._client is not None + assert embedder._async_client is not None + + def test_to_dict(self, monkeypatch): + monkeypatch.setenv("COHERE_API_KEY", "test-api-key") + component = CohereDocumentImageEmbedder( + model="model", + api_base_url="https://api.cohere.com/v1", + timeout=300, + progress_bar=False, + embedding_dimension=256, + embedding_type=EmbeddingTypes.INT8, + ) + data = component.to_dict() + assert data == { + "type": f"{IMPORT_PATH}.CohereDocumentImageEmbedder", + "init_parameters": { + "file_path_meta_field": "file_path", + "root_path": "", + "model": "model", + "image_size": None, + "progress_bar": False, + "embedding_dimension": 256, + "embedding_type": "int8", + "api_base_url": "https://api.cohere.com/v1", + "timeout": 300, + "api_key": {"env_vars": ["COHERE_API_KEY", "CO_API_KEY"], "strict": True, "type": "env_var"}, + }, + } + + def test_from_dict(self, monkeypatch): + monkeypatch.setenv("COHERE_API_KEY", "test-api-key") + init_parameters = { + "file_path_meta_field": "custom_file_path", + "root_path": "root_path", + "model": "model", + "image_size": (100, 100), + "progress_bar": False, + "embedding_dimension": 256, + "embedding_type": "int8", + "api_base_url": "https://api.cohere.com/v1", + "timeout": 300, + "api_key": {"env_vars": ["COHERE_API_KEY", "CO_API_KEY"], "strict": True, "type": "env_var"}, + } + component = CohereDocumentImageEmbedder.from_dict( + {"type": f"{IMPORT_PATH}.CohereDocumentImageEmbedder", "init_parameters": init_parameters} + ) + assert component.file_path_meta_field == "custom_file_path" + assert component.root_path == "root_path" + assert component.model == "model" + assert component.image_size == (100, 100) + assert component.progress_bar is False + assert component.embedding_dimension == 256 + assert component.embedding_type == EmbeddingTypes.INT8 + assert component._api_base_url == "https://api.cohere.com/v1" + assert component._timeout == 300 + assert component._api_key.resolve_value() == "test-api-key" + assert component._client is not None + assert component._async_client is not None + + def test_run_wrong_input_format(self, monkeypatch): + monkeypatch.setenv("COHERE_API_KEY", "test-api-key") + embedder = CohereDocumentImageEmbedder(model="model") + + string_input = "text" + list_integers_input = [1, 2, 3] + + with pytest.raises(TypeError, match="CohereDocumentImageEmbedder expects a list of Documents as input"): + embedder.run(documents=string_input) + + with pytest.raises(TypeError, match="CohereDocumentImageEmbedder expects a list of Documents as input"): + embedder.run(documents=list_integers_input) + + @patch(f"{IMPORT_PATH}._extract_image_sources_info") + def test_run_unsupported_image_mime_type(self, mocked_extract_image_sources_info, monkeypatch): + monkeypatch.setenv("COHERE_API_KEY", "test-api-key") + embedder = CohereDocumentImageEmbedder(model="model") + embedder._client = MagicMock() + + mocked_extract_image_sources_info.return_value = [ + {"path": "unsupported.txt", "mime_type": "text/plain"}, + ] + + documents = [ + Document(content="Doc with unsupported mime type", meta={"file_path": "unsupported.txt"}), + ] + + with pytest.raises(ValueError, match="Unsupported image MIME type"): + embedder.run(documents=documents) + + def test_run(self, test_files_path, monkeypatch): + monkeypatch.setenv("COHERE_API_KEY", "test-api-key") + embedder = CohereDocumentImageEmbedder(model="model") + embedder._client = MagicMock() + + mock_response = EmbedByTypeResponse( + id="test-id", + embeddings=EmbedByTypeResponseEmbeddings(float_=[[random.random() for _ in range(1536)]]), # noqa: S311 + meta=None, + ) + + embedder._client.embed.return_value = mock_response + + image_paths = glob.glob(str(test_files_path / "*.jpg")) + glob.glob(str(test_files_path / "*.pdf")) + assert len(image_paths) == 2 + assert image_paths[0].endswith(".jpg") + assert image_paths[1].endswith(".pdf") + + documents = [] + for i, path in enumerate(image_paths): + document = Document(content=f"document number {i}", meta={"file_path": path}) + if path.endswith(".pdf"): + document.meta["page_number"] = 1 + documents.append(document) + + result = embedder.run(documents=documents) + + assert isinstance(result["documents"], list) + assert len(result["documents"]) == len(documents) + for doc, new_doc in zip(documents, result["documents"]): + assert doc.embedding is None + assert new_doc is not doc + assert isinstance(new_doc, Document) + assert isinstance(new_doc.embedding, list) + assert isinstance(new_doc.embedding[0], float) + assert "embedding_source" not in doc.meta + assert "embedding_source" in new_doc.meta + assert new_doc.meta["embedding_source"]["type"] == "image" + assert "file_path_meta_field" in new_doc.meta["embedding_source"] + + def test_run_client_errors(self, test_files_path, monkeypatch, caplog): + monkeypatch.setenv("COHERE_API_KEY", "test-api-key") + embedder = CohereDocumentImageEmbedder(model="model") + embedder._client = MagicMock() + embedder._client.embed.side_effect = Exception("Error embedding image") + + image_paths = glob.glob(str(test_files_path / "*.jpg")) + glob.glob(str(test_files_path / "*.pdf")) + assert len(image_paths) == 2 + assert image_paths[0].endswith(".jpg") + assert image_paths[1].endswith(".pdf") + + documents = [] + for i, path in enumerate(image_paths): + document = Document(content=f"document number {i}", meta={"file_path": path}) + if path.endswith(".pdf"): + document.meta["page_number"] = 1 + documents.append(document) + + with caplog.at_level(logging.WARNING): + result = embedder.run(documents=documents) + + assert "Error embedding Document" in caplog.text + + assert isinstance(result["documents"], list) + assert len(result["documents"]) == len(documents) + for doc, new_doc in zip(documents, result["documents"]): + assert doc.embedding is None + assert new_doc is not doc + assert isinstance(new_doc, Document) + assert new_doc.embedding is None + + @patch(f"{IMPORT_PATH}._extract_image_sources_info") + @patch(f"{IMPORT_PATH}._batch_convert_pdf_pages_to_images") + @patch(f"{IMPORT_PATH}._encode_image_to_base64") + @patch(f"{IMPORT_PATH}.ByteStream.from_file_path") + def test_run_none_images( + self, + mocked_byte_stream_from_file_path, + mocked_encode_image_to_base64, + mocked_batch_convert_pdf_pages_to_images, + mocked_extract_image_sources_info, + monkeypatch, + ): + monkeypatch.setenv("COHERE_API_KEY", "test-api-key") + embedder = CohereDocumentImageEmbedder(model="model") + embedder._client = MagicMock() + + mocked_extract_image_sources_info.return_value = [ + {"path": "pdf_1.pdf", "mime_type": "application/pdf", "page_number": 999}, # Page 999 doesn't exist + {"path": "image_1.jpg", "mime_type": "image/jpeg"}, + ] + mocked_batch_convert_pdf_pages_to_images.return_value = {} # Empty dict because page was skipped + mocked_encode_image_to_base64.return_value = ("image/jpeg", "base64_image") + mocked_byte_stream_from_file_path.return_value = MagicMock() + + documents = [ + Document(content="PDF 1", meta={"file_path": "pdf_1.pdf", "page_number": 999}), + Document(content="Image 1", meta={"file_path": "image_1.jpg"}), + ] + + with pytest.raises(RuntimeError, match="Conversion failed for some documents."): + embedder.run(documents=documents) + + @pytest.mark.integration + @pytest.mark.skipif( + not os.environ.get("COHERE_API_KEY", None) and not os.environ.get("CO_API_KEY", None), + reason="Export an env var called COHERE_API_KEY/CO_API_KEY containing the Cohere API key to run this test.", + ) + def test_live_run(self, test_files_path): + embedder = CohereDocumentImageEmbedder(model="embed-v4.0", image_size=(100, 100)) + + documents = [ + Document( + content="PDF document", + meta={"file_path": str(test_files_path / "sample_pdf_1.pdf"), "page_number": 1}, + ), + Document(content="Image document", meta={"file_path": str(test_files_path / "apple.jpg")}), + ] + + result = embedder.run(documents=documents) + assert len(result["documents"]) == len(documents) + for doc, new_doc in zip(documents, result["documents"]): + assert doc.embedding is None + assert new_doc is not doc + assert isinstance(new_doc, Document) + assert isinstance(new_doc.embedding, list) + assert len(new_doc.embedding) == 1536 + assert all(isinstance(x, float) for x in new_doc.embedding) + assert "embedding_source" not in doc.meta + assert "embedding_source" in new_doc.meta + assert new_doc.meta["embedding_source"]["type"] == "image" + assert "file_path_meta_field" in new_doc.meta["embedding_source"] diff --git a/integrations/cohere/tests/test_files/sample_pdf_1.pdf b/integrations/cohere/tests/test_files/sample_pdf_1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..87259b897f83b462f521276bf32d210ea008bcd3 GIT binary patch literal 44524 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

1p2->@_y0X+Ac~V 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 LF=KrFr&;|4#k|z* literal 0 HcmV?d00001 From ca561d09b635eae802fe20413f491269ac37a341 Mon Sep 17 00:00:00 2001 From: anakin87 Date: Thu, 14 Aug 2025 15:34:35 +0200 Subject: [PATCH 3/9] async --- .../cohere/document_image_embedder.py | 81 +++++++++++++++++-- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py index 58d7af6370..7f6d60ae60 100644 --- a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py +++ b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py @@ -15,6 +15,7 @@ from haystack.dataclasses import ByteStream from haystack.utils.auth import Secret, deserialize_secrets_inplace from tqdm import tqdm +from tqdm.asyncio import tqdm as tqdm_async from cohere import AsyncClientV2, ClientV2 @@ -169,22 +170,22 @@ def from_dict(cls, data: dict[str, Any]) -> "CohereDocumentImageEmbedder": return default_from_dict(cls, data) - @component.output_types(documents=list[Document]) - def run(self, documents: list[Document]) -> dict[str, list[Document]]: + def _extract_images_to_embed(self, documents: list[Document]) -> list[str]: """ - Embed a list of documents. + Validates the input documents and extracts the images to embed in the format expected by the Cohere API. :param documents: Documents to embed. :returns: - A dictionary with the following keys: - - `documents`: Documents with embeddings. + List of images to embed in the format expected by the Cohere API. :raises TypeError: If the input is not a list of `Documents`. :raises ValueError: If the input contains unsupported image MIME types. + :raises RuntimeError: + If the conversion of some documents fails. """ if not isinstance(documents, list) or (documents and not isinstance(documents[0], Document)): msg = ( @@ -238,6 +239,25 @@ def run(self, documents: list[Document]) -> dict[str, list[Document]]: msg = f"Conversion failed for some documents. Document IDs: {none_images_doc_ids}." raise RuntimeError(msg) + # tested above that image is not None, but mypy doesn't know that + return images_to_embed # type: ignore[return-value] + + + @component.output_types(documents=list[Document]) + def run(self, documents: list[Document]) -> dict[str, list[Document]]: + """ + Embed a list of image documents. + + :param documents: + Documents to embed. + + :returns: + A dictionary with the following keys: + - `documents`: Documents with embeddings. + """ + + images_to_embed = self._extract_images_to_embed(documents) + embeddings = [] # The Cohere API only supports passing one image at a time @@ -245,8 +265,7 @@ def run(self, documents: list[Document]) -> dict[str, list[Document]]: try: response = self._client.embed( model=self.model, - # tested above that image is not None, but mypy doesn't know that - images=[image], # type: ignore[list-item] + images=[image], input_type="image", output_dimension=self.embedding_dimension, embedding_types=[self.embedding_type.value], @@ -271,3 +290,51 @@ def run(self, documents: list[Document]) -> dict[str, list[Document]]: docs_with_embeddings.append(new_doc) return {"documents": docs_with_embeddings} + + @component.output_types(documents=list[Document]) + async def run_async(self, documents: list[Document]) -> dict[str, list[Document]]: + """ + Asynchronously embed a list of image documents. + + :param documents: + Documents to embed. + + :returns: + A dictionary with the following keys: + - `documents`: Documents with embeddings. + """ + + images_to_embed = self._extract_images_to_embed(documents) + + embeddings = [] + + # The Cohere API only supports passing one image at a time + async for doc, image in tqdm_async(zip(documents, images_to_embed), desc="Embedding images", disable=not self.progress_bar): + try: + response = await self._async_client.embed( + model=self.model, + images=[image], + input_type="image", + output_dimension=self.embedding_dimension, + embedding_types=[self.embedding_type.value], + ) + + embedding = getattr(response.embeddings, self.embedding_type.value)[0] + except Exception as e: + msg = f"Error embedding Document {doc.id}. The Document will be skipped. \nException: {e}" + logger.warning(msg) + embedding = None + + embeddings.append(embedding) + + docs_with_embeddings = [] + for doc, emb in zip(documents, embeddings): + # we store this information for later inspection + new_meta = { + **doc.meta, + "embedding_source": {"type": "image", "file_path_meta_field": self.file_path_meta_field}, + } + new_doc = replace(doc, meta=new_meta, embedding=emb) + docs_with_embeddings.append(new_doc) + + return {"documents": docs_with_embeddings} From 66a5ce28c32126ab2fde82d7f4a5b65ad2c9270e Mon Sep 17 00:00:00 2001 From: anakin87 Date: Thu, 14 Aug 2025 15:51:31 +0200 Subject: [PATCH 4/9] reorganize tests --- .../tests/test_document_image_embedder.py | 175 ++++++++++++++---- 1 file changed, 141 insertions(+), 34 deletions(-) diff --git a/integrations/cohere/tests/test_document_image_embedder.py b/integrations/cohere/tests/test_document_image_embedder.py index 8bc1bf7569..02632e1796 100644 --- a/integrations/cohere/tests/test_document_image_embedder.py +++ b/integrations/cohere/tests/test_document_image_embedder.py @@ -6,7 +6,7 @@ import logging import os import random -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, AsyncMock, patch import pytest from cohere.types import EmbedByTypeResponse, EmbedByTypeResponseEmbeddings @@ -122,7 +122,7 @@ def test_from_dict(self, monkeypatch): assert component._client is not None assert component._async_client is not None - def test_run_wrong_input_format(self, monkeypatch): + def test_extract_images_to_embed_wrong_input_format(self, monkeypatch): monkeypatch.setenv("COHERE_API_KEY", "test-api-key") embedder = CohereDocumentImageEmbedder(model="model") @@ -130,16 +130,15 @@ def test_run_wrong_input_format(self, monkeypatch): list_integers_input = [1, 2, 3] with pytest.raises(TypeError, match="CohereDocumentImageEmbedder expects a list of Documents as input"): - embedder.run(documents=string_input) + embedder._extract_images_to_embed(string_input) with pytest.raises(TypeError, match="CohereDocumentImageEmbedder expects a list of Documents as input"): - embedder.run(documents=list_integers_input) + embedder._extract_images_to_embed(list_integers_input) @patch(f"{IMPORT_PATH}._extract_image_sources_info") - def test_run_unsupported_image_mime_type(self, mocked_extract_image_sources_info, monkeypatch): + def test_extract_images_to_embed_unsupported_image_mime_type(self, mocked_extract_image_sources_info, monkeypatch): monkeypatch.setenv("COHERE_API_KEY", "test-api-key") embedder = CohereDocumentImageEmbedder(model="model") - embedder._client = MagicMock() mocked_extract_image_sources_info.return_value = [ {"path": "unsupported.txt", "mime_type": "text/plain"}, @@ -150,7 +149,73 @@ def test_run_unsupported_image_mime_type(self, mocked_extract_image_sources_info ] with pytest.raises(ValueError, match="Unsupported image MIME type"): - embedder.run(documents=documents) + embedder._extract_images_to_embed(documents) + + @patch(f"{IMPORT_PATH}._extract_image_sources_info") + @patch(f"{IMPORT_PATH}._batch_convert_pdf_pages_to_images") + @patch(f"{IMPORT_PATH}._encode_image_to_base64") + @patch(f"{IMPORT_PATH}.ByteStream.from_file_path") + def test_extract_images_to_embed_none_images( + self, + mocked_byte_stream_from_file_path, + mocked_encode_image_to_base64, + mocked_batch_convert_pdf_pages_to_images, + mocked_extract_image_sources_info, + monkeypatch, + ): + monkeypatch.setenv("COHERE_API_KEY", "test-api-key") + embedder = CohereDocumentImageEmbedder(model="model") + + mocked_extract_image_sources_info.return_value = [ + {"path": "pdf_1.pdf", "mime_type": "application/pdf", "page_number": 999}, # Page 999 doesn't exist + {"path": "image_1.jpg", "mime_type": "image/jpeg"}, + ] + mocked_batch_convert_pdf_pages_to_images.return_value = {} # Empty dict because page was skipped + mocked_encode_image_to_base64.return_value = ("image/jpeg", "base64_image") + mocked_byte_stream_from_file_path.return_value = MagicMock() + + documents = [ + Document(content="PDF 1", meta={"file_path": "pdf_1.pdf", "page_number": 999}), + Document(content="Image 1", meta={"file_path": "image_1.jpg"}), + ] + + with pytest.raises(RuntimeError, match="Conversion failed for some documents."): + embedder._extract_images_to_embed(documents) + + @patch(f"{IMPORT_PATH}._extract_image_sources_info") + @patch(f"{IMPORT_PATH}._batch_convert_pdf_pages_to_images") + @patch(f"{IMPORT_PATH}._encode_image_to_base64") + @patch(f"{IMPORT_PATH}.ByteStream.from_file_path") + def test_extract_images_to_embed_success( + self, + mocked_byte_stream_from_file_path, + mocked_encode_image_to_base64, + mocked_batch_convert_pdf_pages_to_images, + mocked_extract_image_sources_info, + monkeypatch, + ): + monkeypatch.setenv("COHERE_API_KEY", "test-api-key") + embedder = CohereDocumentImageEmbedder(model="model") + + mocked_extract_image_sources_info.return_value = [ + {"path": "pdf_1.pdf", "mime_type": "application/pdf", "page_number": 1}, + {"path": "image_1.jpg", "mime_type": "image/jpeg"}, + ] + mocked_batch_convert_pdf_pages_to_images.return_value = {0: "base64_pdf_image"} + mocked_encode_image_to_base64.return_value = ("image/jpeg", "base64_image") + mocked_byte_stream_from_file_path.return_value = MagicMock() + + documents = [ + Document(content="PDF 1", meta={"file_path": "pdf_1.pdf", "page_number": 1}), + Document(content="Image 1", meta={"file_path": "image_1.jpg"}), + ] + + result = embedder._extract_images_to_embed(documents) + + assert len(result) == 2 + assert result[0]=="data:image/jpeg;base64,base64_pdf_image" + assert result[1]=="data:image/jpeg;base64,base64_image" + def test_run(self, test_files_path, monkeypatch): monkeypatch.setenv("COHERE_API_KEY", "test-api-key") @@ -223,37 +288,79 @@ def test_run_client_errors(self, test_files_path, monkeypatch, caplog): assert isinstance(new_doc, Document) assert new_doc.embedding is None - @patch(f"{IMPORT_PATH}._extract_image_sources_info") - @patch(f"{IMPORT_PATH}._batch_convert_pdf_pages_to_images") - @patch(f"{IMPORT_PATH}._encode_image_to_base64") - @patch(f"{IMPORT_PATH}.ByteStream.from_file_path") - def test_run_none_images( - self, - mocked_byte_stream_from_file_path, - mocked_encode_image_to_base64, - mocked_batch_convert_pdf_pages_to_images, - mocked_extract_image_sources_info, - monkeypatch, - ): + @pytest.mark.asyncio + async def test_run_async(self, test_files_path, monkeypatch): monkeypatch.setenv("COHERE_API_KEY", "test-api-key") embedder = CohereDocumentImageEmbedder(model="model") - embedder._client = MagicMock() + embedder._async_client = AsyncMock() - mocked_extract_image_sources_info.return_value = [ - {"path": "pdf_1.pdf", "mime_type": "application/pdf", "page_number": 999}, # Page 999 doesn't exist - {"path": "image_1.jpg", "mime_type": "image/jpeg"}, - ] - mocked_batch_convert_pdf_pages_to_images.return_value = {} # Empty dict because page was skipped - mocked_encode_image_to_base64.return_value = ("image/jpeg", "base64_image") - mocked_byte_stream_from_file_path.return_value = MagicMock() + mock_response = EmbedByTypeResponse( + id="test-id", + embeddings=EmbedByTypeResponseEmbeddings(float_=[[random.random() for _ in range(1536)]]), # noqa: S311 + meta=None, + ) - documents = [ - Document(content="PDF 1", meta={"file_path": "pdf_1.pdf", "page_number": 999}), - Document(content="Image 1", meta={"file_path": "image_1.jpg"}), - ] + embedder._async_client.embed.return_value = mock_response + + image_paths = glob.glob(str(test_files_path / "*.jpg")) + glob.glob(str(test_files_path / "*.pdf")) + assert len(image_paths) == 2 + assert image_paths[0].endswith(".jpg") + assert image_paths[1].endswith(".pdf") + + documents = [] + for i, path in enumerate(image_paths): + document = Document(content=f"document number {i}", meta={"file_path": path}) + if path.endswith(".pdf"): + document.meta["page_number"] = 1 + documents.append(document) + + result = await embedder.run_async(documents=documents) + + assert isinstance(result["documents"], list) + assert len(result["documents"]) == len(documents) + for doc, new_doc in zip(documents, result["documents"]): + assert doc.embedding is None + assert new_doc is not doc + assert isinstance(new_doc, Document) + assert isinstance(new_doc.embedding, list) + assert isinstance(new_doc.embedding[0], float) + assert "embedding_source" not in doc.meta + assert "embedding_source" in new_doc.meta + assert new_doc.meta["embedding_source"]["type"] == "image" + assert "file_path_meta_field" in new_doc.meta["embedding_source"] + + @pytest.mark.asyncio + async def test_run_async_client_errors(self, test_files_path, monkeypatch, caplog): + monkeypatch.setenv("COHERE_API_KEY", "test-api-key") + embedder = CohereDocumentImageEmbedder(model="model") + embedder._async_client = AsyncMock() + embedder._async_client.embed.side_effect = Exception("Error embedding image") + + image_paths = glob.glob(str(test_files_path / "*.jpg")) + glob.glob(str(test_files_path / "*.pdf")) + assert len(image_paths) == 2 + assert image_paths[0].endswith(".jpg") + assert image_paths[1].endswith(".pdf") + + documents = [] + for i, path in enumerate(image_paths): + document = Document(content=f"document number {i}", meta={"file_path": path}) + if path.endswith(".pdf"): + document.meta["page_number"] = 1 + documents.append(document) + + with caplog.at_level(logging.WARNING): + result = await embedder.run_async(documents=documents) + + assert "Error embedding Document" in caplog.text + + assert isinstance(result["documents"], list) + assert len(result["documents"]) == len(documents) + for doc, new_doc in zip(documents, result["documents"]): + assert doc.embedding is None + assert new_doc is not doc + assert isinstance(new_doc, Document) + assert new_doc.embedding is None - with pytest.raises(RuntimeError, match="Conversion failed for some documents."): - embedder.run(documents=documents) @pytest.mark.integration @pytest.mark.skipif( @@ -283,4 +390,4 @@ def test_live_run(self, test_files_path): assert "embedding_source" not in doc.meta assert "embedding_source" in new_doc.meta assert new_doc.meta["embedding_source"]["type"] == "image" - assert "file_path_meta_field" in new_doc.meta["embedding_source"] + assert "file_path_meta_field" in new_doc.meta["embedding_source"] \ No newline at end of file From 9d322d2cf4e08d79bb63dd78f88bf1ca64add932 Mon Sep 17 00:00:00 2001 From: anakin87 Date: Thu, 14 Aug 2025 15:51:52 +0200 Subject: [PATCH 5/9] fmt --- .../embedders/cohere/document_image_embedder.py | 7 ++++--- .../cohere/tests/test_document_image_embedder.py | 12 +++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py index 7f6d60ae60..477d47a99a 100644 --- a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py +++ b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py @@ -242,7 +242,6 @@ def _extract_images_to_embed(self, documents: list[Document]) -> list[str]: # tested above that image is not None, but mypy doesn't know that return images_to_embed # type: ignore[return-value] - @component.output_types(documents=list[Document]) def run(self, documents: list[Document]) -> dict[str, list[Document]]: """ @@ -309,7 +308,9 @@ async def run_async(self, documents: list[Document]) -> dict[str, list[Document] embeddings = [] # The Cohere API only supports passing one image at a time - async for doc, image in tqdm_async(zip(documents, images_to_embed), desc="Embedding images", disable=not self.progress_bar): + async for doc, image in tqdm_async( + zip(documents, images_to_embed), desc="Embedding images", disable=not self.progress_bar + ): try: response = await self._async_client.embed( model=self.model, @@ -337,4 +338,4 @@ async def run_async(self, documents: list[Document]) -> dict[str, list[Document] new_doc = replace(doc, meta=new_meta, embedding=emb) docs_with_embeddings.append(new_doc) - return {"documents": docs_with_embeddings} + return {"documents": docs_with_embeddings} diff --git a/integrations/cohere/tests/test_document_image_embedder.py b/integrations/cohere/tests/test_document_image_embedder.py index 02632e1796..ee42186ad8 100644 --- a/integrations/cohere/tests/test_document_image_embedder.py +++ b/integrations/cohere/tests/test_document_image_embedder.py @@ -6,7 +6,7 @@ import logging import os import random -from unittest.mock import MagicMock, AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from cohere.types import EmbedByTypeResponse, EmbedByTypeResponseEmbeddings @@ -211,11 +211,10 @@ def test_extract_images_to_embed_success( ] result = embedder._extract_images_to_embed(documents) - - assert len(result) == 2 - assert result[0]=="data:image/jpeg;base64,base64_pdf_image" - assert result[1]=="data:image/jpeg;base64,base64_image" + assert len(result) == 2 + assert result[0] == "data:image/jpeg;base64,base64_pdf_image" + assert result[1] == "data:image/jpeg;base64,base64_image" def test_run(self, test_files_path, monkeypatch): monkeypatch.setenv("COHERE_API_KEY", "test-api-key") @@ -361,7 +360,6 @@ async def test_run_async_client_errors(self, test_files_path, monkeypatch, caplo assert isinstance(new_doc, Document) assert new_doc.embedding is None - @pytest.mark.integration @pytest.mark.skipif( not os.environ.get("COHERE_API_KEY", None) and not os.environ.get("CO_API_KEY", None), @@ -390,4 +388,4 @@ def test_live_run(self, test_files_path): assert "embedding_source" not in doc.meta assert "embedding_source" in new_doc.meta assert new_doc.meta["embedding_source"]["type"] == "image" - assert "file_path_meta_field" in new_doc.meta["embedding_source"] \ No newline at end of file + assert "file_path_meta_field" in new_doc.meta["embedding_source"] From f41541b6670333a9eefa8eee590148f51b60e2e9 Mon Sep 17 00:00:00 2001 From: anakin87 Date: Thu, 14 Aug 2025 16:00:38 +0200 Subject: [PATCH 6/9] small fix --- .../components/embedders/cohere/document_image_embedder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py index 477d47a99a..f597e720e1 100644 --- a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py +++ b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py @@ -101,7 +101,7 @@ def __init__( supported models. :param embedding_type: The type of embeddings to return. Defaults to float embeddings. - Note that int8, uint8, binary, and ubinary are only valid for v3 models. + Specifying a type different from float is only supported for Embed v3.0 and newer models. :param progress_bar: Whether to show a progress bar or not. Can be helpful to disable in production deployments to keep the logs clean. From 6f6496ea9657277769f5ce9249890348f8cb47b4 Mon Sep 17 00:00:00 2001 From: anakin87 Date: Mon, 18 Aug 2025 15:17:08 +0200 Subject: [PATCH 7/9] fix comments --- .../embedders/cohere/document_embedder.py | 2 +- .../cohere/document_image_embedder.py | 21 +++++++------------ .../embedders/cohere/text_embedder.py | 2 +- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_embedder.py b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_embedder.py index ea98849817..0b118924bf 100644 --- a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_embedder.py +++ b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_embedder.py @@ -42,7 +42,7 @@ def __init__( input_type: str = "search_document", api_base_url: str = "https://api.cohere.com", truncate: str = "END", - timeout: int = 120, + timeout: float = 120.0, batch_size: int = 32, progress_bar: bool = True, meta_fields_to_embed: Optional[List[str]] = None, diff --git a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py index f597e720e1..3bf2207199 100644 --- a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py +++ b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/document_image_embedder.py @@ -15,7 +15,6 @@ from haystack.dataclasses import ByteStream from haystack.utils.auth import Secret, deserialize_secrets_inplace from tqdm import tqdm -from tqdm.asyncio import tqdm as tqdm_async from cohere import AsyncClientV2, ClientV2 @@ -69,7 +68,7 @@ def __init__( api_key: Secret = Secret.from_env_var(["COHERE_API_KEY", "CO_API_KEY"]), model: str = "embed-v4.0", api_base_url: str = "https://api.cohere.com", - timeout: int = 120, + timeout: float = 120.0, embedding_dimension: Optional[int] = None, embedding_type: EmbeddingTypes = EmbeddingTypes.FLOAT, progress_bar: bool = True, @@ -187,7 +186,7 @@ def _extract_images_to_embed(self, documents: list[Document]) -> list[str]: :raises RuntimeError: If the conversion of some documents fails. """ - if not isinstance(documents, list) or (documents and not isinstance(documents[0], Document)): + if not isinstance(documents, list) or not all(isinstance(d, Document) for d in documents): msg = ( "CohereDocumentImageEmbedder expects a list of Documents as input. " "In case you want to embed a string, please use the CohereTextEmbedder." @@ -269,12 +268,10 @@ def run(self, documents: list[Document]) -> dict[str, list[Document]]: output_dimension=self.embedding_dimension, embedding_types=[self.embedding_type.value], ) - embedding = getattr(response.embeddings, self.embedding_type.value)[0] except Exception as e: - msg = f"Error embedding Document {doc.id}. The Document will be skipped. \nException: {e}" - logger.warning(msg) - embedding = None + msg = f"Error embedding Document {doc.id}" + raise RuntimeError(msg) from e embeddings.append(embedding) @@ -308,9 +305,7 @@ async def run_async(self, documents: list[Document]) -> dict[str, list[Document] embeddings = [] # The Cohere API only supports passing one image at a time - async for doc, image in tqdm_async( - zip(documents, images_to_embed), desc="Embedding images", disable=not self.progress_bar - ): + for doc, image in tqdm(zip(documents, images_to_embed), desc="Embedding images", disable=not self.progress_bar): try: response = await self._async_client.embed( model=self.model, @@ -319,12 +314,10 @@ async def run_async(self, documents: list[Document]) -> dict[str, list[Document] output_dimension=self.embedding_dimension, embedding_types=[self.embedding_type.value], ) - embedding = getattr(response.embeddings, self.embedding_type.value)[0] except Exception as e: - msg = f"Error embedding Document {doc.id}. The Document will be skipped. \nException: {e}" - logger.warning(msg) - embedding = None + msg = f"Error embedding Document {doc.id}" + raise RuntimeError(msg) from e embeddings.append(embedding) diff --git a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/text_embedder.py b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/text_embedder.py index e593589817..717ad992d0 100644 --- a/integrations/cohere/src/haystack_integrations/components/embedders/cohere/text_embedder.py +++ b/integrations/cohere/src/haystack_integrations/components/embedders/cohere/text_embedder.py @@ -39,7 +39,7 @@ def __init__( input_type: str = "search_query", api_base_url: str = "https://api.cohere.com", truncate: str = "END", - timeout: int = 120, + timeout: float = 120.0, embedding_type: Optional[EmbeddingTypes] = None, ): """ From dc4136987206dd2e4ccf85961431a5cf1426595a Mon Sep 17 00:00:00 2001 From: anakin87 Date: Mon, 18 Aug 2025 15:23:09 +0200 Subject: [PATCH 8/9] adjust test --- .../tests/test_document_image_embedder.py | 33 ++++--------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/integrations/cohere/tests/test_document_image_embedder.py b/integrations/cohere/tests/test_document_image_embedder.py index ee42186ad8..b4829d0487 100644 --- a/integrations/cohere/tests/test_document_image_embedder.py +++ b/integrations/cohere/tests/test_document_image_embedder.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 import glob -import logging import os import random from unittest.mock import AsyncMock, MagicMock, patch @@ -256,7 +255,7 @@ def test_run(self, test_files_path, monkeypatch): assert new_doc.meta["embedding_source"]["type"] == "image" assert "file_path_meta_field" in new_doc.meta["embedding_source"] - def test_run_client_errors(self, test_files_path, monkeypatch, caplog): + def test_run_client_errors(self, test_files_path, monkeypatch): monkeypatch.setenv("COHERE_API_KEY", "test-api-key") embedder = CohereDocumentImageEmbedder(model="model") embedder._client = MagicMock() @@ -274,18 +273,8 @@ def test_run_client_errors(self, test_files_path, monkeypatch, caplog): document.meta["page_number"] = 1 documents.append(document) - with caplog.at_level(logging.WARNING): - result = embedder.run(documents=documents) - - assert "Error embedding Document" in caplog.text - - assert isinstance(result["documents"], list) - assert len(result["documents"]) == len(documents) - for doc, new_doc in zip(documents, result["documents"]): - assert doc.embedding is None - assert new_doc is not doc - assert isinstance(new_doc, Document) - assert new_doc.embedding is None + with pytest.raises(RuntimeError, match="Error embedding Document"): + embedder.run(documents=documents) @pytest.mark.asyncio async def test_run_async(self, test_files_path, monkeypatch): @@ -329,7 +318,7 @@ async def test_run_async(self, test_files_path, monkeypatch): assert "file_path_meta_field" in new_doc.meta["embedding_source"] @pytest.mark.asyncio - async def test_run_async_client_errors(self, test_files_path, monkeypatch, caplog): + async def test_run_async_client_errors(self, test_files_path, monkeypatch): monkeypatch.setenv("COHERE_API_KEY", "test-api-key") embedder = CohereDocumentImageEmbedder(model="model") embedder._async_client = AsyncMock() @@ -347,18 +336,8 @@ async def test_run_async_client_errors(self, test_files_path, monkeypatch, caplo document.meta["page_number"] = 1 documents.append(document) - with caplog.at_level(logging.WARNING): - result = await embedder.run_async(documents=documents) - - assert "Error embedding Document" in caplog.text - - assert isinstance(result["documents"], list) - assert len(result["documents"]) == len(documents) - for doc, new_doc in zip(documents, result["documents"]): - assert doc.embedding is None - assert new_doc is not doc - assert isinstance(new_doc, Document) - assert new_doc.embedding is None + with pytest.raises(RuntimeError, match="Error embedding Document"): + await embedder.run_async(documents=documents) @pytest.mark.integration @pytest.mark.skipif( From 27299f5bba2c4b2b71bee67160cf19bbda3d47af Mon Sep 17 00:00:00 2001 From: anakin87 Date: Mon, 18 Aug 2025 16:29:40 +0200 Subject: [PATCH 9/9] add test_live_run_async --- .../tests/test_document_image_embedder.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/integrations/cohere/tests/test_document_image_embedder.py b/integrations/cohere/tests/test_document_image_embedder.py index b4829d0487..75d8f89ad7 100644 --- a/integrations/cohere/tests/test_document_image_embedder.py +++ b/integrations/cohere/tests/test_document_image_embedder.py @@ -368,3 +368,34 @@ def test_live_run(self, test_files_path): assert "embedding_source" in new_doc.meta assert new_doc.meta["embedding_source"]["type"] == "image" assert "file_path_meta_field" in new_doc.meta["embedding_source"] + + @pytest.mark.asyncio + @pytest.mark.integration + @pytest.mark.skipif( + not os.environ.get("COHERE_API_KEY", None) and not os.environ.get("CO_API_KEY", None), + reason="Export an env var called COHERE_API_KEY/CO_API_KEY containing the Cohere API key to run this test.", + ) + async def test_live_run_async(self, test_files_path): + embedder = CohereDocumentImageEmbedder(model="embed-v4.0", image_size=(100, 100)) + + documents = [ + Document( + content="PDF document", + meta={"file_path": str(test_files_path / "sample_pdf_1.pdf"), "page_number": 1}, + ), + Document(content="Image document", meta={"file_path": str(test_files_path / "apple.jpg")}), + ] + + result = await embedder.run_async(documents=documents) + assert len(result["documents"]) == len(documents) + for doc, new_doc in zip(documents, result["documents"]): + assert doc.embedding is None + assert new_doc is not doc + assert isinstance(new_doc, Document) + assert isinstance(new_doc.embedding, list) + assert len(new_doc.embedding) == 1536 + assert all(isinstance(x, float) for x in new_doc.embedding) + assert "embedding_source" not in doc.meta + assert "embedding_source" in new_doc.meta + assert new_doc.meta["embedding_source"]["type"] == "image" + assert "file_path_meta_field" in new_doc.meta["embedding_source"]