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
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:
@@ -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'.
|
||||
"""
|
||||
...
|
||||
Reference in New Issue
Block a user