huggingface-api-haystack 0.5.0__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/CHANGELOG.md +20 -1
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/PKG-INFO +8 -4
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/README.md +4 -1
- huggingface_api_haystack-1.0.0/docker-compose.yml +14 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/pydoc/config_docusaurus.yml +2 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/pyproject.toml +1 -1
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/common/huggingface_api/utils.py +41 -8
- huggingface_api_haystack-1.0.0/src/haystack_integrations/components/embedders/huggingface_api/__init__.py +14 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/embedders/huggingface_api/document_embedder.py +54 -8
- huggingface_api_haystack-1.0.0/src/haystack_integrations/components/embedders/huggingface_api/sparse_document_embedder.py +199 -0
- huggingface_api_haystack-1.0.0/src/haystack_integrations/components/embedders/huggingface_api/sparse_embedding_utils.py +72 -0
- huggingface_api_haystack-1.0.0/src/haystack_integrations/components/embedders/huggingface_api/sparse_text_embedder.py +116 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/embedders/huggingface_api/text_embedder.py +40 -3
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/generators/huggingface_api/chat/chat_generator.py +55 -22
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/test_chat_generator.py +197 -78
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/test_document_embedder.py +161 -30
- huggingface_api_haystack-1.0.0/tests/test_sparse_document_embedder.py +342 -0
- huggingface_api_haystack-1.0.0/tests/test_sparse_text_embedder.py +263 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/test_text_embedder.py +153 -28
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/test_utils.py +35 -1
- huggingface_api_haystack-0.5.0/src/haystack_integrations/components/embedders/huggingface_api/__init__.py +0 -7
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/.gitignore +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/LICENSE.txt +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/common/huggingface_api/__init__.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/common/py.typed +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/embedders/py.typed +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/generators/huggingface_api/__init__.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/generators/huggingface_api/chat/__init__.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/generators/py.typed +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/rankers/huggingface_api/__init__.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/rankers/huggingface_api/ranker.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/rankers/py.typed +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/__init__.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/conftest.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/test_files/apple.jpg +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/test_ranker.py +0 -0
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [integrations/huggingface_api-v0.6.0] - 2026-08-27
|
|
4
|
+
|
|
5
|
+
### 🚀 Features
|
|
6
|
+
|
|
7
|
+
- *(huggingface_api)* Add SPLADE sparse embedder components (#3835)
|
|
8
|
+
|
|
9
|
+
## [integrations/huggingface_api-v0.5.0] - 2026-08-24
|
|
10
|
+
|
|
11
|
+
### 🐛 Bug Fixes
|
|
12
|
+
|
|
13
|
+
- Fix new issues raised by ruff 0.16.0 (#3670)
|
|
14
|
+
- Serialize missing init params in to_dict (transformers, huggingface_api, ragas) (#3808)
|
|
15
|
+
|
|
16
|
+
### 🧹 Chores
|
|
17
|
+
|
|
18
|
+
- Re-standardize READMEs (#3768)
|
|
19
|
+
- Clarify how generation_kwargs passed in run are handled (#3805)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## [integrations/huggingface_api-v0.4.0] - 2026-07-09
|
|
4
23
|
|
|
5
24
|
### 🧪 Testing
|
|
6
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: huggingface-api-haystack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Haystack integration for Hugging Face API
|
|
5
5
|
Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/huggingface_api#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
|
|
@@ -19,8 +19,9 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
19
19
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
20
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
|
-
Requires-Dist: haystack-ai>=
|
|
23
|
-
Requires-Dist:
|
|
22
|
+
Requires-Dist: haystack-ai>=3.0.0
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Requires-Dist: huggingface-hub[inference]>=1.0.0
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
25
26
|
|
|
26
27
|
# huggingface-api-haystack
|
|
@@ -37,4 +38,7 @@ Description-Content-Type: text/markdown
|
|
|
37
38
|
|
|
38
39
|
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
|
|
39
40
|
|
|
40
|
-
To run integration tests locally, you need
|
|
41
|
+
To run integration tests locally, you need a Docker container running a Text Embeddings Inference (TEI) server with
|
|
42
|
+
a sparse embedding model. Use the provided `docker-compose.yml` file to start the container: `docker compose up -d`.
|
|
43
|
+
|
|
44
|
+
The tests that call the Hugging Face Inference API are skipped unless `HF_TOKEN` is set as an evironment variable.
|
|
@@ -12,4 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
|
|
14
14
|
|
|
15
|
-
To run integration tests locally, you need
|
|
15
|
+
To run integration tests locally, you need a Docker container running a Text Embeddings Inference (TEI) server with
|
|
16
|
+
a sparse embedding model. Use the provided `docker-compose.yml` file to start the container: `docker compose up -d`.
|
|
17
|
+
|
|
18
|
+
The tests that call the Hugging Face Inference API are skipped unless `HF_TOKEN` is set as an evironment variable.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
services:
|
|
2
|
+
tei-sparse:
|
|
3
|
+
image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.9.3
|
|
4
|
+
command: ["--model-id", "opensearch-project/opensearch-neural-sparse-encoding-doc-v2-mini", "--pooling", "splade"]
|
|
5
|
+
ports:
|
|
6
|
+
- "8080:80"
|
|
7
|
+
environment:
|
|
8
|
+
- HF_TOKEN
|
|
9
|
+
volumes:
|
|
10
|
+
- tei-data:/data
|
|
11
|
+
restart: no
|
|
12
|
+
|
|
13
|
+
volumes:
|
|
14
|
+
tei-data:
|
{huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/pydoc/config_docusaurus.yml
RENAMED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
loaders:
|
|
2
2
|
- modules:
|
|
3
3
|
- haystack_integrations.components.embedders.huggingface_api.document_embedder
|
|
4
|
+
- haystack_integrations.components.embedders.huggingface_api.sparse_document_embedder
|
|
5
|
+
- haystack_integrations.components.embedders.huggingface_api.sparse_text_embedder
|
|
4
6
|
- haystack_integrations.components.embedders.huggingface_api.text_embedder
|
|
5
7
|
- haystack_integrations.components.generators.huggingface_api.chat.chat_generator
|
|
6
8
|
- haystack_integrations.components.rankers.huggingface_api.ranker
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
24
24
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
25
25
|
]
|
|
26
|
-
dependencies = ["haystack-ai>=
|
|
26
|
+
dependencies = ["haystack-ai>=3.0.0", "httpx>=0.27.0", "huggingface_hub[inference]>=1.0.0"]
|
|
27
27
|
|
|
28
28
|
[project.urls]
|
|
29
29
|
Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/huggingface_api#readme"
|
|
@@ -7,6 +7,7 @@ from enum import Enum
|
|
|
7
7
|
from haystack.utils import Secret
|
|
8
8
|
from huggingface_hub import HfApi
|
|
9
9
|
from huggingface_hub.errors import RepositoryNotFoundError
|
|
10
|
+
from huggingface_hub.utils import build_hf_headers, get_async_session, hf_raise_for_status
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class HFGenerationAPIType(Enum):
|
|
@@ -82,14 +83,15 @@ class HFModelType(Enum):
|
|
|
82
83
|
|
|
83
84
|
def _check_valid_model(model_id: str, model_type: HFModelType, token: Secret | None) -> None:
|
|
84
85
|
"""
|
|
85
|
-
Check if the provided model ID corresponds to a valid model on
|
|
86
|
+
Check if the provided model ID corresponds to a valid model on Hugging Face Hub.
|
|
86
87
|
|
|
87
|
-
Also check if the model
|
|
88
|
+
Also check if the model supports the requested embedding or generation task.
|
|
88
89
|
|
|
89
|
-
:param model_id: A string representing the
|
|
90
|
-
:param model_type:
|
|
91
|
-
:param token: The optional authentication
|
|
92
|
-
:raises ValueError: If the model is not found or
|
|
90
|
+
:param model_id: A string representing the Hugging Face model ID.
|
|
91
|
+
:param model_type: The model type, `HFModelType.EMBEDDING` or `HFModelType.GENERATION`.
|
|
92
|
+
:param token: The optional authentication secret.
|
|
93
|
+
:raises ValueError: If the secret cannot be resolved, the model is not found, or it does not match the
|
|
94
|
+
requested type.
|
|
93
95
|
"""
|
|
94
96
|
api = HfApi()
|
|
95
97
|
try:
|
|
@@ -98,11 +100,42 @@ def _check_valid_model(model_id: str, model_type: HFModelType, token: Secret | N
|
|
|
98
100
|
msg = f"Model {model_id} not found on HuggingFace Hub. Please provide a valid HuggingFace model_id."
|
|
99
101
|
raise ValueError(msg) from e
|
|
100
102
|
|
|
103
|
+
_validate_model_type(model_id, model_type, model_info.pipeline_tag)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def _check_valid_model_async(model_id: str, model_type: HFModelType, token: Secret | None) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Asynchronously check if the provided model ID corresponds to a valid model on Hugging Face Hub.
|
|
109
|
+
|
|
110
|
+
Also check if the model supports the requested embedding or generation task.
|
|
111
|
+
|
|
112
|
+
:param model_id: A string representing the Hugging Face model ID.
|
|
113
|
+
:param model_type: The model type, `HFModelType.EMBEDDING` or `HFModelType.GENERATION`.
|
|
114
|
+
:param token: The optional authentication secret.
|
|
115
|
+
:raises ValueError: If the secret cannot be resolved, the model is not found, or it does not match the
|
|
116
|
+
requested type.
|
|
117
|
+
"""
|
|
118
|
+
api = HfApi()
|
|
119
|
+
try:
|
|
120
|
+
async with get_async_session() as client:
|
|
121
|
+
response = await client.get(
|
|
122
|
+
f"{api.endpoint}/api/models/{model_id}",
|
|
123
|
+
headers=build_hf_headers(token=token.resolve_value() if token else None),
|
|
124
|
+
)
|
|
125
|
+
hf_raise_for_status(response)
|
|
126
|
+
except RepositoryNotFoundError as e:
|
|
127
|
+
msg = f"Model {model_id} not found on HuggingFace Hub. Please provide a valid HuggingFace model_id."
|
|
128
|
+
raise ValueError(msg) from e
|
|
129
|
+
|
|
130
|
+
_validate_model_type(model_id, model_type, response.json().get("pipeline_tag"))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _validate_model_type(model_id: str, model_type: HFModelType, pipeline_tag: str | None) -> None:
|
|
101
134
|
if model_type == HFModelType.EMBEDDING:
|
|
102
|
-
allowed_model =
|
|
135
|
+
allowed_model = pipeline_tag in ["sentence-similarity", "feature-extraction"]
|
|
103
136
|
error_msg = f"Model {model_id} is not a embedding model. Please provide a embedding model."
|
|
104
137
|
elif model_type == HFModelType.GENERATION:
|
|
105
|
-
allowed_model =
|
|
138
|
+
allowed_model = pipeline_tag in ["text-generation", "text2text-generation", "image-text-to-text"]
|
|
106
139
|
error_msg = f"Model {model_id} is not a text generation model. Please provide a text generation model."
|
|
107
140
|
else:
|
|
108
141
|
allowed_model = False
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
from .document_embedder import HuggingFaceAPIDocumentEmbedder
|
|
5
|
+
from .sparse_document_embedder import HuggingFaceAPISparseDocumentEmbedder
|
|
6
|
+
from .sparse_text_embedder import HuggingFaceAPISparseTextEmbedder
|
|
7
|
+
from .text_embedder import HuggingFaceAPITextEmbedder
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"HuggingFaceAPIDocumentEmbedder",
|
|
11
|
+
"HuggingFaceAPISparseDocumentEmbedder",
|
|
12
|
+
"HuggingFaceAPISparseTextEmbedder",
|
|
13
|
+
"HuggingFaceAPITextEmbedder",
|
|
14
|
+
]
|
|
@@ -18,6 +18,7 @@ from haystack_integrations.common.huggingface_api.utils import (
|
|
|
18
18
|
HFEmbeddingAPIType,
|
|
19
19
|
HFModelType,
|
|
20
20
|
_check_valid_model,
|
|
21
|
+
_check_valid_model_async,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
logger = logging.getLogger(__name__)
|
|
@@ -161,7 +162,6 @@ class HuggingFaceAPIDocumentEmbedder:
|
|
|
161
162
|
if model is None:
|
|
162
163
|
msg = "To use the Serverless Inference API, you need to specify the `model` parameter in `api_params`."
|
|
163
164
|
raise ValueError(msg)
|
|
164
|
-
_check_valid_model(model, HFModelType.EMBEDDING, token)
|
|
165
165
|
model_or_url = model
|
|
166
166
|
elif api_type in [HFEmbeddingAPIType.INFERENCE_ENDPOINTS, HFEmbeddingAPIType.TEXT_EMBEDDINGS_INFERENCE]:
|
|
167
167
|
url = api_params.get("url")
|
|
@@ -179,8 +179,6 @@ class HuggingFaceAPIDocumentEmbedder:
|
|
|
179
179
|
msg = f"Unknown api_type {api_type}"
|
|
180
180
|
raise ValueError(msg)
|
|
181
181
|
|
|
182
|
-
client_args: dict[str, Any] = {"model": model_or_url, "token": token.resolve_value() if token else None}
|
|
183
|
-
|
|
184
182
|
self.api_type = api_type
|
|
185
183
|
self.api_params = api_params
|
|
186
184
|
self.token = token
|
|
@@ -193,8 +191,39 @@ class HuggingFaceAPIDocumentEmbedder:
|
|
|
193
191
|
self.meta_fields_to_embed = meta_fields_to_embed or []
|
|
194
192
|
self.embedding_separator = embedding_separator
|
|
195
193
|
self.concurrency_limit = concurrency_limit
|
|
196
|
-
self.
|
|
197
|
-
self.
|
|
194
|
+
self._model_or_url = model_or_url
|
|
195
|
+
self._client: InferenceClient | None = None
|
|
196
|
+
self._async_client: AsyncInferenceClient | None = None
|
|
197
|
+
|
|
198
|
+
def _client_kwargs(self) -> dict[str, Any]:
|
|
199
|
+
"""Build the keyword arguments used to create Hugging Face clients."""
|
|
200
|
+
return {"model": self._model_or_url, "token": self.token.resolve_value() if self.token else None}
|
|
201
|
+
|
|
202
|
+
def warm_up(self) -> None:
|
|
203
|
+
"""Create the synchronous Hugging Face client."""
|
|
204
|
+
if self._client is None:
|
|
205
|
+
if self.api_type == HFEmbeddingAPIType.SERVERLESS_INFERENCE_API:
|
|
206
|
+
_check_valid_model(self._model_or_url, HFModelType.EMBEDDING, self.token)
|
|
207
|
+
self._client = InferenceClient(**self._client_kwargs())
|
|
208
|
+
|
|
209
|
+
async def warm_up_async(self) -> None:
|
|
210
|
+
"""Create the asynchronous Hugging Face client."""
|
|
211
|
+
if self._async_client is None:
|
|
212
|
+
if self.api_type == HFEmbeddingAPIType.SERVERLESS_INFERENCE_API:
|
|
213
|
+
await _check_valid_model_async(self._model_or_url, HFModelType.EMBEDDING, self.token)
|
|
214
|
+
self._async_client = AsyncInferenceClient(**self._client_kwargs())
|
|
215
|
+
|
|
216
|
+
def close(self) -> None:
|
|
217
|
+
"""Close the synchronous Hugging Face client."""
|
|
218
|
+
if self._client is not None:
|
|
219
|
+
self._client.close()
|
|
220
|
+
self._client = None
|
|
221
|
+
|
|
222
|
+
async def close_async(self) -> None:
|
|
223
|
+
"""Close the asynchronous Hugging Face client."""
|
|
224
|
+
if self._async_client is not None:
|
|
225
|
+
await self._async_client.close()
|
|
226
|
+
self._async_client = None
|
|
198
227
|
|
|
199
228
|
def to_dict(self) -> dict[str, Any]:
|
|
200
229
|
"""
|
|
@@ -270,6 +299,7 @@ class HuggingFaceAPIDocumentEmbedder:
|
|
|
270
299
|
"""
|
|
271
300
|
Embed a list of texts in batches.
|
|
272
301
|
"""
|
|
302
|
+
assert self._client is not None # noqa: S101
|
|
273
303
|
truncate, normalize = self._adjust_api_parameters(self.truncate, self.normalize, self.api_type)
|
|
274
304
|
|
|
275
305
|
all_embeddings: list = []
|
|
@@ -278,7 +308,12 @@ class HuggingFaceAPIDocumentEmbedder:
|
|
|
278
308
|
):
|
|
279
309
|
batch = texts_to_embed[i : i + batch_size]
|
|
280
310
|
|
|
281
|
-
|
|
311
|
+
# a batch is accepted but text is typed as str
|
|
312
|
+
np_embeddings = self._client.feature_extraction(
|
|
313
|
+
text=batch, # type: ignore[arg-type]
|
|
314
|
+
truncate=truncate,
|
|
315
|
+
normalize=normalize,
|
|
316
|
+
)
|
|
282
317
|
|
|
283
318
|
if np_embeddings.ndim != _EXPECTED_EMBEDDING_NDIM or np_embeddings.shape[0] != len(batch):
|
|
284
319
|
msg = f"Expected embedding shape ({batch_size}, embedding_dim), got {np_embeddings.shape}"
|
|
@@ -292,6 +327,8 @@ class HuggingFaceAPIDocumentEmbedder:
|
|
|
292
327
|
"""
|
|
293
328
|
Embed a list of texts in batches asynchronously.
|
|
294
329
|
"""
|
|
330
|
+
assert self._async_client is not None # noqa: S101
|
|
331
|
+
async_client = self._async_client
|
|
295
332
|
truncate, normalize = self._adjust_api_parameters(self.truncate, self.normalize, self.api_type)
|
|
296
333
|
sem = Semaphore(max(1, self.concurrency_limit))
|
|
297
334
|
num_batches = (len(texts_to_embed) + batch_size - 1) // batch_size
|
|
@@ -299,8 +336,11 @@ class HuggingFaceAPIDocumentEmbedder:
|
|
|
299
336
|
|
|
300
337
|
async def _runner(batch: list[str]) -> list[list[float]]:
|
|
301
338
|
async with sem:
|
|
302
|
-
|
|
303
|
-
|
|
339
|
+
# a batch is accepted but text is typed as str
|
|
340
|
+
np_embeddings = await async_client.feature_extraction(
|
|
341
|
+
text=batch, # type: ignore[arg-type]
|
|
342
|
+
truncate=truncate,
|
|
343
|
+
normalize=normalize,
|
|
304
344
|
)
|
|
305
345
|
|
|
306
346
|
if np_embeddings.ndim != _EXPECTED_EMBEDDING_NDIM or np_embeddings.shape[0] != len(batch):
|
|
@@ -342,6 +382,9 @@ class HuggingFaceAPIDocumentEmbedder:
|
|
|
342
382
|
A dictionary with the following keys:
|
|
343
383
|
- `documents`: A list of documents with embeddings.
|
|
344
384
|
"""
|
|
385
|
+
self.warm_up()
|
|
386
|
+
assert self._client is not None # noqa: S101
|
|
387
|
+
|
|
345
388
|
if not isinstance(documents, list) or (documents and not isinstance(documents[0], Document)):
|
|
346
389
|
msg = (
|
|
347
390
|
"HuggingFaceAPIDocumentEmbedder expects a list of Documents as input."
|
|
@@ -375,6 +418,9 @@ class HuggingFaceAPIDocumentEmbedder:
|
|
|
375
418
|
A dictionary with the following keys:
|
|
376
419
|
- `documents`: A list of documents with embeddings.
|
|
377
420
|
"""
|
|
421
|
+
await self.warm_up_async()
|
|
422
|
+
assert self._async_client is not None # noqa: S101
|
|
423
|
+
|
|
378
424
|
if not isinstance(documents, list) or (documents and not isinstance(documents[0], Document)):
|
|
379
425
|
msg = (
|
|
380
426
|
"HuggingFaceAPIDocumentEmbedder expects a list of Documents as input."
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from asyncio import Semaphore, gather
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from itertools import chain
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from haystack import Document, component, default_from_dict, default_to_dict
|
|
12
|
+
from haystack.dataclasses import SparseEmbedding
|
|
13
|
+
from haystack.utils import Secret
|
|
14
|
+
from haystack.utils.url_validation import is_valid_http_url
|
|
15
|
+
from tqdm import tqdm
|
|
16
|
+
|
|
17
|
+
from .sparse_embedding_utils import _build_client_kwargs, _embed_sparse, _embed_sparse_async
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@component
|
|
21
|
+
class HuggingFaceAPISparseDocumentEmbedder:
|
|
22
|
+
"""
|
|
23
|
+
Embeds Documents into sparse vectors using a Hugging Face Text Embeddings Inference (TEI) server.
|
|
24
|
+
|
|
25
|
+
The component batches requests and returns copies of the input Documents with `sparse_embedding` set.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from haystack import Document
|
|
29
|
+
from haystack_integrations.components.embedders.huggingface_api import HuggingFaceAPISparseDocumentEmbedder
|
|
30
|
+
|
|
31
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(api_base_url="http://localhost:8080")
|
|
32
|
+
documents = embedder.run([Document(content="Sparse retrieval")])["documents"]
|
|
33
|
+
print(documents[0].sparse_embedding)
|
|
34
|
+
```
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
api_base_url: str = "http://localhost:8080",
|
|
41
|
+
token: Secret | None = Secret.from_env_var(["HF_API_TOKEN", "HF_TOKEN"], strict=False),
|
|
42
|
+
prefix: str = "",
|
|
43
|
+
suffix: str = "",
|
|
44
|
+
batch_size: int = 32,
|
|
45
|
+
progress_bar: bool = True,
|
|
46
|
+
meta_fields_to_embed: list[str] | None = None,
|
|
47
|
+
embedding_separator: str = "\n",
|
|
48
|
+
timeout: float | None = 30.0,
|
|
49
|
+
headers: dict[str, str] | None = None,
|
|
50
|
+
concurrency_limit: int = 4,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Create a sparse Document embedder backed by TEI.
|
|
54
|
+
|
|
55
|
+
:param api_base_url: Base URL of the TEI server.
|
|
56
|
+
:param token: Token sent to TEI as HTTP bearer authorization, if set.
|
|
57
|
+
:param prefix: A string to add before each prepared Document text.
|
|
58
|
+
:param suffix: A string to add after each prepared Document text.
|
|
59
|
+
:param batch_size: Number of Documents sent in each request.
|
|
60
|
+
:param progress_bar: If `True`, show a progress bar while embedding.
|
|
61
|
+
:param meta_fields_to_embed: Metadata fields to embed before the Document content.
|
|
62
|
+
:param embedding_separator: Separator for metadata fields and Document content.
|
|
63
|
+
:param timeout: HTTP request timeout in seconds. Set to `None` to disable it.
|
|
64
|
+
:param headers: Additional HTTP headers to send with each request.
|
|
65
|
+
:param concurrency_limit: Maximum concurrent requests made by `run_async`.
|
|
66
|
+
:raises ValueError: If `api_base_url` is invalid or a numeric parameter is not positive.
|
|
67
|
+
"""
|
|
68
|
+
if not is_valid_http_url(api_base_url):
|
|
69
|
+
msg = f"api_base_url must be a valid HTTP URL, but got {api_base_url}"
|
|
70
|
+
raise ValueError(msg)
|
|
71
|
+
if batch_size <= 0:
|
|
72
|
+
msg = f"batch_size must be > 0, but got {batch_size}"
|
|
73
|
+
raise ValueError(msg)
|
|
74
|
+
if concurrency_limit <= 0:
|
|
75
|
+
msg = f"concurrency_limit must be > 0, but got {concurrency_limit}"
|
|
76
|
+
raise ValueError(msg)
|
|
77
|
+
|
|
78
|
+
self.api_base_url = api_base_url
|
|
79
|
+
self.token = token
|
|
80
|
+
self.prefix = prefix
|
|
81
|
+
self.suffix = suffix
|
|
82
|
+
self.batch_size = batch_size
|
|
83
|
+
self.progress_bar = progress_bar
|
|
84
|
+
self.meta_fields_to_embed = meta_fields_to_embed or []
|
|
85
|
+
self.embedding_separator = embedding_separator
|
|
86
|
+
self.timeout = timeout
|
|
87
|
+
self.headers = headers or {}
|
|
88
|
+
self.concurrency_limit = concurrency_limit
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> dict[str, Any]:
|
|
91
|
+
"""Serialize this component to a dictionary."""
|
|
92
|
+
return default_to_dict(
|
|
93
|
+
self,
|
|
94
|
+
api_base_url=self.api_base_url,
|
|
95
|
+
token=self.token,
|
|
96
|
+
prefix=self.prefix,
|
|
97
|
+
suffix=self.suffix,
|
|
98
|
+
batch_size=self.batch_size,
|
|
99
|
+
progress_bar=self.progress_bar,
|
|
100
|
+
meta_fields_to_embed=self.meta_fields_to_embed,
|
|
101
|
+
embedding_separator=self.embedding_separator,
|
|
102
|
+
timeout=self.timeout,
|
|
103
|
+
headers=self.headers,
|
|
104
|
+
concurrency_limit=self.concurrency_limit,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def from_dict(cls, data: dict[str, Any]) -> "HuggingFaceAPISparseDocumentEmbedder":
|
|
109
|
+
"""Deserialize this component from a dictionary."""
|
|
110
|
+
return default_from_dict(cls, data)
|
|
111
|
+
|
|
112
|
+
def _client_kwargs(self) -> dict[str, Any]:
|
|
113
|
+
return _build_client_kwargs(
|
|
114
|
+
api_base_url=self.api_base_url, timeout=self.timeout, headers=self.headers, token=self.token
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def _prepare_texts_to_embed(self, documents: list[Document]) -> list[str]:
|
|
118
|
+
texts = []
|
|
119
|
+
for document in documents:
|
|
120
|
+
meta = [
|
|
121
|
+
str(document.meta[field])
|
|
122
|
+
for field in self.meta_fields_to_embed
|
|
123
|
+
if field in document.meta and document.meta[field] is not None
|
|
124
|
+
]
|
|
125
|
+
texts.append(self.prefix + self.embedding_separator.join([*meta, document.content or ""]) + self.suffix)
|
|
126
|
+
return texts
|
|
127
|
+
|
|
128
|
+
def _embed_batches(self, client: httpx.Client, texts: list[str]) -> list[SparseEmbedding]:
|
|
129
|
+
embeddings = []
|
|
130
|
+
for start in tqdm(
|
|
131
|
+
range(0, len(texts), self.batch_size),
|
|
132
|
+
disable=not self.progress_bar,
|
|
133
|
+
desc="Calculating sparse embeddings",
|
|
134
|
+
):
|
|
135
|
+
embeddings.extend(_embed_sparse(client=client, inputs=texts[start : start + self.batch_size]))
|
|
136
|
+
return embeddings
|
|
137
|
+
|
|
138
|
+
async def _embed_batches_async(self, client: httpx.AsyncClient, texts: list[str]) -> list[SparseEmbedding]:
|
|
139
|
+
semaphore = Semaphore(self.concurrency_limit)
|
|
140
|
+
batches = [texts[start : start + self.batch_size] for start in range(0, len(texts), self.batch_size)]
|
|
141
|
+
progress = tqdm(total=len(batches), disable=not self.progress_bar, desc="Calculating sparse embeddings")
|
|
142
|
+
|
|
143
|
+
async def embed_batch(batch: list[str]) -> list[SparseEmbedding]:
|
|
144
|
+
async with semaphore:
|
|
145
|
+
result = await _embed_sparse_async(client=client, inputs=batch)
|
|
146
|
+
progress.update(1)
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
return list(chain.from_iterable(await gather(*(embed_batch(batch) for batch in batches))))
|
|
151
|
+
finally:
|
|
152
|
+
progress.close()
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _validate_documents(documents: list[Document]) -> None:
|
|
156
|
+
if not isinstance(documents, list) or any(not isinstance(document, Document) for document in documents):
|
|
157
|
+
msg = (
|
|
158
|
+
"HuggingFaceAPISparseDocumentEmbedder expects a list of Documents as input. "
|
|
159
|
+
"To embed a string, use HuggingFaceAPISparseTextEmbedder."
|
|
160
|
+
)
|
|
161
|
+
raise TypeError(msg)
|
|
162
|
+
|
|
163
|
+
@component.output_types(documents=list[Document])
|
|
164
|
+
def run(self, documents: list[Document]) -> dict[str, list[Document]]:
|
|
165
|
+
"""
|
|
166
|
+
Embed a list of Documents.
|
|
167
|
+
|
|
168
|
+
:param documents: Documents to embed.
|
|
169
|
+
:returns: Copies of the Documents with sparse embeddings.
|
|
170
|
+
"""
|
|
171
|
+
self._validate_documents(documents)
|
|
172
|
+
texts = self._prepare_texts_to_embed(documents)
|
|
173
|
+
with httpx.Client(**self._client_kwargs()) as client:
|
|
174
|
+
embeddings = self._embed_batches(client, texts)
|
|
175
|
+
return {
|
|
176
|
+
"documents": [
|
|
177
|
+
replace(document, sparse_embedding=embedding)
|
|
178
|
+
for document, embedding in zip(documents, embeddings, strict=True)
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@component.output_types(documents=list[Document])
|
|
183
|
+
async def run_async(self, documents: list[Document]) -> dict[str, list[Document]]:
|
|
184
|
+
"""
|
|
185
|
+
Embed a list of Documents asynchronously.
|
|
186
|
+
|
|
187
|
+
:param documents: Documents to embed.
|
|
188
|
+
:returns: Copies of the Documents with sparse embeddings.
|
|
189
|
+
"""
|
|
190
|
+
self._validate_documents(documents)
|
|
191
|
+
texts = self._prepare_texts_to_embed(documents)
|
|
192
|
+
async with httpx.AsyncClient(**self._client_kwargs()) as client:
|
|
193
|
+
embeddings = await self._embed_batches_async(client, texts)
|
|
194
|
+
return {
|
|
195
|
+
"documents": [
|
|
196
|
+
replace(document, sparse_embedding=embedding)
|
|
197
|
+
for document, embedding in zip(documents, embeddings, strict=True)
|
|
198
|
+
]
|
|
199
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from haystack.dataclasses import SparseEmbedding
|
|
9
|
+
from haystack.utils import Secret
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _build_client_kwargs(
|
|
13
|
+
*, api_base_url: str, timeout: float | None, headers: dict[str, str], token: Secret | None
|
|
14
|
+
) -> dict[str, Any]:
|
|
15
|
+
"""
|
|
16
|
+
Build the `httpx` client options shared by the sync and async clients.
|
|
17
|
+
|
|
18
|
+
`headers` is applied after the token, so an `Authorization` header passed at initialization wins over the one
|
|
19
|
+
derived from `token`. Otherwise, an explicit header would be silently replaced whenever `HF_TOKEN` happens to be
|
|
20
|
+
set in the environment.
|
|
21
|
+
"""
|
|
22
|
+
request_headers: dict[str, str] = {}
|
|
23
|
+
if token and (token_value := token.resolve_value()):
|
|
24
|
+
request_headers["Authorization"] = f"Bearer {token_value}"
|
|
25
|
+
request_headers.update(headers)
|
|
26
|
+
return {"base_url": f"{api_base_url.rstrip('/')}/", "timeout": timeout, "headers": request_headers}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _parse_sparse_embeddings(result: Any, expected_count: int) -> list[SparseEmbedding]:
|
|
30
|
+
if not isinstance(result, list) or len(result) != expected_count:
|
|
31
|
+
msg = f"Expected one sparse embedding per input ({expected_count}), got {result!r}"
|
|
32
|
+
raise ValueError(msg)
|
|
33
|
+
|
|
34
|
+
embeddings = []
|
|
35
|
+
for sparse_values in result:
|
|
36
|
+
if not isinstance(sparse_values, list):
|
|
37
|
+
msg = f"Expected each sparse embedding to be a list, got {sparse_values!r}"
|
|
38
|
+
raise ValueError(msg)
|
|
39
|
+
|
|
40
|
+
indices = []
|
|
41
|
+
values = []
|
|
42
|
+
for sparse_value in sparse_values:
|
|
43
|
+
if not isinstance(sparse_value, dict) or "index" not in sparse_value or "value" not in sparse_value:
|
|
44
|
+
msg = f"Expected sparse values with 'index' and 'value' fields, got {sparse_value!r}"
|
|
45
|
+
raise ValueError(msg)
|
|
46
|
+
index = sparse_value["index"]
|
|
47
|
+
value = sparse_value["value"]
|
|
48
|
+
if (
|
|
49
|
+
not isinstance(index, int)
|
|
50
|
+
or isinstance(index, bool)
|
|
51
|
+
or not isinstance(value, int | float)
|
|
52
|
+
or isinstance(value, bool)
|
|
53
|
+
):
|
|
54
|
+
msg = f"Invalid sparse value returned by TEI: {sparse_value!r}"
|
|
55
|
+
raise ValueError(msg)
|
|
56
|
+
indices.append(index)
|
|
57
|
+
values.append(float(value))
|
|
58
|
+
|
|
59
|
+
embeddings.append(SparseEmbedding(indices=indices, values=values))
|
|
60
|
+
return embeddings
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _embed_sparse(*, client: httpx.Client, inputs: str | list[str]) -> list[SparseEmbedding]:
|
|
64
|
+
response = client.post("embed_sparse", json={"inputs": inputs})
|
|
65
|
+
response.raise_for_status()
|
|
66
|
+
return _parse_sparse_embeddings(response.json(), 1 if isinstance(inputs, str) else len(inputs))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _embed_sparse_async(*, client: httpx.AsyncClient, inputs: str | list[str]) -> list[SparseEmbedding]:
|
|
70
|
+
response = await client.post("embed_sparse", json={"inputs": inputs})
|
|
71
|
+
response.raise_for_status()
|
|
72
|
+
return _parse_sparse_embeddings(response.json(), 1 if isinstance(inputs, str) else len(inputs))
|