chore: import upstream snapshot with attribution
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
Docker image release / Build base image (push) Has been cancelled
Sync docs with Docusaurus / sync (push) Has been cancelled
Tests / Check if changed (push) Has been cancelled
Tests / format (push) Has been cancelled
Tests / check-imports (push) Has been cancelled
Tests / Unit / macos-latest (push) Has been cancelled
Tests / Unit / ubuntu-latest (push) Has been cancelled
Tests / Unit / windows-latest (push) Has been cancelled
Tests / mypy (push) Has been cancelled
Tests / Integration / ubuntu-latest (push) Has been cancelled
Tests / Integration / macos-latest (push) Has been cancelled
Tests / Integration / windows-latest (push) Has been cancelled
Tests / notify-slack-on-failure (push) Has been cancelled
Tests / Mark tests as completed (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:22:28 +08:00
commit c56bef871b
9296 changed files with 1854228 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import sys
from typing import TYPE_CHECKING
from lazy_imports import LazyImporter
_import_structure = {
"azure_document_embedder": ["AzureOpenAIDocumentEmbedder"],
"azure_text_embedder": ["AzureOpenAITextEmbedder"],
"mock_document_embedder": ["MockDocumentEmbedder"],
"mock_text_embedder": ["MockTextEmbedder"],
"openai_document_embedder": ["OpenAIDocumentEmbedder"],
"openai_text_embedder": ["OpenAITextEmbedder"],
}
if TYPE_CHECKING:
from .azure_document_embedder import AzureOpenAIDocumentEmbedder as AzureOpenAIDocumentEmbedder
from .azure_text_embedder import AzureOpenAITextEmbedder as AzureOpenAITextEmbedder
from .mock_document_embedder import MockDocumentEmbedder as MockDocumentEmbedder
from .mock_text_embedder import MockTextEmbedder as MockTextEmbedder
from .openai_document_embedder import OpenAIDocumentEmbedder as OpenAIDocumentEmbedder
from .openai_text_embedder import OpenAITextEmbedder as OpenAITextEmbedder
else:
sys.modules[__name__] = LazyImporter(name=__name__, module_file=__file__, import_structure=_import_structure)
@@ -0,0 +1,250 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import os
from typing import Any
from openai.lib.azure import AsyncAzureOpenAI, AzureADTokenProvider, AzureOpenAI
from haystack import component, default_from_dict, default_to_dict, logging
from haystack.components.embedders import OpenAIDocumentEmbedder
from haystack.utils import Secret, deserialize_callable, serialize_callable
from haystack.utils.http_client import init_http_client
logger = logging.getLogger(__name__)
@component
class AzureOpenAIDocumentEmbedder(OpenAIDocumentEmbedder):
"""
Calculates document embeddings using OpenAI models deployed on Azure.
### Usage example
<!-- test-ignore -->
```python
from haystack import Document
from haystack.components.embedders import AzureOpenAIDocumentEmbedder
doc = Document(content="I love pizza!")
document_embedder = AzureOpenAIDocumentEmbedder()
result = document_embedder.run([doc])
print(result['documents'][0].embedding)
# [0.017020374536514282, -0.023255806416273117, ...]
```
"""
def __init__( # noqa: PLR0913 (too-many-arguments)
self,
azure_endpoint: str | None = None,
api_version: str | None = "2023-05-15",
azure_deployment: str = "text-embedding-ada-002",
dimensions: int | None = None,
api_key: Secret | None = Secret.from_env_var("AZURE_OPENAI_API_KEY", strict=False),
azure_ad_token: Secret | None = Secret.from_env_var("AZURE_OPENAI_AD_TOKEN", strict=False),
organization: str | None = None,
prefix: str = "",
suffix: str = "",
batch_size: int = 32,
progress_bar: bool = True,
meta_fields_to_embed: list[str] | None = None,
embedding_separator: str = "\n",
timeout: float | None = None,
max_retries: int | None = None,
*,
default_headers: dict[str, str] | None = None,
azure_ad_token_provider: AzureADTokenProvider | None = None,
http_client_kwargs: dict[str, Any] | None = None,
raise_on_failure: bool = False,
) -> None:
"""
Creates an AzureOpenAIDocumentEmbedder component.
:param azure_endpoint:
The endpoint of the model deployed on Azure.
:param api_version:
The version of the API to use.
:param azure_deployment:
The name of the model deployed on Azure. The default model is text-embedding-ada-002.
:param dimensions:
The number of dimensions of the resulting embeddings. Only supported in text-embedding-3
and later models.
:param api_key:
The Azure OpenAI API key.
You can set it with an environment variable `AZURE_OPENAI_API_KEY`, or pass with this
parameter during initialization.
:param azure_ad_token:
Microsoft Entra ID token, see Microsoft's
[Entra ID](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id)
documentation for more information. You can set it with an environment variable
`AZURE_OPENAI_AD_TOKEN`, or pass with this parameter during initialization.
Previously called Azure Active Directory.
:param organization:
Your organization ID. See OpenAI's
[Setting Up Your Organization](https://platform.openai.com/docs/guides/production-best-practices/setting-up-your-organization)
for more information.
:param prefix:
A string to add at the beginning of each text.
:param suffix:
A string to add at the end of each text.
:param batch_size:
Number of documents to embed at once.
:param progress_bar:
If `True`, shows a progress bar when running.
:param meta_fields_to_embed:
List of metadata fields to embed along with the document text.
:param embedding_separator:
Separator used to concatenate the metadata fields to the document text.
:param timeout: The timeout for `AzureOpenAI` client calls, in seconds.
If not set, defaults to either the
`OPENAI_TIMEOUT` environment variable, or 30 seconds.
:param max_retries: Maximum number of retries to contact AzureOpenAI after an internal error.
If not set, defaults to either the `OPENAI_MAX_RETRIES` environment variable or to 5 retries.
:param default_headers: Default headers to send to the AzureOpenAI client.
:param azure_ad_token_provider: A function that returns an Azure Active Directory token, will be invoked on
every request.
:param http_client_kwargs:
A dictionary of keyword arguments to configure a custom `httpx.Client`or `httpx.AsyncClient`.
For more information, see the [HTTPX documentation](https://www.python-httpx.org/api/#client).
:param raise_on_failure:
Whether to raise an exception if the embedding request fails. If `False`, the component will log the error
and continue processing the remaining documents. If `True`, it will raise an exception on failure.
"""
# We intentionally do not call super().__init__ here because we only need to instantiate the client to interact
# with the API.
# if not provided as a parameter, azure_endpoint is read from the env var AZURE_OPENAI_ENDPOINT
azure_endpoint = azure_endpoint or os.environ.get("AZURE_OPENAI_ENDPOINT")
if not azure_endpoint:
raise ValueError("Please provide an Azure endpoint or set the environment variable AZURE_OPENAI_ENDPOINT.")
if api_key is None and azure_ad_token is None:
raise ValueError("Please provide an API key or an Azure Active Directory token.")
self.api_key = api_key # type: ignore[assignment] # mypy does not understand that api_key can be None
self.azure_ad_token = azure_ad_token
self.api_version = api_version
self.azure_endpoint = azure_endpoint
self.azure_deployment = azure_deployment
self.model = azure_deployment
self.dimensions = dimensions
self.organization = organization
self.prefix = prefix
self.suffix = suffix
self.batch_size = batch_size
self.progress_bar = progress_bar
self.meta_fields_to_embed = meta_fields_to_embed or []
self.embedding_separator = embedding_separator
self.timeout = timeout
self.max_retries = max_retries
self.default_headers = default_headers or {}
self.azure_ad_token_provider = azure_ad_token_provider
self.http_client_kwargs = http_client_kwargs
self.raise_on_failure = raise_on_failure
self.client: AzureOpenAI | None = None
self.async_client: AsyncAzureOpenAI | None = None
def _client_kwargs(self) -> dict[str, Any]:
timeout = self.timeout if self.timeout is not None else float(os.environ.get("OPENAI_TIMEOUT", "30.0"))
max_retries = (
self.max_retries if self.max_retries is not None else int(os.environ.get("OPENAI_MAX_RETRIES", "5"))
)
return {
"api_version": self.api_version,
"azure_endpoint": self.azure_endpoint,
"azure_deployment": self.azure_deployment,
"azure_ad_token_provider": self.azure_ad_token_provider,
"api_key": self.api_key.resolve_value() if self.api_key is not None else None,
"azure_ad_token": self.azure_ad_token.resolve_value() if self.azure_ad_token is not None else None,
"organization": self.organization,
"timeout": timeout,
"max_retries": max_retries,
"default_headers": self.default_headers,
}
def warm_up(self) -> None:
"""
Initializes the synchronous AzureOpenAI client.
"""
if self.client is None:
self.client = AzureOpenAI(
http_client=init_http_client(self.http_client_kwargs, async_client=False), **self._client_kwargs()
)
async def warm_up_async(self) -> None: # noqa: RUF029
"""
Initializes the asynchronous AzureOpenAI client on the serving event loop.
"""
if self.async_client is None:
self.async_client = AsyncAzureOpenAI(
http_client=init_http_client(self.http_client_kwargs, async_client=True), **self._client_kwargs()
)
def close(self) -> None:
"""
Releases the synchronous AzureOpenAI client.
"""
if self.client is not None:
self.client.close()
self.client = None
async def close_async(self) -> None:
"""
Releases the asynchronous AzureOpenAI client.
"""
if self.async_client is not None:
await self.async_client.close()
self.async_client = None
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
Dictionary with serialized data.
"""
azure_ad_token_provider_name = None
if self.azure_ad_token_provider:
azure_ad_token_provider_name = serialize_callable(self.azure_ad_token_provider)
return default_to_dict(
self,
azure_endpoint=self.azure_endpoint,
azure_deployment=self.azure_deployment,
dimensions=self.dimensions,
organization=self.organization,
api_version=self.api_version,
prefix=self.prefix,
suffix=self.suffix,
batch_size=self.batch_size,
progress_bar=self.progress_bar,
meta_fields_to_embed=self.meta_fields_to_embed,
embedding_separator=self.embedding_separator,
api_key=self.api_key,
azure_ad_token=self.azure_ad_token,
timeout=self.timeout,
max_retries=self.max_retries,
default_headers=self.default_headers,
azure_ad_token_provider=azure_ad_token_provider_name,
http_client_kwargs=self.http_client_kwargs,
raise_on_failure=self.raise_on_failure,
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "AzureOpenAIDocumentEmbedder":
"""
Deserializes the component from a dictionary.
:param data:
Dictionary to deserialize from.
:returns:
Deserialized component.
"""
serialized_azure_ad_token_provider = data["init_parameters"].get("azure_ad_token_provider")
if serialized_azure_ad_token_provider:
data["init_parameters"]["azure_ad_token_provider"] = deserialize_callable(
serialized_azure_ad_token_provider
)
return default_from_dict(cls, data)
@@ -0,0 +1,226 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import os
from typing import Any
from openai.lib.azure import AsyncAzureOpenAI, AzureADTokenProvider, AzureOpenAI
from haystack import component, default_from_dict, default_to_dict
from haystack.components.embedders import OpenAITextEmbedder
from haystack.utils import Secret, deserialize_callable, serialize_callable
from haystack.utils.http_client import init_http_client
@component
class AzureOpenAITextEmbedder(OpenAITextEmbedder):
"""
Embeds strings using OpenAI models deployed on Azure.
### Usage example
<!-- test-ignore -->
```python
from haystack.components.embedders import AzureOpenAITextEmbedder
text_to_embed = "I love pizza!"
text_embedder = AzureOpenAITextEmbedder()
print(text_embedder.run(text_to_embed))
# {'embedding': [0.017020374536514282, -0.023255806416273117, ...],
# 'meta': {'model': 'text-embedding-ada-002-v2',
# 'usage': {'prompt_tokens': 4, 'total_tokens': 4}}}
```
"""
def __init__( # noqa: PLR0913
self,
azure_endpoint: str | None = None,
api_version: str | None = "2023-05-15",
azure_deployment: str = "text-embedding-ada-002",
dimensions: int | None = None,
api_key: Secret | None = Secret.from_env_var("AZURE_OPENAI_API_KEY", strict=False),
azure_ad_token: Secret | None = Secret.from_env_var("AZURE_OPENAI_AD_TOKEN", strict=False),
organization: str | None = None,
timeout: float | None = None,
max_retries: int | None = None,
prefix: str = "",
suffix: str = "",
*,
default_headers: dict[str, str] | None = None,
azure_ad_token_provider: AzureADTokenProvider | None = None,
http_client_kwargs: dict[str, Any] | None = None,
) -> None:
"""
Creates an AzureOpenAITextEmbedder component.
:param azure_endpoint:
The endpoint of the model deployed on Azure.
:param api_version:
The version of the API to use.
:param azure_deployment:
The name of the model deployed on Azure. The default model is text-embedding-ada-002.
:param dimensions:
The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3
and later models.
:param api_key:
The Azure OpenAI API key.
You can set it with an environment variable `AZURE_OPENAI_API_KEY`, or pass with this
parameter during initialization.
:param azure_ad_token:
Microsoft Entra ID token, see Microsoft's
[Entra ID](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id)
documentation for more information. You can set it with an environment variable
`AZURE_OPENAI_AD_TOKEN`, or pass with this parameter during initialization.
Previously called Azure Active Directory.
:param organization:
Your organization ID. See OpenAI's
[Setting Up Your Organization](https://platform.openai.com/docs/guides/production-best-practices/setting-up-your-organization)
for more information.
:param timeout: The timeout for `AzureOpenAI` client calls, in seconds.
If not set, defaults to either the
`OPENAI_TIMEOUT` environment variable, or 30 seconds.
:param max_retries: Maximum number of retries to contact AzureOpenAI after an internal error.
If not set, defaults to either the `OPENAI_MAX_RETRIES` environment variable, or to 5 retries.
:param prefix:
A string to add at the beginning of each text.
:param suffix:
A string to add at the end of each text.
:param default_headers: Default headers to send to the AzureOpenAI client.
:param azure_ad_token_provider: A function that returns an Azure Active Directory token, will be invoked on
every request.
:param http_client_kwargs:
A dictionary of keyword arguments to configure a custom `httpx.Client`or `httpx.AsyncClient`.
For more information, see the [HTTPX documentation](https://www.python-httpx.org/api/#client).
"""
# We intentionally do not call super().__init__ here because we only need to instantiate the client to interact
# with the API.
# Why is this here?
# AzureOpenAI init is forcing us to use an init method that takes either base_url or azure_endpoint as not
# None init parameters. This way we accommodate the use case where env var AZURE_OPENAI_ENDPOINT is set instead
# of passing it as a parameter.
azure_endpoint = azure_endpoint or os.environ.get("AZURE_OPENAI_ENDPOINT")
if not azure_endpoint:
raise ValueError("Please provide an Azure endpoint or set the environment variable AZURE_OPENAI_ENDPOINT.")
if api_key is None and azure_ad_token is None:
raise ValueError("Please provide an API key or an Azure Active Directory token.")
self.api_key = api_key # type: ignore[assignment] # mypy does not understand that api_key can be None
self.azure_ad_token = azure_ad_token
self.api_version = api_version
self.azure_endpoint = azure_endpoint
self.azure_deployment = azure_deployment
self.model = azure_deployment
self.dimensions = dimensions
self.organization = organization
self.timeout = timeout
self.max_retries = max_retries
self.prefix = prefix
self.suffix = suffix
self.default_headers = default_headers or {}
self.azure_ad_token_provider = azure_ad_token_provider
self.http_client_kwargs = http_client_kwargs
self.client: AzureOpenAI | None = None
self.async_client: AsyncAzureOpenAI | None = None
def _client_kwargs(self) -> dict[str, Any]:
timeout = self.timeout if self.timeout is not None else float(os.environ.get("OPENAI_TIMEOUT", "30.0"))
max_retries = (
self.max_retries if self.max_retries is not None else int(os.environ.get("OPENAI_MAX_RETRIES", "5"))
)
return {
"api_version": self.api_version,
"azure_endpoint": self.azure_endpoint,
"azure_deployment": self.azure_deployment,
"azure_ad_token_provider": self.azure_ad_token_provider,
"api_key": self.api_key.resolve_value() if self.api_key is not None else None,
"azure_ad_token": self.azure_ad_token.resolve_value() if self.azure_ad_token is not None else None,
"organization": self.organization,
"timeout": timeout,
"max_retries": max_retries,
"default_headers": self.default_headers,
}
def warm_up(self) -> None:
"""
Initializes the synchronous Azure OpenAI client.
"""
if self.client is None:
self.client = AzureOpenAI(
http_client=init_http_client(self.http_client_kwargs, async_client=False), **self._client_kwargs()
)
async def warm_up_async(self) -> None: # noqa: RUF029
"""
Initializes the asynchronous Azure OpenAI client on the serving event loop.
"""
if self.async_client is None:
self.async_client = AsyncAzureOpenAI(
http_client=init_http_client(self.http_client_kwargs, async_client=True), **self._client_kwargs()
)
def close(self) -> None:
"""
Releases the synchronous Azure OpenAI client.
"""
if self.client is not None:
self.client.close()
self.client = None
async def close_async(self) -> None:
"""
Releases the asynchronous Azure OpenAI client.
"""
if self.async_client is not None:
await self.async_client.close()
self.async_client = None
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
Dictionary with serialized data.
"""
azure_ad_token_provider_name = None
if self.azure_ad_token_provider:
azure_ad_token_provider_name = serialize_callable(self.azure_ad_token_provider)
return default_to_dict(
self,
azure_endpoint=self.azure_endpoint,
azure_deployment=self.azure_deployment,
dimensions=self.dimensions,
organization=self.organization,
api_version=self.api_version,
prefix=self.prefix,
suffix=self.suffix,
api_key=self.api_key,
azure_ad_token=self.azure_ad_token,
timeout=self.timeout,
max_retries=self.max_retries,
default_headers=self.default_headers,
azure_ad_token_provider=azure_ad_token_provider_name,
http_client_kwargs=self.http_client_kwargs,
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "AzureOpenAITextEmbedder":
"""
Deserializes the component from a dictionary.
:param data:
Dictionary to deserialize from.
:returns:
Deserialized component.
"""
serialized_azure_ad_token_provider = data["init_parameters"].get("azure_ad_token_provider")
if serialized_azure_ad_token_provider:
data["init_parameters"]["azure_ad_token_provider"] = deserialize_callable(
serialized_azure_ad_token_provider
)
return default_from_dict(cls, data)
@@ -0,0 +1,194 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from dataclasses import replace
from typing import Any
from haystack import Document, component, default_from_dict, default_to_dict
from haystack.components.embedders.mock_utils import (
EmbeddingFn,
_coerce_embedding,
_deterministic_embedding,
_estimate_usage,
)
from haystack.utils import deserialize_callable, serialize_callable
@component
class MockDocumentEmbedder:
"""
A Document Embedder that returns deterministic embeddings without calling any API.
It is a drop-in replacement for real Document Embedders (such as `OpenAIDocumentEmbedder`) in tests, smoke tests,
and quick prototypes. It implements the same interface (`run`, `run_async`, serialization) but never contacts an
external service, so it is fully deterministic and free to run.
The embedding is selected based on how the component is configured:
- **Deterministic (default)**: with no configuration, each document's embedding is derived from a hash of its
(prepared) text. The same text always yields the same embedding, and different texts yield different
embeddings, so the mock works in retrieval pipelines and is reproducible across runs and processes.
- **Fixed embedding**: pass an `embedding` vector. The same vector is assigned to every document.
- **Dynamic embedding**: pass an `embedding_fn` callable that receives the (prepared) text of a document and
returns the embedding. This is useful when the embedding should depend on the input in a custom way.
Like real Document Embedders, the metadata fields listed in `meta_fields_to_embed` are concatenated with the
document content before embedding, so the deterministic embedding reflects the embedded metadata.
### Usage example
```python
from haystack import Document
from haystack.components.embedders import MockDocumentEmbedder
embedder = MockDocumentEmbedder(dimension=8)
result = embedder.run([Document(content="I love pizza!")])
print(result["documents"][0].embedding) # a deterministic list of 8 floats
```
"""
def __init__(
self,
embedding: list[float] | None = None,
*,
embedding_fn: EmbeddingFn | None = None,
dimension: int = 768,
model: str = "mock-model",
meta: dict[str, Any] | None = None,
prefix: str = "",
suffix: str = "",
meta_fields_to_embed: list[str] | None = None,
embedding_separator: str = "\n",
progress_bar: bool = False,
) -> None:
"""
Creates an instance of MockDocumentEmbedder.
:param embedding: An optional fixed embedding assigned to every document. Mutually exclusive with
`embedding_fn`. If neither is provided, a deterministic embedding is derived from each document's text.
:param embedding_fn: An optional callable that receives the prepared text of a document and returns the
embedding as a list of floats. Mutually exclusive with `embedding`. To support serialization, pass a
named function (lambdas and nested functions cannot be serialized).
:param dimension: The number of dimensions of the deterministic embedding. Ignored when `embedding` or
`embedding_fn` is provided, since their length is determined by the value or callable.
:param model: The model name reported in the metadata. Purely cosmetic; no model is loaded.
:param meta: Additional metadata merged into the output `meta`.
:param prefix: A string to add at the beginning of each text before embedding.
:param suffix: A string to add at the end of each text before embedding.
:param meta_fields_to_embed: List of metadata fields to embed along with the document text.
:param embedding_separator: Separator used to concatenate the metadata fields to the document text.
:param progress_bar: Accepted for interface compatibility with real Document Embedders and ignored.
:raises ValueError: If both `embedding` and `embedding_fn` are provided, if `dimension` is not positive, or
if `embedding` is an empty list.
:raises TypeError: If `embedding` is not a sequence of numbers.
"""
if embedding is not None and embedding_fn is not None:
raise ValueError("Pass either 'embedding' or 'embedding_fn', not both.")
if dimension <= 0:
raise ValueError("'dimension' must be a positive integer.")
self.embedding = _coerce_embedding(embedding, name="'embedding'") if embedding is not None else None
self.embedding_fn = embedding_fn
self.dimension = dimension
self.model = model
self.meta = meta or {}
self.prefix = prefix
self.suffix = suffix
self.meta_fields_to_embed = meta_fields_to_embed or []
self.embedding_separator = embedding_separator
self.progress_bar = progress_bar
self._is_warmed_up = False
def to_dict(self) -> dict[str, Any]:
"""Serialize the component to a dictionary."""
embedding_fn = serialize_callable(self.embedding_fn) if self.embedding_fn is not None else None
return default_to_dict(
self,
embedding=self.embedding,
embedding_fn=embedding_fn,
dimension=self.dimension,
model=self.model,
meta=self.meta,
prefix=self.prefix,
suffix=self.suffix,
meta_fields_to_embed=self.meta_fields_to_embed,
embedding_separator=self.embedding_separator,
progress_bar=self.progress_bar,
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> MockDocumentEmbedder:
"""Deserialize the component from a dictionary."""
init_params = data.get("init_parameters", {})
embedding_fn = init_params.get("embedding_fn")
if embedding_fn:
init_params["embedding_fn"] = deserialize_callable(embedding_fn)
return default_from_dict(cls, data)
def warm_up(self) -> None:
"""No-op warm up, provided for interface compatibility with real Embedders."""
self._is_warmed_up = True
def _prepare_text_to_embed(self, document: Document) -> str:
"""Concatenate the document content with the metadata fields to embed, mirroring real Document Embedders."""
meta_values_to_embed = [
str(document.meta[key])
for key in self.meta_fields_to_embed
if key in document.meta and document.meta[key] is not None
]
return (
self.prefix + self.embedding_separator.join([*meta_values_to_embed, document.content or ""]) + self.suffix
)
def _embed(self, text: str) -> list[float]:
"""Produce the embedding for the prepared text according to the configured mode."""
if self.embedding_fn is not None:
return _coerce_embedding(self.embedding_fn(text), name="the return value of 'embedding_fn'")
if self.embedding is not None:
return list(self.embedding)
return _deterministic_embedding(text, self.dimension)
@component.output_types(documents=list[Document], meta=dict[str, Any])
def run(self, documents: list[Document]) -> dict[str, Any]:
"""
Return the input documents with deterministic embeddings added, without calling any API.
:param documents: A list of documents to embed.
:returns: A dictionary with the following keys:
- `documents`: A list of documents with embeddings.
- `meta`: Metadata about the (mock) model.
:raises TypeError: If `documents` is not a list of `Document` objects.
"""
self.warm_up()
if not isinstance(documents, list) or (documents and not isinstance(documents[0], Document)):
raise TypeError(
"MockDocumentEmbedder expects a list of Documents as input. "
"In case you want to embed a string, please use the MockTextEmbedder."
)
texts_to_embed = [self._prepare_text_to_embed(document) for document in documents]
new_documents = [
replace(document, embedding=self._embed(text))
for document, text in zip(documents, texts_to_embed, strict=True)
]
meta: dict[str, Any] = {"model": self.model, "usage": _estimate_usage(texts_to_embed)}
meta.update(self.meta)
return {"documents": new_documents, "meta": meta}
@component.output_types(documents=list[Document], meta=dict[str, Any])
async def run_async(self, documents: list[Document]) -> dict[str, Any]:
"""
Asynchronously return the input documents with deterministic embeddings added, without calling any API.
:param documents: A list of documents to embed.
:returns: A dictionary with the following keys:
- `documents`: A list of documents with embeddings.
- `meta`: Metadata about the (mock) model.
:raises TypeError: If `documents` is not a list of `Document` objects.
"""
return self.run(documents=documents)
@@ -0,0 +1,161 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from typing import Any
from haystack import component, default_from_dict, default_to_dict
from haystack.components.embedders.mock_utils import (
EmbeddingFn,
_coerce_embedding,
_deterministic_embedding,
_estimate_usage,
)
from haystack.utils import deserialize_callable, serialize_callable
@component
class MockTextEmbedder:
"""
A Text Embedder that returns deterministic embeddings without calling any API.
It is a drop-in replacement for real Text Embedders (such as `OpenAITextEmbedder`) in tests, smoke tests, and
quick prototypes. It implements the same interface (`run`, `run_async`, serialization) but never contacts an
external service, so it is fully deterministic and free to run.
The embedding is selected based on how the component is configured:
- **Deterministic (default)**: with no configuration, the embedding is derived from a hash of the input text.
The same text always yields the same embedding, and different texts yield different embeddings, so the mock
works in retrieval pipelines and is reproducible across runs and processes.
- **Fixed embedding**: pass an `embedding` vector. The same vector is returned for every input.
- **Dynamic embedding**: pass an `embedding_fn` callable that receives the (prepared) text and returns the
embedding. This is useful when the embedding should depend on the input in a custom way.
### Usage example
```python
from haystack.components.embedders import MockTextEmbedder
embedder = MockTextEmbedder(dimension=8)
result = embedder.run("I love pizza!")
print(result["embedding"]) # a deterministic list of 8 floats
```
"""
def __init__(
self,
embedding: list[float] | None = None,
*,
embedding_fn: EmbeddingFn | None = None,
dimension: int = 768,
model: str = "mock-model",
meta: dict[str, Any] | None = None,
prefix: str = "",
suffix: str = "",
) -> None:
"""
Creates an instance of MockTextEmbedder.
:param embedding: An optional fixed embedding returned for every input. Mutually exclusive with
`embedding_fn`. If neither is provided, a deterministic embedding is derived from the input text.
:param embedding_fn: An optional callable that receives the prepared text (after `prefix`/`suffix` are
applied) and returns the embedding as a list of floats. Mutually exclusive with `embedding`. To support
serialization, pass a named function (lambdas and nested functions cannot be serialized).
:param dimension: The number of dimensions of the deterministic embedding. Ignored when `embedding` or
`embedding_fn` is provided, since their length is determined by the value or callable.
:param model: The model name reported in the metadata. Purely cosmetic; no model is loaded.
:param meta: Additional metadata merged into the output `meta`.
:param prefix: A string to add at the beginning of the text before embedding.
:param suffix: A string to add at the end of the text before embedding.
:raises ValueError: If both `embedding` and `embedding_fn` are provided, if `dimension` is not positive, or
if `embedding` is an empty list.
:raises TypeError: If `embedding` is not a sequence of numbers.
"""
if embedding is not None and embedding_fn is not None:
raise ValueError("Pass either 'embedding' or 'embedding_fn', not both.")
if dimension <= 0:
raise ValueError("'dimension' must be a positive integer.")
self.embedding = _coerce_embedding(embedding, name="'embedding'") if embedding is not None else None
self.embedding_fn = embedding_fn
self.dimension = dimension
self.model = model
self.meta = meta or {}
self.prefix = prefix
self.suffix = suffix
self._is_warmed_up = False
def to_dict(self) -> dict[str, Any]:
"""Serialize the component to a dictionary."""
embedding_fn = serialize_callable(self.embedding_fn) if self.embedding_fn is not None else None
return default_to_dict(
self,
embedding=self.embedding,
embedding_fn=embedding_fn,
dimension=self.dimension,
model=self.model,
meta=self.meta,
prefix=self.prefix,
suffix=self.suffix,
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> MockTextEmbedder:
"""Deserialize the component from a dictionary."""
init_params = data.get("init_parameters", {})
embedding_fn = init_params.get("embedding_fn")
if embedding_fn:
init_params["embedding_fn"] = deserialize_callable(embedding_fn)
return default_from_dict(cls, data)
def warm_up(self) -> None:
"""No-op warm up, provided for interface compatibility with real Embedders."""
self._is_warmed_up = True
def _embed(self, text: str) -> list[float]:
"""Produce the embedding for the prepared text according to the configured mode."""
if self.embedding_fn is not None:
return _coerce_embedding(self.embedding_fn(text), name="the return value of 'embedding_fn'")
if self.embedding is not None:
return list(self.embedding)
return _deterministic_embedding(text, self.dimension)
@component.output_types(embedding=list[float], meta=dict[str, Any])
def run(self, text: str) -> dict[str, Any]:
"""
Return a deterministic embedding for the input text without calling any API.
:param text: The text to embed.
:returns: A dictionary with the following keys:
- `embedding`: The embedding of the input text.
- `meta`: Metadata about the (mock) model.
:raises TypeError: If `text` is not a string.
"""
self.warm_up()
if not isinstance(text, str):
raise TypeError(
"MockTextEmbedder expects a string as an input. "
"In case you want to embed a list of Documents, please use the MockDocumentEmbedder."
)
text_to_embed = self.prefix + text + self.suffix
meta: dict[str, Any] = {"model": self.model, "usage": _estimate_usage([text_to_embed])}
meta.update(self.meta)
return {"embedding": self._embed(text_to_embed), "meta": meta}
@component.output_types(embedding=list[float], meta=dict[str, Any])
async def run_async(self, text: str) -> dict[str, Any]:
"""
Asynchronously return a deterministic embedding for the input text without calling any API.
:param text: The text to embed.
:returns: A dictionary with the following keys:
- `embedding`: The embedding of the input text.
- `meta`: Metadata about the (mock) model.
:raises TypeError: If `text` is not a string.
"""
return self.run(text=text)
@@ -0,0 +1,63 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import hashlib
import math
import random
from collections.abc import Callable
# A callable that derives an embedding from the (prepared) text to embed. It receives the text and returns the
# embedding as a list of floats.
EmbeddingFn = Callable[[str], list[float]]
def _l2_normalize(vector: list[float]) -> list[float]:
"""Return the L2-normalized vector, so that mock embeddings behave like real (unit-length) ones."""
norm = math.sqrt(sum(value * value for value in vector))
if norm == 0.0:
return vector
return [value / norm for value in vector]
def _deterministic_embedding(text: str, dimension: int) -> list[float]:
"""
Generate a deterministic, unit-length embedding from the given text.
The same text always yields the same embedding, and different texts yield different embeddings, which makes mock
embeddings usable in retrieval pipelines and reproducible across runs and processes. The seed is derived from a
SHA-256 digest of the text (not the process-salted built-in `hash`) to guarantee cross-process stability.
:param text: The text to embed.
:param dimension: The number of dimensions of the resulting embedding.
:returns: A deterministic, L2-normalized embedding of length `dimension`.
"""
digest = hashlib.sha256(text.encode("utf-8")).digest()
seed = int.from_bytes(digest[:8], "big")
rng = random.Random(seed)
vector = [rng.uniform(-1.0, 1.0) for _ in range(dimension)]
return _l2_normalize(vector)
def _coerce_embedding(value: object, *, name: str) -> list[float]:
"""
Validate that `value` is a non-empty sequence of numbers and coerce it into a list of floats.
:param value: The value to validate, e.g. a user-provided fixed embedding or the output of an `embedding_fn`.
:param name: How to refer to `value` in error messages, e.g. ``"'embedding'"``.
"""
if not isinstance(value, (list, tuple)) or not all(isinstance(item, (int, float)) for item in value):
raise TypeError(f"{name} must be a sequence of numbers, got {type(value)}.")
if len(value) == 0:
raise ValueError(f"{name} must not be empty.")
return [float(item) for item in value]
def _estimate_usage(texts: list[str]) -> dict[str, int]:
"""
Roughly estimate token usage as whitespace-separated word counts.
This is an approximation (not real tokenization) intended to give downstream code realistic-looking metadata.
"""
prompt_tokens = sum(len(text.split()) for text in texts)
return {"prompt_tokens": prompt_tokens, "total_tokens": prompt_tokens}
@@ -0,0 +1,390 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import os
from dataclasses import replace
from typing import Any
from more_itertools import batched
from openai import APIError, AsyncOpenAI, OpenAI
from tqdm import tqdm
from tqdm.asyncio import tqdm as async_tqdm
from haystack import Document, component, default_from_dict, default_to_dict, logging
from haystack.utils import Secret
from haystack.utils.http_client import init_http_client
logger = logging.getLogger(__name__)
@component
class OpenAIDocumentEmbedder:
"""
Computes document embeddings using OpenAI models.
### Usage example
<!-- test-ignore -->
```python
from haystack import Document
from haystack.components.embedders import OpenAIDocumentEmbedder
doc = Document(content="I love pizza!")
document_embedder = OpenAIDocumentEmbedder()
result = document_embedder.run([doc])
print(result['documents'][0].embedding)
# [0.017020374536514282, -0.023255806416273117, ...]
```
"""
def __init__( # noqa: PLR0913 (too-many-arguments)
self,
api_key: Secret = Secret.from_env_var("OPENAI_API_KEY"),
model: str = "text-embedding-ada-002",
dimensions: int | None = None,
api_base_url: str | None = None,
organization: str | None = None,
prefix: str = "",
suffix: str = "",
batch_size: int = 32,
progress_bar: bool = True,
meta_fields_to_embed: list[str] | None = None,
embedding_separator: str = "\n",
timeout: float | None = None,
max_retries: int | None = None,
http_client_kwargs: dict[str, Any] | None = None,
*,
raise_on_failure: bool = False,
) -> None:
"""
Creates an OpenAIDocumentEmbedder component.
Before initializing the component, you can set the 'OPENAI_TIMEOUT' and 'OPENAI_MAX_RETRIES'
environment variables to override the `timeout` and `max_retries` parameters respectively
in the OpenAI client.
:param api_key:
The OpenAI API key.
You can set it with an environment variable `OPENAI_API_KEY`, or pass with this parameter
during initialization.
:param model:
The name of the model to use for calculating embeddings.
The default model is `text-embedding-ada-002`.
:param dimensions:
The number of dimensions of the resulting embeddings. Only `text-embedding-3` and
later models support this parameter.
:param api_base_url:
Overrides the default base URL for all HTTP requests.
:param organization:
Your OpenAI organization ID. See OpenAI's
[Setting Up Your Organization](https://platform.openai.com/docs/guides/production-best-practices/setting-up-your-organization)
for more information.
:param prefix:
A string to add at the beginning of each text.
:param suffix:
A string to add at the end of each text.
:param batch_size:
Number of documents to embed at once.
:param progress_bar:
If `True`, shows a progress bar when running.
:param meta_fields_to_embed:
List of metadata fields to embed along with the document text.
:param embedding_separator:
Separator used to concatenate the metadata fields to the document text.
:param timeout:
Timeout for OpenAI client calls. If not set, it defaults to either the
`OPENAI_TIMEOUT` environment variable, or 30 seconds.
:param max_retries:
Maximum number of retries to contact OpenAI after an internal error.
If not set, it defaults to either the `OPENAI_MAX_RETRIES` environment variable, or 5 retries.
:param http_client_kwargs:
A dictionary of keyword arguments to configure a custom `httpx.Client`or `httpx.AsyncClient`.
For more information, see the [HTTPX documentation](https://www.python-httpx.org/api/#client).
:param raise_on_failure:
Whether to raise an exception if the embedding request fails. If `False`, the component will log the error
and continue processing the remaining documents. If `True`, it will raise an exception on failure.
"""
self.api_key = api_key
self.model = model
self.dimensions = dimensions
self.api_base_url = api_base_url
self.organization = organization
self.prefix = prefix
self.suffix = suffix
self.batch_size = batch_size
self.progress_bar = progress_bar
self.meta_fields_to_embed = meta_fields_to_embed or []
self.embedding_separator = embedding_separator
self.timeout = timeout
self.max_retries = max_retries
self.http_client_kwargs = http_client_kwargs
self.raise_on_failure = raise_on_failure
self.client: OpenAI | None = None
self.async_client: AsyncOpenAI | None = None
def _client_kwargs(self) -> dict[str, Any]:
timeout = self.timeout if self.timeout is not None else float(os.environ.get("OPENAI_TIMEOUT", "30.0"))
max_retries = (
self.max_retries if self.max_retries is not None else int(os.environ.get("OPENAI_MAX_RETRIES", "5"))
)
return {
"api_key": self.api_key.resolve_value(),
"organization": self.organization,
"base_url": self.api_base_url,
"timeout": timeout,
"max_retries": max_retries,
}
def warm_up(self) -> None:
"""
Initializes the synchronous OpenAI client.
"""
if self.client is None:
self.client = OpenAI(
http_client=init_http_client(self.http_client_kwargs, async_client=False), **self._client_kwargs()
)
async def warm_up_async(self) -> None: # noqa: RUF029
"""
Initializes the asynchronous OpenAI client on the serving event loop.
"""
if self.async_client is None:
self.async_client = AsyncOpenAI(
http_client=init_http_client(self.http_client_kwargs, async_client=True), **self._client_kwargs()
)
def close(self) -> None:
"""
Releases the synchronous OpenAI client.
"""
if self.client is not None:
self.client.close()
self.client = None
async def close_async(self) -> None:
"""
Releases the asynchronous OpenAI client.
"""
if self.async_client is not None:
await self.async_client.close()
self.async_client = None
def _get_telemetry_data(self) -> dict[str, Any]:
"""
Data that is sent to Posthog for usage analytics.
"""
return {"model": self.model}
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
Dictionary with serialized data.
"""
return default_to_dict(
self,
api_key=self.api_key,
model=self.model,
dimensions=self.dimensions,
api_base_url=self.api_base_url,
organization=self.organization,
prefix=self.prefix,
suffix=self.suffix,
batch_size=self.batch_size,
progress_bar=self.progress_bar,
meta_fields_to_embed=self.meta_fields_to_embed,
embedding_separator=self.embedding_separator,
timeout=self.timeout,
max_retries=self.max_retries,
http_client_kwargs=self.http_client_kwargs,
raise_on_failure=self.raise_on_failure,
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "OpenAIDocumentEmbedder":
"""
Deserializes the component from a dictionary.
:param data:
Dictionary to deserialize from.
:returns:
Deserialized component.
"""
return default_from_dict(cls, data)
def _prepare_texts_to_embed(self, documents: list[Document]) -> dict[str, str]:
"""
Prepare the texts to embed by concatenating the Document text with the metadata fields to embed.
"""
texts_to_embed = {}
for doc in documents:
meta_values_to_embed = [
str(doc.meta[key]) for key in self.meta_fields_to_embed if key in doc.meta and doc.meta[key] is not None
]
texts_to_embed[doc.id] = (
self.prefix + self.embedding_separator.join(meta_values_to_embed + [doc.content or ""]) + self.suffix
)
return texts_to_embed
def _embed_batch(
self, texts_to_embed: dict[str, str], batch_size: int
) -> tuple[dict[str, list[float]], dict[str, Any]]:
"""
Embed a list of texts in batches.
"""
doc_ids_to_embeddings: dict[str, list[float]] = {}
meta: dict[str, Any] = {}
for batch in tqdm(
batched(texts_to_embed.items(), batch_size), disable=not self.progress_bar, desc="Calculating embeddings"
):
args: dict[str, Any] = {"model": self.model, "input": [b[1] for b in batch], "encoding_format": "float"}
if self.dimensions is not None:
args["dimensions"] = self.dimensions
try:
# this method is invoked after warm_up, so client is not None
assert self.client is not None
response = self.client.embeddings.create(**args)
except APIError as exc:
ids = ", ".join(b[0] for b in batch)
msg = "Failed embedding of documents {ids} caused by {exc}"
logger.exception(msg, ids=ids, exc=exc)
if self.raise_on_failure:
raise exc
continue
embeddings = [el.embedding for el in response.data]
doc_ids_to_embeddings.update(dict(zip((b[0] for b in batch), embeddings, strict=True)))
if "model" not in meta:
meta["model"] = response.model
if "usage" not in meta:
meta["usage"] = dict(response.usage)
else:
meta["usage"]["prompt_tokens"] += response.usage.prompt_tokens
meta["usage"]["total_tokens"] += response.usage.total_tokens
return doc_ids_to_embeddings, meta
async def _embed_batch_async(
self, texts_to_embed: dict[str, str], batch_size: int
) -> tuple[dict[str, list[float]], dict[str, Any]]:
"""
Embed a list of texts in batches asynchronously.
"""
doc_ids_to_embeddings: dict[str, list[float]] = {}
meta: dict[str, Any] = {}
batches = list(batched(texts_to_embed.items(), batch_size))
if self.progress_bar:
batches = async_tqdm(batches, desc="Calculating embeddings")
for batch in batches:
args: dict[str, Any] = {"model": self.model, "input": [b[1] for b in batch]}
if self.dimensions is not None:
args["dimensions"] = self.dimensions
try:
# this method is invoked after warm_up_async, so async_client is not None
assert self.async_client is not None
response = await self.async_client.embeddings.create(**args)
except APIError as exc:
ids = ", ".join(b[0] for b in batch)
msg = "Failed embedding of documents {ids} caused by {exc}"
logger.exception(msg, ids=ids, exc=exc)
if self.raise_on_failure:
raise exc
continue
embeddings = [el.embedding for el in response.data]
doc_ids_to_embeddings.update(dict(zip((b[0] for b in batch), embeddings, strict=True)))
if "model" not in meta:
meta["model"] = response.model
if "usage" not in meta:
meta["usage"] = dict(response.usage)
else:
meta["usage"]["prompt_tokens"] += response.usage.prompt_tokens
meta["usage"]["total_tokens"] += response.usage.total_tokens
return doc_ids_to_embeddings, meta
@component.output_types(documents=list[Document], meta=dict[str, Any])
def run(self, documents: list[Document]) -> dict[str, Any]:
"""
Embeds a list of documents.
:param documents:
A list of documents to embed.
:returns:
A dictionary with the following keys:
- `documents`: A list of documents with embeddings.
- `meta`: Information about the usage of the model.
"""
if not isinstance(documents, list) or documents and not isinstance(documents[0], Document):
raise TypeError(
"OpenAIDocumentEmbedder expects a list of Documents as input."
"In case you want to embed a string, please use the OpenAITextEmbedder."
)
self.warm_up()
texts_to_embed = self._prepare_texts_to_embed(documents=documents)
doc_ids_to_embeddings, meta = self._embed_batch(texts_to_embed=texts_to_embed, batch_size=self.batch_size)
new_documents = []
for doc in documents:
if doc.id in doc_ids_to_embeddings:
new_documents.append(replace(doc, embedding=doc_ids_to_embeddings[doc.id]))
else:
new_documents.append(replace(doc))
return {"documents": new_documents, "meta": meta}
@component.output_types(documents=list[Document], meta=dict[str, Any])
async def run_async(self, documents: list[Document]) -> dict[str, Any]:
"""
Embeds a list of documents asynchronously.
:param documents:
A list of documents to embed.
:returns:
A dictionary with the following keys:
- `documents`: A list of documents with embeddings.
- `meta`: Information about the usage of the model.
"""
if not isinstance(documents, list) or documents and not isinstance(documents[0], Document):
raise TypeError(
"OpenAIDocumentEmbedder expects a list of Documents as input. "
"In case you want to embed a string, please use the OpenAITextEmbedder."
)
await self.warm_up_async()
texts_to_embed = self._prepare_texts_to_embed(documents=documents)
doc_ids_to_embeddings, meta = await self._embed_batch_async(
texts_to_embed=texts_to_embed, batch_size=self.batch_size
)
new_documents = []
for doc in documents:
if doc.id in doc_ids_to_embeddings:
new_documents.append(replace(doc, embedding=doc_ids_to_embeddings[doc.id]))
else:
new_documents.append(replace(doc))
return {"documents": new_documents, "meta": meta}
@@ -0,0 +1,245 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import os
from typing import Any
from openai import AsyncOpenAI, OpenAI
from openai.types import CreateEmbeddingResponse
from haystack import component, default_from_dict, default_to_dict
from haystack.utils import Secret
from haystack.utils.http_client import init_http_client
@component
class OpenAITextEmbedder:
"""
Embeds strings using OpenAI models.
You can use it to embed user query and send it to an embedding Retriever.
### Usage example
<!-- test-ignore -->
```python
from haystack.components.embedders import OpenAITextEmbedder
text_to_embed = "I love pizza!"
text_embedder = OpenAITextEmbedder()
print(text_embedder.run(text_to_embed))
# {'embedding': [0.017020374536514282, -0.023255806416273117, ...],
# 'meta': {'model': 'text-embedding-ada-002-v2',
# 'usage': {'prompt_tokens': 4, 'total_tokens': 4}}}
```
"""
def __init__(
self,
api_key: Secret = Secret.from_env_var("OPENAI_API_KEY"),
model: str = "text-embedding-ada-002",
dimensions: int | None = None,
api_base_url: str | None = None,
organization: str | None = None,
prefix: str = "",
suffix: str = "",
timeout: float | None = None,
max_retries: int | None = None,
http_client_kwargs: dict[str, Any] | None = None,
) -> None:
"""
Creates an OpenAITextEmbedder component.
Before initializing the component, you can set the 'OPENAI_TIMEOUT' and 'OPENAI_MAX_RETRIES'
environment variables to override the `timeout` and `max_retries` parameters respectively
in the OpenAI client.
:param api_key:
The OpenAI API key.
You can set it with an environment variable `OPENAI_API_KEY`, or pass with this parameter
during initialization.
:param model:
The name of the model to use for calculating embeddings.
The default model is `text-embedding-ada-002`.
:param dimensions:
The number of dimensions of the resulting embeddings. Only `text-embedding-3` and
later models support this parameter.
:param api_base_url:
Overrides default base URL for all HTTP requests.
:param organization:
Your organization ID. See OpenAI's
[production best practices](https://platform.openai.com/docs/guides/production-best-practices/setting-up-your-organization)
for more information.
:param prefix:
A string to add at the beginning of each text to embed.
:param suffix:
A string to add at the end of each text to embed.
:param timeout:
Timeout for OpenAI client calls. If not set, it defaults to either the
`OPENAI_TIMEOUT` environment variable, or 30 seconds.
:param max_retries:
Maximum number of retries to contact OpenAI after an internal error.
If not set, it defaults to either the `OPENAI_MAX_RETRIES` environment variable, or set to 5.
:param http_client_kwargs:
A dictionary of keyword arguments to configure a custom `httpx.Client`or `httpx.AsyncClient`.
For more information, see the [HTTPX documentation](https://www.python-httpx.org/api/#client).
"""
self.model = model
self.dimensions = dimensions
self.api_base_url = api_base_url
self.organization = organization
self.prefix = prefix
self.suffix = suffix
self.api_key = api_key
self.timeout = timeout
self.max_retries = max_retries
self.http_client_kwargs = http_client_kwargs
self.client: OpenAI | None = None
self.async_client: AsyncOpenAI | None = None
def _client_kwargs(self) -> dict[str, Any]:
timeout = self.timeout if self.timeout is not None else float(os.environ.get("OPENAI_TIMEOUT", "30.0"))
max_retries = (
self.max_retries if self.max_retries is not None else int(os.environ.get("OPENAI_MAX_RETRIES", "5"))
)
return {
"api_key": self.api_key.resolve_value(),
"organization": self.organization,
"base_url": self.api_base_url,
"timeout": timeout,
"max_retries": max_retries,
}
def warm_up(self) -> None:
"""
Initializes the synchronous OpenAI client.
"""
if self.client is None:
self.client = OpenAI(
http_client=init_http_client(self.http_client_kwargs, async_client=False), **self._client_kwargs()
)
async def warm_up_async(self) -> None: # noqa: RUF029
"""
Initializes the asynchronous OpenAI client on the serving event loop.
"""
if self.async_client is None:
self.async_client = AsyncOpenAI(
http_client=init_http_client(self.http_client_kwargs, async_client=True), **self._client_kwargs()
)
def close(self) -> None:
"""
Releases the synchronous OpenAI client.
"""
if self.client is not None:
self.client.close()
self.client = None
async def close_async(self) -> None:
"""
Releases the asynchronous OpenAI client.
"""
if self.async_client is not None:
await self.async_client.close()
self.async_client = None
def _get_telemetry_data(self) -> dict[str, Any]:
"""
Data that is sent to Posthog for usage analytics.
"""
return {"model": self.model}
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
Dictionary with serialized data.
"""
return default_to_dict(
self,
api_key=self.api_key,
model=self.model,
dimensions=self.dimensions,
api_base_url=self.api_base_url,
organization=self.organization,
prefix=self.prefix,
suffix=self.suffix,
timeout=self.timeout,
max_retries=self.max_retries,
http_client_kwargs=self.http_client_kwargs,
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "OpenAITextEmbedder":
"""
Deserializes the component from a dictionary.
:param data:
Dictionary to deserialize from.
:returns:
Deserialized component.
"""
return default_from_dict(cls, data)
def _prepare_input(self, text: str) -> dict[str, Any]:
if not isinstance(text, str):
raise TypeError(
"OpenAITextEmbedder expects a string as an input."
"In case you want to embed a list of Documents, please use the OpenAIDocumentEmbedder."
)
text_to_embed = self.prefix + text + self.suffix
kwargs: dict[str, Any] = {"model": self.model, "input": text_to_embed, "encoding_format": "float"}
if self.dimensions is not None:
kwargs["dimensions"] = self.dimensions
return kwargs
def _prepare_output(self, result: CreateEmbeddingResponse) -> dict[str, Any]:
return {"embedding": result.data[0].embedding, "meta": {"model": result.model, "usage": dict(result.usage)}}
@component.output_types(embedding=list[float], meta=dict[str, Any])
def run(self, text: str) -> dict[str, Any]:
"""
Embeds a single string.
:param text:
Text to embed.
:returns:
A dictionary with the following keys:
- `embedding`: The embedding of the input text.
- `meta`: Information about the usage of the model.
"""
self.warm_up()
create_kwargs = self._prepare_input(text=text)
assert self.client is not None # mypy: client is built by warm_up above
response = self.client.embeddings.create(**create_kwargs)
return self._prepare_output(result=response)
@component.output_types(embedding=list[float], meta=dict[str, Any])
async def run_async(self, text: str) -> dict[str, Any]:
"""
Asynchronously embed a single string.
This is the asynchronous version of the `run` method. It has the same parameters and return values
but can be used with `await` in async code.
:param text:
Text to embed.
:returns:
A dictionary with the following keys:
- `embedding`: The embedding of the input text.
- `meta`: Information about the usage of the model.
"""
await self.warm_up_async()
create_kwargs = self._prepare_input(text=text)
assert self.async_client is not None # mypy: async_client is built by warm_up_async above
response = await self.async_client.embeddings.create(**create_kwargs)
return self._prepare_output(result=response)
@@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from .protocol import DocumentEmbedder, TextEmbedder
__all__ = ["DocumentEmbedder", "TextEmbedder"]
@@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from typing import Any, Protocol
from haystack import Document
class TextEmbedder(Protocol):
"""
Protocol for Text Embedders.
"""
def run(self, text: str) -> dict[str, Any]:
"""
Generate embeddings for the input text.
Implementing classes may accept additional optional parameters in their run method.
For example: `def run (self, text: str, param_a="default", param_b="another_default")`.
:param text:
The input text to be embedded.
:returns:
A dictionary containing the keys:
- 'embedding', which is expected to be a list[float] representing the embedding.
- any optional keys such as 'metadata'.
"""
...
class DocumentEmbedder(Protocol):
"""
Protocol for Document Embedders.
"""
def run(self, documents: list[Document]) -> dict[str, Any]:
"""
Generate embeddings for the input documents.
Implementing classes may accept additional optional parameters in their run method.
For example: `def run (self, documents: list[Document], param_a="default", param_b="another_default")`.
:param documents:
The input documents to be embedded.
:returns:
A dictionary containing the keys:
- 'documents', which is expected to be a list[Document] with embeddings added to each document.
- any optional keys such as 'metadata'.
"""
...