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.
Files changed (36) hide show
  1. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/CHANGELOG.md +20 -1
  2. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/PKG-INFO +8 -4
  3. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/README.md +4 -1
  4. huggingface_api_haystack-1.0.0/docker-compose.yml +14 -0
  5. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/pydoc/config_docusaurus.yml +2 -0
  6. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/pyproject.toml +1 -1
  7. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/common/huggingface_api/utils.py +41 -8
  8. huggingface_api_haystack-1.0.0/src/haystack_integrations/components/embedders/huggingface_api/__init__.py +14 -0
  9. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/embedders/huggingface_api/document_embedder.py +54 -8
  10. huggingface_api_haystack-1.0.0/src/haystack_integrations/components/embedders/huggingface_api/sparse_document_embedder.py +199 -0
  11. huggingface_api_haystack-1.0.0/src/haystack_integrations/components/embedders/huggingface_api/sparse_embedding_utils.py +72 -0
  12. huggingface_api_haystack-1.0.0/src/haystack_integrations/components/embedders/huggingface_api/sparse_text_embedder.py +116 -0
  13. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/embedders/huggingface_api/text_embedder.py +40 -3
  14. {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
  15. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/test_chat_generator.py +197 -78
  16. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/test_document_embedder.py +161 -30
  17. huggingface_api_haystack-1.0.0/tests/test_sparse_document_embedder.py +342 -0
  18. huggingface_api_haystack-1.0.0/tests/test_sparse_text_embedder.py +263 -0
  19. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/test_text_embedder.py +153 -28
  20. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/test_utils.py +35 -1
  21. huggingface_api_haystack-0.5.0/src/haystack_integrations/components/embedders/huggingface_api/__init__.py +0 -7
  22. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/.gitignore +0 -0
  23. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/LICENSE.txt +0 -0
  24. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/common/huggingface_api/__init__.py +0 -0
  25. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/common/py.typed +0 -0
  26. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/embedders/py.typed +0 -0
  27. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/generators/huggingface_api/__init__.py +0 -0
  28. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/generators/huggingface_api/chat/__init__.py +0 -0
  29. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/generators/py.typed +0 -0
  30. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/rankers/huggingface_api/__init__.py +0 -0
  31. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/rankers/huggingface_api/ranker.py +0 -0
  32. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/src/haystack_integrations/components/rankers/py.typed +0 -0
  33. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/__init__.py +0 -0
  34. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/conftest.py +0 -0
  35. {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-1.0.0}/tests/test_files/apple.jpg +0 -0
  36. {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
- ## [unreleased]
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.5.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>=2.30.0
23
- Requires-Dist: huggingface-hub[inference]>=0.35.0
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 to export the `HF_TOKEN` environment variable.
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 to export the `HF_TOKEN` environment variable.
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:
@@ -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>=2.30.0", "huggingface_hub[inference]>=0.35.0"]
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 HuggingFace Hub.
86
+ Check if the provided model ID corresponds to a valid model on Hugging Face Hub.
86
87
 
87
- Also check if the model is an embedding or generation model.
88
+ Also check if the model supports the requested embedding or generation task.
88
89
 
89
- :param model_id: A string representing the HuggingFace model ID.
90
- :param model_type: the model type, HFModelType.EMBEDDING or HFModelType.GENERATION
91
- :param token: The optional authentication token.
92
- :raises ValueError: If the model is not found or is not a embedding model.
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 = model_info.pipeline_tag in ["sentence-similarity", "feature-extraction"]
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 = model_info.pipeline_tag in ["text-generation", "text2text-generation", "image-text-to-text"]
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._client = InferenceClient(**client_args)
197
- self._async_client = AsyncInferenceClient(**client_args)
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
- np_embeddings = self._client.feature_extraction(text=batch, truncate=truncate, normalize=normalize)
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
- np_embeddings = await self._async_client.feature_extraction(
303
- text=batch, truncate=truncate, normalize=normalize
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))