huggingface-api-haystack 0.5.0__tar.gz → 0.6.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-0.6.0}/PKG-INFO +6 -2
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/README.md +4 -1
- huggingface_api_haystack-0.6.0/docker-compose.yml +14 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/pydoc/config_docusaurus.yml +2 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/pyproject.toml +1 -1
- huggingface_api_haystack-0.6.0/src/haystack_integrations/components/embedders/huggingface_api/__init__.py +14 -0
- huggingface_api_haystack-0.6.0/src/haystack_integrations/components/embedders/huggingface_api/sparse_document_embedder.py +199 -0
- huggingface_api_haystack-0.6.0/src/haystack_integrations/components/embedders/huggingface_api/sparse_embedding_utils.py +72 -0
- huggingface_api_haystack-0.6.0/src/haystack_integrations/components/embedders/huggingface_api/sparse_text_embedder.py +116 -0
- huggingface_api_haystack-0.6.0/tests/test_sparse_document_embedder.py +342 -0
- huggingface_api_haystack-0.6.0/tests/test_sparse_text_embedder.py +263 -0
- 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-0.6.0}/.gitignore +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/CHANGELOG.md +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/LICENSE.txt +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/common/huggingface_api/__init__.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/common/huggingface_api/utils.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/common/py.typed +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/components/embedders/huggingface_api/document_embedder.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/components/embedders/huggingface_api/text_embedder.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/components/embedders/py.typed +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/components/generators/huggingface_api/__init__.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/components/generators/huggingface_api/chat/__init__.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/components/generators/huggingface_api/chat/chat_generator.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/components/generators/py.typed +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/components/rankers/huggingface_api/__init__.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/components/rankers/huggingface_api/ranker.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/src/haystack_integrations/components/rankers/py.typed +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/tests/__init__.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/tests/conftest.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/tests/test_chat_generator.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/tests/test_document_embedder.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/tests/test_files/apple.jpg +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/tests/test_ranker.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/tests/test_text_embedder.py +0 -0
- {huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: huggingface-api-haystack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
|
20
20
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Requires-Dist: haystack-ai>=2.30.0
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
23
24
|
Requires-Dist: huggingface-hub[inference]>=0.35.0
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
25
26
|
|
|
@@ -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-0.6.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>=2.30.0", "huggingface_hub[inference]>=0.35.0"]
|
|
26
|
+
dependencies = ["haystack-ai>=2.30.0", "httpx>=0.27.0", "huggingface_hub[inference]>=0.35.0"]
|
|
27
27
|
|
|
28
28
|
[project.urls]
|
|
29
29
|
Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/huggingface_api#readme"
|
|
@@ -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
|
+
]
|
|
@@ -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))
|
|
@@ -0,0 +1,116 @@
|
|
|
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 import component, default_from_dict, default_to_dict
|
|
9
|
+
from haystack.dataclasses import SparseEmbedding
|
|
10
|
+
from haystack.utils import Secret
|
|
11
|
+
from haystack.utils.url_validation import is_valid_http_url
|
|
12
|
+
|
|
13
|
+
from .sparse_embedding_utils import _build_client_kwargs, _embed_sparse, _embed_sparse_async
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@component
|
|
17
|
+
class HuggingFaceAPISparseTextEmbedder:
|
|
18
|
+
"""
|
|
19
|
+
Embeds text into a sparse vector using a Hugging Face Text Embeddings Inference (TEI) server.
|
|
20
|
+
|
|
21
|
+
The TEI server must be running a sparse embedding model and expose the `/embed_sparse` endpoint.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from haystack_integrations.components.embedders.huggingface_api import HuggingFaceAPISparseTextEmbedder
|
|
25
|
+
|
|
26
|
+
embedder = HuggingFaceAPISparseTextEmbedder(api_base_url="http://localhost:8080")
|
|
27
|
+
result = embedder.run("What is sparse retrieval?")
|
|
28
|
+
print(result["sparse_embedding"])
|
|
29
|
+
```
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
api_base_url: str = "http://localhost:8080",
|
|
36
|
+
token: Secret | None = Secret.from_env_var(["HF_API_TOKEN", "HF_TOKEN"], strict=False),
|
|
37
|
+
prefix: str = "",
|
|
38
|
+
suffix: str = "",
|
|
39
|
+
timeout: float | None = 30.0,
|
|
40
|
+
headers: dict[str, str] | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Create a sparse text embedder backed by TEI.
|
|
44
|
+
|
|
45
|
+
:param api_base_url: Base URL of the TEI server.
|
|
46
|
+
:param token: Token sent to TEI as HTTP bearer authorization, if set.
|
|
47
|
+
:param prefix: A string to add before the text.
|
|
48
|
+
:param suffix: A string to add after the text.
|
|
49
|
+
:param timeout: HTTP request timeout in seconds. Set to `None` to disable it.
|
|
50
|
+
:param headers: Additional HTTP headers to send with each request.
|
|
51
|
+
:raises ValueError: If `api_base_url` is not a valid HTTP URL.
|
|
52
|
+
"""
|
|
53
|
+
if not is_valid_http_url(api_base_url):
|
|
54
|
+
msg = f"api_base_url must be a valid HTTP URL, but got {api_base_url}"
|
|
55
|
+
raise ValueError(msg)
|
|
56
|
+
self.api_base_url = api_base_url
|
|
57
|
+
self.token = token
|
|
58
|
+
self.prefix = prefix
|
|
59
|
+
self.suffix = suffix
|
|
60
|
+
self.timeout = timeout
|
|
61
|
+
self.headers = headers or {}
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> dict[str, Any]:
|
|
64
|
+
"""Serialize this component to a dictionary."""
|
|
65
|
+
return default_to_dict(
|
|
66
|
+
self,
|
|
67
|
+
api_base_url=self.api_base_url,
|
|
68
|
+
token=self.token,
|
|
69
|
+
prefix=self.prefix,
|
|
70
|
+
suffix=self.suffix,
|
|
71
|
+
timeout=self.timeout,
|
|
72
|
+
headers=self.headers,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_dict(cls, data: dict[str, Any]) -> "HuggingFaceAPISparseTextEmbedder":
|
|
77
|
+
"""Deserialize this component from a dictionary."""
|
|
78
|
+
return default_from_dict(cls, data)
|
|
79
|
+
|
|
80
|
+
def _client_kwargs(self) -> dict[str, Any]:
|
|
81
|
+
return _build_client_kwargs(
|
|
82
|
+
api_base_url=self.api_base_url, timeout=self.timeout, headers=self.headers, token=self.token
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _prepare_input(self, text: str) -> str:
|
|
86
|
+
if not isinstance(text, str):
|
|
87
|
+
msg = (
|
|
88
|
+
"HuggingFaceAPISparseTextEmbedder expects a string as input. "
|
|
89
|
+
"To embed Documents, use HuggingFaceAPISparseDocumentEmbedder."
|
|
90
|
+
)
|
|
91
|
+
raise TypeError(msg)
|
|
92
|
+
return self.prefix + text + self.suffix
|
|
93
|
+
|
|
94
|
+
@component.output_types(sparse_embedding=SparseEmbedding)
|
|
95
|
+
def run(self, text: str) -> dict[str, SparseEmbedding]:
|
|
96
|
+
"""
|
|
97
|
+
Embed a single string.
|
|
98
|
+
|
|
99
|
+
:param text: Text to embed.
|
|
100
|
+
:returns: The sparse embedding of the input text.
|
|
101
|
+
"""
|
|
102
|
+
with httpx.Client(**self._client_kwargs()) as client:
|
|
103
|
+
embeddings = _embed_sparse(client=client, inputs=self._prepare_input(text))
|
|
104
|
+
return {"sparse_embedding": embeddings[0]}
|
|
105
|
+
|
|
106
|
+
@component.output_types(sparse_embedding=SparseEmbedding)
|
|
107
|
+
async def run_async(self, text: str) -> dict[str, SparseEmbedding]:
|
|
108
|
+
"""
|
|
109
|
+
Embed a single string asynchronously.
|
|
110
|
+
|
|
111
|
+
:param text: Text to embed.
|
|
112
|
+
:returns: The sparse embedding of the input text.
|
|
113
|
+
"""
|
|
114
|
+
async with httpx.AsyncClient(**self._client_kwargs()) as client:
|
|
115
|
+
embeddings = await _embed_sparse_async(client=client, inputs=self._prepare_input(text))
|
|
116
|
+
return {"sparse_embedding": embeddings[0]}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from typing import Any
|
|
9
|
+
from unittest.mock import MagicMock, call, patch
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
import pytest
|
|
13
|
+
from haystack import Document
|
|
14
|
+
from haystack.dataclasses import SparseEmbedding
|
|
15
|
+
from haystack.utils import Secret
|
|
16
|
+
|
|
17
|
+
from haystack_integrations.components.embedders.huggingface_api import HuggingFaceAPISparseDocumentEmbedder
|
|
18
|
+
|
|
19
|
+
API_BASE_URL = "http://localhost:8080"
|
|
20
|
+
MODULE = "haystack_integrations.components.embedders.huggingface_api.sparse_document_embedder"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def sparse_response(data: Any) -> MagicMock:
|
|
24
|
+
response = MagicMock(spec=httpx.Response)
|
|
25
|
+
response.json.return_value = data
|
|
26
|
+
return response
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@contextmanager
|
|
30
|
+
def patched_client(*, is_async: bool = False) -> Iterator[tuple[MagicMock, MagicMock]]:
|
|
31
|
+
"""
|
|
32
|
+
Patch the `httpx` client constructor and yield the (client, constructor) mocks.
|
|
33
|
+
|
|
34
|
+
The component builds a client per call, so tests reach the client through the constructor rather than through
|
|
35
|
+
an attribute. `__enter__`/`__aenter__` yield the same mock, so `client.post` assertions read naturally.
|
|
36
|
+
"""
|
|
37
|
+
name = "AsyncClient" if is_async else "Client"
|
|
38
|
+
client = MagicMock(spec=getattr(httpx, name))
|
|
39
|
+
if is_async:
|
|
40
|
+
client.__aenter__.return_value = client
|
|
41
|
+
client.__aexit__.return_value = False
|
|
42
|
+
else:
|
|
43
|
+
client.__enter__.return_value = client
|
|
44
|
+
client.__exit__.return_value = False
|
|
45
|
+
with patch(f"{MODULE}.httpx.{name}", return_value=client) as constructor:
|
|
46
|
+
yield client, constructor
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestHuggingFaceAPISparseDocumentEmbedder:
|
|
50
|
+
@pytest.mark.parametrize("api_base_url", ["not-a-url", "file:///path", "localhost:8080"])
|
|
51
|
+
def test_init_rejects_invalid_api_base_url(self, api_base_url: str) -> None:
|
|
52
|
+
with pytest.raises(ValueError, match="api_base_url must be a valid HTTP URL"):
|
|
53
|
+
HuggingFaceAPISparseDocumentEmbedder(api_base_url=api_base_url)
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize(("parameter", "value"), [("batch_size", 0), ("batch_size", -2), ("concurrency_limit", 0)])
|
|
56
|
+
def test_init_rejects_non_positive_numeric_options(self, parameter: str, value: int) -> None:
|
|
57
|
+
with pytest.raises(ValueError, match=f"{parameter} must be > 0"):
|
|
58
|
+
HuggingFaceAPISparseDocumentEmbedder(**{parameter: value})
|
|
59
|
+
|
|
60
|
+
def test_init_defaults(self) -> None:
|
|
61
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder()
|
|
62
|
+
|
|
63
|
+
assert embedder.api_base_url == "http://localhost:8080"
|
|
64
|
+
assert embedder.batch_size == 32
|
|
65
|
+
assert embedder.progress_bar is True
|
|
66
|
+
assert embedder.meta_fields_to_embed == []
|
|
67
|
+
assert embedder.embedding_separator == "\n"
|
|
68
|
+
assert embedder.timeout == 30.0
|
|
69
|
+
assert embedder.headers == {}
|
|
70
|
+
assert embedder.concurrency_limit == 4
|
|
71
|
+
|
|
72
|
+
def test_to_dict_and_from_dict_preserve_configuration_and_secret(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
73
|
+
monkeypatch.setenv("CUSTOM_HF_TOKEN", "secret")
|
|
74
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(
|
|
75
|
+
api_base_url="https://tei.example.test",
|
|
76
|
+
token=Secret.from_env_var("CUSTOM_HF_TOKEN"),
|
|
77
|
+
prefix="passage: ",
|
|
78
|
+
suffix="!",
|
|
79
|
+
batch_size=2,
|
|
80
|
+
progress_bar=False,
|
|
81
|
+
meta_fields_to_embed=["title"],
|
|
82
|
+
embedding_separator=" | ",
|
|
83
|
+
timeout=None,
|
|
84
|
+
headers={"X-Test": "yes"},
|
|
85
|
+
concurrency_limit=3,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
data = embedder.to_dict()
|
|
89
|
+
|
|
90
|
+
assert data["init_parameters"]["token"] == {
|
|
91
|
+
"type": "env_var",
|
|
92
|
+
"env_vars": ["CUSTOM_HF_TOKEN"],
|
|
93
|
+
"strict": True,
|
|
94
|
+
}
|
|
95
|
+
assert data["init_parameters"] == {
|
|
96
|
+
"api_base_url": "https://tei.example.test",
|
|
97
|
+
"token": {"type": "env_var", "env_vars": ["CUSTOM_HF_TOKEN"], "strict": True},
|
|
98
|
+
"prefix": "passage: ",
|
|
99
|
+
"suffix": "!",
|
|
100
|
+
"batch_size": 2,
|
|
101
|
+
"progress_bar": False,
|
|
102
|
+
"meta_fields_to_embed": ["title"],
|
|
103
|
+
"embedding_separator": " | ",
|
|
104
|
+
"timeout": None,
|
|
105
|
+
"headers": {"X-Test": "yes"},
|
|
106
|
+
"concurrency_limit": 3,
|
|
107
|
+
}
|
|
108
|
+
restored = HuggingFaceAPISparseDocumentEmbedder.from_dict(data)
|
|
109
|
+
|
|
110
|
+
assert restored.api_base_url == embedder.api_base_url
|
|
111
|
+
assert restored.prefix == "passage: "
|
|
112
|
+
assert restored.suffix == "!"
|
|
113
|
+
assert restored.batch_size == 2
|
|
114
|
+
assert restored.progress_bar is False
|
|
115
|
+
assert restored.meta_fields_to_embed == ["title"]
|
|
116
|
+
assert restored.embedding_separator == " | "
|
|
117
|
+
assert restored.timeout is None
|
|
118
|
+
assert restored.headers == {"X-Test": "yes"}
|
|
119
|
+
assert restored.concurrency_limit == 3
|
|
120
|
+
assert restored.token is not None and restored.token.resolve_value() == "secret"
|
|
121
|
+
|
|
122
|
+
def test_token_secret_cannot_be_serialized(self) -> None:
|
|
123
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(token=Secret.from_token("do-not-serialize"))
|
|
124
|
+
|
|
125
|
+
with pytest.raises(ValueError, match="Cannot serialize token-based secret"):
|
|
126
|
+
embedder.to_dict()
|
|
127
|
+
|
|
128
|
+
def test_prepare_texts_handles_metadata_prefix_suffix_and_missing_content(self) -> None:
|
|
129
|
+
documents = [
|
|
130
|
+
Document(content="body", meta={"title": "Title", "priority": 3, "ignored": "no"}),
|
|
131
|
+
Document(content=None, meta={"title": None, "priority": 0}),
|
|
132
|
+
Document(content="only body", meta={}),
|
|
133
|
+
]
|
|
134
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(
|
|
135
|
+
prefix="<p>", suffix="</p>", meta_fields_to_embed=["title", "priority"], embedding_separator=" | "
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
assert embedder._prepare_texts_to_embed(documents) == [
|
|
139
|
+
"<p>Title | 3 | body</p>",
|
|
140
|
+
"<p>0 | </p>",
|
|
141
|
+
"<p>only body</p>",
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
@pytest.mark.parametrize("documents", [None, "document", [1, 2], [Document(content="valid"), "invalid"]])
|
|
145
|
+
def test_run_rejects_invalid_and_mixed_inputs(self, documents: Any) -> None:
|
|
146
|
+
with pytest.raises(TypeError, match="expects a list of Documents"):
|
|
147
|
+
HuggingFaceAPISparseDocumentEmbedder(progress_bar=False).run(documents)
|
|
148
|
+
|
|
149
|
+
@pytest.mark.asyncio
|
|
150
|
+
async def test_run_async_rejects_mixed_inputs(self) -> None:
|
|
151
|
+
with pytest.raises(TypeError, match="expects a list of Documents"):
|
|
152
|
+
await HuggingFaceAPISparseDocumentEmbedder(progress_bar=False).run_async([Document(content="ok"), None])
|
|
153
|
+
|
|
154
|
+
def test_empty_list_returns_without_http_request(self) -> None:
|
|
155
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(progress_bar=False)
|
|
156
|
+
|
|
157
|
+
with patched_client() as (client, _):
|
|
158
|
+
result = embedder.run([])
|
|
159
|
+
|
|
160
|
+
assert result == {"documents": []}
|
|
161
|
+
client.post.assert_not_called()
|
|
162
|
+
|
|
163
|
+
@pytest.mark.asyncio
|
|
164
|
+
async def test_empty_list_async_returns_without_http_request(self) -> None:
|
|
165
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(progress_bar=False)
|
|
166
|
+
|
|
167
|
+
with patched_client(is_async=True) as (client, _):
|
|
168
|
+
result = await embedder.run_async([])
|
|
169
|
+
|
|
170
|
+
assert result == {"documents": []}
|
|
171
|
+
client.post.assert_not_called()
|
|
172
|
+
|
|
173
|
+
def test_run_batches_requests_preserves_order_and_copies_documents(self) -> None:
|
|
174
|
+
documents = [Document(content=f"doc {number}", meta={"number": number}) for number in range(5)]
|
|
175
|
+
responses = [
|
|
176
|
+
sparse_response([[{"index": 10, "value": 1}], [{"index": 11, "value": 2}]]),
|
|
177
|
+
sparse_response([[{"index": 12, "value": 3}], [{"index": 13, "value": 4}]]),
|
|
178
|
+
sparse_response([[{"index": 14, "value": 5}]]),
|
|
179
|
+
]
|
|
180
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(
|
|
181
|
+
api_base_url="http://tei:80/root/",
|
|
182
|
+
token=Secret.from_token("token"),
|
|
183
|
+
prefix="passage: ",
|
|
184
|
+
batch_size=2,
|
|
185
|
+
progress_bar=False,
|
|
186
|
+
timeout=6,
|
|
187
|
+
headers={"X-Tenant": "tenant"},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
with patched_client() as (client, constructor):
|
|
191
|
+
client.post.side_effect = responses
|
|
192
|
+
result = embedder.run(documents)
|
|
193
|
+
|
|
194
|
+
constructor.assert_called_once_with(
|
|
195
|
+
base_url="http://tei:80/root/",
|
|
196
|
+
timeout=6,
|
|
197
|
+
headers={"Authorization": "Bearer token", "X-Tenant": "tenant"},
|
|
198
|
+
)
|
|
199
|
+
assert client.post.call_args_list == [
|
|
200
|
+
call("embed_sparse", json={"inputs": ["passage: doc 0", "passage: doc 1"]}),
|
|
201
|
+
call("embed_sparse", json={"inputs": ["passage: doc 2", "passage: doc 3"]}),
|
|
202
|
+
call("embed_sparse", json={"inputs": ["passage: doc 4"]}),
|
|
203
|
+
]
|
|
204
|
+
output = result["documents"]
|
|
205
|
+
assert [document.sparse_embedding.indices for document in output] == [[10], [11], [12], [13], [14]]
|
|
206
|
+
assert [document.sparse_embedding.values for document in output] == [[1.0], [2.0], [3.0], [4.0], [5.0]]
|
|
207
|
+
assert all(new is not original for original, new in zip(documents, output, strict=True))
|
|
208
|
+
assert all(original.sparse_embedding is None for original in documents)
|
|
209
|
+
assert [document.meta for document in output] == [document.meta for document in documents]
|
|
210
|
+
|
|
211
|
+
@pytest.mark.asyncio
|
|
212
|
+
async def test_run_async_batches_with_one_client_and_preserves_batch_order(self) -> None:
|
|
213
|
+
async def post(_url: str, *, json: dict[str, list[str]]) -> MagicMock:
|
|
214
|
+
inputs = json["inputs"]
|
|
215
|
+
# Let the second batch finish first; gather must still preserve input order.
|
|
216
|
+
await asyncio.sleep(0.01 if inputs[0] == "doc 0" else 0)
|
|
217
|
+
offset = int(inputs[0].split()[-1])
|
|
218
|
+
return sparse_response([[{"index": offset + position, "value": 1}] for position, _ in enumerate(inputs)])
|
|
219
|
+
|
|
220
|
+
documents = [Document(content=f"doc {number}") for number in range(4)]
|
|
221
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(
|
|
222
|
+
api_base_url="https://tei.test/", batch_size=2, progress_bar=False, timeout=None, headers={"X-Test": "yes"}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
with patched_client(is_async=True) as (client, constructor):
|
|
226
|
+
client.post.side_effect = post
|
|
227
|
+
result = await embedder.run_async(documents)
|
|
228
|
+
|
|
229
|
+
# Both batches share the single client opened for this call.
|
|
230
|
+
constructor.assert_called_once_with(base_url="https://tei.test/", timeout=None, headers={"X-Test": "yes"})
|
|
231
|
+
assert client.post.await_args_list == [
|
|
232
|
+
call("embed_sparse", json={"inputs": ["doc 0", "doc 1"]}),
|
|
233
|
+
call("embed_sparse", json={"inputs": ["doc 2", "doc 3"]}),
|
|
234
|
+
]
|
|
235
|
+
assert [document.sparse_embedding.indices for document in result["documents"]] == [[0], [1], [2], [3]]
|
|
236
|
+
|
|
237
|
+
@pytest.mark.asyncio
|
|
238
|
+
async def test_run_async_builds_and_closes_a_client_per_call(self) -> None:
|
|
239
|
+
"""
|
|
240
|
+
The component must not hold on to an `httpx.AsyncClient`.
|
|
241
|
+
|
|
242
|
+
A cached client binds its keep-alive connection pool to the first event loop that used it, so a component
|
|
243
|
+
reused under a second `asyncio.run` would fail with `RuntimeError: Event loop is closed`.
|
|
244
|
+
"""
|
|
245
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(progress_bar=False)
|
|
246
|
+
|
|
247
|
+
with patched_client(is_async=True) as (client, constructor):
|
|
248
|
+
client.post.return_value = sparse_response([[{"index": 1, "value": 1}]])
|
|
249
|
+
await embedder.run_async([Document(content="one")])
|
|
250
|
+
await embedder.run_async([Document(content="two")])
|
|
251
|
+
|
|
252
|
+
assert constructor.call_count == 2
|
|
253
|
+
assert client.__aexit__.await_count == 2
|
|
254
|
+
assert not hasattr(embedder, "_async_client")
|
|
255
|
+
|
|
256
|
+
def test_run_builds_and_closes_a_client_per_call(self) -> None:
|
|
257
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(progress_bar=False)
|
|
258
|
+
|
|
259
|
+
with patched_client() as (client, constructor):
|
|
260
|
+
client.post.return_value = sparse_response([[{"index": 1, "value": 1}]])
|
|
261
|
+
embedder.run([Document(content="one")])
|
|
262
|
+
embedder.run([Document(content="two")])
|
|
263
|
+
|
|
264
|
+
assert constructor.call_count == 2
|
|
265
|
+
assert client.__exit__.call_count == 2
|
|
266
|
+
assert not hasattr(embedder, "_client")
|
|
267
|
+
|
|
268
|
+
def test_explicit_authorization_header_wins_over_token(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
269
|
+
"""An explicit header must not be replaced by a token that only happens to be set in the environment."""
|
|
270
|
+
monkeypatch.delenv("HF_API_TOKEN", raising=False)
|
|
271
|
+
monkeypatch.setenv("HF_TOKEN", "env-token")
|
|
272
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(progress_bar=False, headers={"Authorization": "Basic test-key"})
|
|
273
|
+
|
|
274
|
+
with patched_client() as (client, constructor):
|
|
275
|
+
client.post.return_value = sparse_response([[{"index": 1, "value": 1}]])
|
|
276
|
+
embedder.run([Document(content="one")])
|
|
277
|
+
|
|
278
|
+
assert constructor.call_args.kwargs["headers"] == {"Authorization": "Basic test-key"}
|
|
279
|
+
|
|
280
|
+
@pytest.mark.asyncio
|
|
281
|
+
async def test_async_concurrency_limit_is_respected(self) -> None:
|
|
282
|
+
active = 0
|
|
283
|
+
maximum_active = 0
|
|
284
|
+
|
|
285
|
+
async def embed_batch(**kwargs: Any) -> list[SparseEmbedding]:
|
|
286
|
+
nonlocal active, maximum_active
|
|
287
|
+
active += 1
|
|
288
|
+
maximum_active = max(maximum_active, active)
|
|
289
|
+
await asyncio.sleep(0.01)
|
|
290
|
+
active -= 1
|
|
291
|
+
return [SparseEmbedding(indices=[int(text)], values=[1.0]) for text in kwargs["inputs"]]
|
|
292
|
+
|
|
293
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(batch_size=1, concurrency_limit=2, progress_bar=False)
|
|
294
|
+
client = MagicMock(spec=httpx.AsyncClient)
|
|
295
|
+
with patch(f"{MODULE}._embed_sparse_async", side_effect=embed_batch):
|
|
296
|
+
embeddings = await embedder._embed_batches_async(client, ["0", "1", "2", "3"])
|
|
297
|
+
|
|
298
|
+
assert maximum_active == 2
|
|
299
|
+
assert [embedding.indices for embedding in embeddings] == [[0], [1], [2], [3]]
|
|
300
|
+
|
|
301
|
+
def test_run_rejects_response_with_wrong_embedding_count(self) -> None:
|
|
302
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(progress_bar=False)
|
|
303
|
+
|
|
304
|
+
with (
|
|
305
|
+
patched_client() as (client, _),
|
|
306
|
+
pytest.raises(ValueError, match="Expected one sparse embedding per input"),
|
|
307
|
+
):
|
|
308
|
+
client.post.return_value = sparse_response([[{"index": 1, "value": 1}]])
|
|
309
|
+
embedder.run([Document(content="one"), Document(content="two")])
|
|
310
|
+
|
|
311
|
+
def test_run_propagates_http_error(self) -> None:
|
|
312
|
+
request = httpx.Request("POST", "http://localhost:8080/embed_sparse")
|
|
313
|
+
embedder = HuggingFaceAPISparseDocumentEmbedder(progress_bar=False)
|
|
314
|
+
|
|
315
|
+
with patched_client() as (client, _), pytest.raises(httpx.HTTPStatusError):
|
|
316
|
+
client.post.return_value = httpx.Response(500, request=request)
|
|
317
|
+
embedder.run([Document(content="text")])
|
|
318
|
+
|
|
319
|
+
@pytest.mark.integration
|
|
320
|
+
def test_live_run_tei(self) -> None:
|
|
321
|
+
documents = [Document(content="sparse retrieval"), Document(content="dense retrieval")]
|
|
322
|
+
result = HuggingFaceAPISparseDocumentEmbedder(api_base_url=API_BASE_URL, progress_bar=False).run(documents)
|
|
323
|
+
|
|
324
|
+
documents_with_embeddings = result["documents"]
|
|
325
|
+
assert len(documents_with_embeddings) == len(documents)
|
|
326
|
+
for document in documents_with_embeddings:
|
|
327
|
+
assert isinstance(document.sparse_embedding, SparseEmbedding)
|
|
328
|
+
assert document.sparse_embedding.indices
|
|
329
|
+
|
|
330
|
+
@pytest.mark.integration
|
|
331
|
+
@pytest.mark.asyncio
|
|
332
|
+
async def test_live_run_async_tei(self) -> None:
|
|
333
|
+
documents = [Document(content="sparse retrieval"), Document(content="dense retrieval")]
|
|
334
|
+
result = await HuggingFaceAPISparseDocumentEmbedder(api_base_url=API_BASE_URL, progress_bar=False).run_async(
|
|
335
|
+
documents
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
documents_with_embeddings = result["documents"]
|
|
339
|
+
assert len(documents_with_embeddings) == len(documents)
|
|
340
|
+
for document in documents_with_embeddings:
|
|
341
|
+
assert isinstance(document.sparse_embedding, SparseEmbedding)
|
|
342
|
+
assert document.sparse_embedding.indices
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from typing import Any
|
|
8
|
+
from unittest.mock import MagicMock, patch
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import pytest
|
|
12
|
+
from haystack.dataclasses import SparseEmbedding
|
|
13
|
+
from haystack.utils import Secret
|
|
14
|
+
|
|
15
|
+
from haystack_integrations.components.embedders.huggingface_api import HuggingFaceAPISparseTextEmbedder
|
|
16
|
+
|
|
17
|
+
API_BASE_URL = "http://localhost:8080"
|
|
18
|
+
MODULE = "haystack_integrations.components.embedders.huggingface_api.sparse_text_embedder"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def sparse_response(data: Any) -> MagicMock:
|
|
22
|
+
response = MagicMock(spec=httpx.Response)
|
|
23
|
+
response.json.return_value = data
|
|
24
|
+
return response
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@contextmanager
|
|
28
|
+
def patched_client(*, is_async: bool = False) -> Iterator[tuple[MagicMock, MagicMock]]:
|
|
29
|
+
"""
|
|
30
|
+
Patch the `httpx` client constructor and yield the (client, constructor) mocks.
|
|
31
|
+
|
|
32
|
+
The component builds a client per call, so tests reach the client through the constructor rather than through
|
|
33
|
+
an attribute. `__enter__`/`__aenter__` yield the same mock, so `client.post` assertions read naturally.
|
|
34
|
+
"""
|
|
35
|
+
name = "AsyncClient" if is_async else "Client"
|
|
36
|
+
client = MagicMock(spec=getattr(httpx, name))
|
|
37
|
+
if is_async:
|
|
38
|
+
client.__aenter__.return_value = client
|
|
39
|
+
client.__aexit__.return_value = False
|
|
40
|
+
else:
|
|
41
|
+
client.__enter__.return_value = client
|
|
42
|
+
client.__exit__.return_value = False
|
|
43
|
+
with patch(f"{MODULE}.httpx.{name}", return_value=client) as constructor:
|
|
44
|
+
yield client, constructor
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestHuggingFaceAPISparseTextEmbedder:
|
|
48
|
+
@pytest.mark.parametrize("api_base_url", ["not-a-url", "ftp://localhost/path", "localhost:8080"])
|
|
49
|
+
def test_init_rejects_invalid_api_base_url(self, api_base_url: str) -> None:
|
|
50
|
+
with pytest.raises(ValueError, match="api_base_url must be a valid HTTP URL"):
|
|
51
|
+
HuggingFaceAPISparseTextEmbedder(api_base_url=api_base_url)
|
|
52
|
+
|
|
53
|
+
def test_init_defaults(self) -> None:
|
|
54
|
+
embedder = HuggingFaceAPISparseTextEmbedder()
|
|
55
|
+
|
|
56
|
+
assert embedder.api_base_url == "http://localhost:8080"
|
|
57
|
+
assert embedder.prefix == ""
|
|
58
|
+
assert embedder.suffix == ""
|
|
59
|
+
assert embedder.timeout == 30.0
|
|
60
|
+
assert embedder.headers == {}
|
|
61
|
+
|
|
62
|
+
def test_to_dict_and_from_dict_preserve_env_secret(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
63
|
+
monkeypatch.setenv("CUSTOM_HF_TOKEN", "resolved-token")
|
|
64
|
+
embedder = HuggingFaceAPISparseTextEmbedder(
|
|
65
|
+
api_base_url="https://tei.example.test/base/",
|
|
66
|
+
token=Secret.from_env_var("CUSTOM_HF_TOKEN"),
|
|
67
|
+
prefix="query: ",
|
|
68
|
+
suffix="!",
|
|
69
|
+
timeout=None,
|
|
70
|
+
headers={"X-Tenant": "test"},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
data = embedder.to_dict()
|
|
74
|
+
|
|
75
|
+
assert data == {
|
|
76
|
+
"type": "haystack_integrations.components.embedders.huggingface_api.sparse_text_embedder."
|
|
77
|
+
"HuggingFaceAPISparseTextEmbedder",
|
|
78
|
+
"init_parameters": {
|
|
79
|
+
"api_base_url": "https://tei.example.test/base/",
|
|
80
|
+
"token": {"type": "env_var", "env_vars": ["CUSTOM_HF_TOKEN"], "strict": True},
|
|
81
|
+
"prefix": "query: ",
|
|
82
|
+
"suffix": "!",
|
|
83
|
+
"timeout": None,
|
|
84
|
+
"headers": {"X-Tenant": "test"},
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
restored = HuggingFaceAPISparseTextEmbedder.from_dict(data)
|
|
88
|
+
assert restored.api_base_url == embedder.api_base_url
|
|
89
|
+
assert restored.prefix == "query: "
|
|
90
|
+
assert restored.suffix == "!"
|
|
91
|
+
assert restored.timeout is None
|
|
92
|
+
assert restored.headers == {"X-Tenant": "test"}
|
|
93
|
+
assert restored.token is not None
|
|
94
|
+
assert restored.token.resolve_value() == "resolved-token"
|
|
95
|
+
|
|
96
|
+
def test_token_secret_cannot_be_serialized(self) -> None:
|
|
97
|
+
embedder = HuggingFaceAPISparseTextEmbedder(token=Secret.from_token("do-not-serialize"))
|
|
98
|
+
|
|
99
|
+
with pytest.raises(ValueError, match="Cannot serialize token-based secret"):
|
|
100
|
+
embedder.to_dict()
|
|
101
|
+
|
|
102
|
+
@pytest.mark.parametrize("invalid_text", [None, 42, ["text"]])
|
|
103
|
+
def test_run_rejects_non_string_input(self, invalid_text: Any) -> None:
|
|
104
|
+
embedder = HuggingFaceAPISparseTextEmbedder()
|
|
105
|
+
|
|
106
|
+
with pytest.raises(TypeError, match="expects a string"):
|
|
107
|
+
embedder.run(invalid_text)
|
|
108
|
+
|
|
109
|
+
def test_run_posts_tei_request_and_converts_response(self) -> None:
|
|
110
|
+
embedder = HuggingFaceAPISparseTextEmbedder(
|
|
111
|
+
api_base_url="https://tei.example.test/api/",
|
|
112
|
+
token=Secret.from_token("secret"),
|
|
113
|
+
prefix="query: ",
|
|
114
|
+
suffix=" </s>",
|
|
115
|
+
timeout=4.5,
|
|
116
|
+
headers={"X-Tenant": "one"},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
with patched_client() as (client, constructor):
|
|
120
|
+
response = sparse_response([[{"index": 12, "value": 1}, {"index": 99, "value": 0.25}]])
|
|
121
|
+
client.post.return_value = response
|
|
122
|
+
result = embedder.run("cheese")
|
|
123
|
+
|
|
124
|
+
constructor.assert_called_once_with(
|
|
125
|
+
base_url="https://tei.example.test/api/",
|
|
126
|
+
timeout=4.5,
|
|
127
|
+
headers={"Authorization": "Bearer secret", "X-Tenant": "one"},
|
|
128
|
+
)
|
|
129
|
+
client.post.assert_called_once_with("embed_sparse", json={"inputs": "query: cheese </s>"})
|
|
130
|
+
response.raise_for_status.assert_called_once_with()
|
|
131
|
+
assert result == {"sparse_embedding": SparseEmbedding(indices=[12, 99], values=[1.0, 0.25])}
|
|
132
|
+
|
|
133
|
+
def test_token_is_sent_as_bearer_authorization(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
134
|
+
monkeypatch.delenv("HF_API_TOKEN", raising=False)
|
|
135
|
+
monkeypatch.setenv("HF_TOKEN", "env-token")
|
|
136
|
+
embedder = HuggingFaceAPISparseTextEmbedder()
|
|
137
|
+
|
|
138
|
+
with patched_client() as (client, constructor):
|
|
139
|
+
client.post.return_value = sparse_response([[{"index": 1, "value": 1}]])
|
|
140
|
+
embedder.run("text")
|
|
141
|
+
|
|
142
|
+
assert constructor.call_args.kwargs["headers"] == {"Authorization": "Bearer env-token"}
|
|
143
|
+
|
|
144
|
+
def test_explicit_authorization_header_wins_over_token(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
145
|
+
"""An explicit header must not be replaced by a token that only happens to be set in the environment."""
|
|
146
|
+
monkeypatch.delenv("HF_API_TOKEN", raising=False)
|
|
147
|
+
monkeypatch.setenv("HF_TOKEN", "env-token")
|
|
148
|
+
embedder = HuggingFaceAPISparseTextEmbedder(headers={"Authorization": "Basic test-key"})
|
|
149
|
+
|
|
150
|
+
with patched_client() as (client, constructor):
|
|
151
|
+
client.post.return_value = sparse_response([[{"index": 1, "value": 1}]])
|
|
152
|
+
embedder.run("text")
|
|
153
|
+
|
|
154
|
+
assert constructor.call_args.kwargs["headers"] == {"Authorization": "Basic test-key"}
|
|
155
|
+
|
|
156
|
+
def test_token_is_resolved_per_call(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
157
|
+
"""The token is read at request time, so a rotated environment variable is picked up."""
|
|
158
|
+
monkeypatch.delenv("HF_API_TOKEN", raising=False)
|
|
159
|
+
monkeypatch.setenv("HF_TOKEN", "first-token")
|
|
160
|
+
embedder = HuggingFaceAPISparseTextEmbedder()
|
|
161
|
+
|
|
162
|
+
with patched_client() as (client, constructor):
|
|
163
|
+
client.post.return_value = sparse_response([[{"index": 1, "value": 1}]])
|
|
164
|
+
embedder.run("text")
|
|
165
|
+
monkeypatch.setenv("HF_TOKEN", "second-token")
|
|
166
|
+
embedder.run("text")
|
|
167
|
+
|
|
168
|
+
assert [each.kwargs["headers"]["Authorization"] for each in constructor.call_args_list] == [
|
|
169
|
+
"Bearer first-token",
|
|
170
|
+
"Bearer second-token",
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
@pytest.mark.asyncio
|
|
174
|
+
async def test_run_async_uses_async_client_and_request_options(self) -> None:
|
|
175
|
+
embedder = HuggingFaceAPISparseTextEmbedder(
|
|
176
|
+
api_base_url="http://tei:8080/", token=Secret.from_token("token"), timeout=9, headers={"X-Test": "yes"}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
with patched_client(is_async=True) as (client, constructor):
|
|
180
|
+
response = sparse_response([[{"index": 7, "value": 2.5}]])
|
|
181
|
+
client.post.return_value = response
|
|
182
|
+
result = await embedder.run_async("input")
|
|
183
|
+
|
|
184
|
+
constructor.assert_called_once_with(
|
|
185
|
+
base_url="http://tei:8080/", timeout=9, headers={"Authorization": "Bearer token", "X-Test": "yes"}
|
|
186
|
+
)
|
|
187
|
+
client.post.assert_awaited_once_with("embed_sparse", json={"inputs": "input"})
|
|
188
|
+
response.raise_for_status.assert_called_once_with()
|
|
189
|
+
assert result["sparse_embedding"] == SparseEmbedding(indices=[7], values=[2.5])
|
|
190
|
+
|
|
191
|
+
@pytest.mark.asyncio
|
|
192
|
+
async def test_run_async_builds_and_closes_a_client_per_call(self) -> None:
|
|
193
|
+
"""
|
|
194
|
+
The component must not hold on to an `httpx.AsyncClient`.
|
|
195
|
+
|
|
196
|
+
A cached client binds its keep-alive connection pool to the first event loop that used it, so a component
|
|
197
|
+
reused under a second `asyncio.run` would fail with `RuntimeError: Event loop is closed`.
|
|
198
|
+
"""
|
|
199
|
+
embedder = HuggingFaceAPISparseTextEmbedder()
|
|
200
|
+
|
|
201
|
+
with patched_client(is_async=True) as (client, constructor):
|
|
202
|
+
client.post.return_value = sparse_response([[{"index": 1, "value": 1}]])
|
|
203
|
+
await embedder.run_async("one")
|
|
204
|
+
await embedder.run_async("two")
|
|
205
|
+
|
|
206
|
+
assert constructor.call_count == 2
|
|
207
|
+
assert client.__aexit__.await_count == 2
|
|
208
|
+
assert not hasattr(embedder, "_async_client")
|
|
209
|
+
|
|
210
|
+
def test_run_builds_and_closes_a_client_per_call(self) -> None:
|
|
211
|
+
embedder = HuggingFaceAPISparseTextEmbedder()
|
|
212
|
+
|
|
213
|
+
with patched_client() as (client, constructor):
|
|
214
|
+
client.post.return_value = sparse_response([[{"index": 1, "value": 1}]])
|
|
215
|
+
embedder.run("one")
|
|
216
|
+
embedder.run("two")
|
|
217
|
+
|
|
218
|
+
assert constructor.call_count == 2
|
|
219
|
+
assert client.__exit__.call_count == 2
|
|
220
|
+
assert not hasattr(embedder, "_client")
|
|
221
|
+
|
|
222
|
+
@pytest.mark.parametrize(
|
|
223
|
+
"payload",
|
|
224
|
+
[
|
|
225
|
+
{"index": 1, "value": 0.5},
|
|
226
|
+
[],
|
|
227
|
+
["not-a-list"],
|
|
228
|
+
[[{"index": 1}]],
|
|
229
|
+
[[{"index": "1", "value": 0.5}]],
|
|
230
|
+
[[{"index": 1, "value": True}]],
|
|
231
|
+
],
|
|
232
|
+
)
|
|
233
|
+
def test_run_rejects_malformed_tei_responses(self, payload: Any) -> None:
|
|
234
|
+
embedder = HuggingFaceAPISparseTextEmbedder()
|
|
235
|
+
|
|
236
|
+
with patched_client() as (client, _), pytest.raises(ValueError):
|
|
237
|
+
client.post.return_value = sparse_response(payload)
|
|
238
|
+
embedder.run("text")
|
|
239
|
+
|
|
240
|
+
def test_run_propagates_http_error(self) -> None:
|
|
241
|
+
request = httpx.Request("POST", "http://localhost:8080/embed_sparse")
|
|
242
|
+
embedder = HuggingFaceAPISparseTextEmbedder()
|
|
243
|
+
|
|
244
|
+
with patched_client() as (client, _), pytest.raises(httpx.HTTPStatusError) as exc_info:
|
|
245
|
+
client.post.return_value = httpx.Response(503, request=request)
|
|
246
|
+
embedder.run("text")
|
|
247
|
+
|
|
248
|
+
assert exc_info.value.response.status_code == 503
|
|
249
|
+
|
|
250
|
+
@pytest.mark.integration
|
|
251
|
+
def test_live_run_tei(self) -> None:
|
|
252
|
+
result = HuggingFaceAPISparseTextEmbedder(api_base_url=API_BASE_URL).run("sparse retrieval")
|
|
253
|
+
|
|
254
|
+
assert isinstance(result["sparse_embedding"], SparseEmbedding)
|
|
255
|
+
assert result["sparse_embedding"].indices
|
|
256
|
+
|
|
257
|
+
@pytest.mark.integration
|
|
258
|
+
@pytest.mark.asyncio
|
|
259
|
+
async def test_live_run_async_tei(self) -> None:
|
|
260
|
+
result = await HuggingFaceAPISparseTextEmbedder(api_base_url=API_BASE_URL).run_async("sparse retrieval")
|
|
261
|
+
|
|
262
|
+
assert isinstance(result["sparse_embedding"], SparseEmbedding)
|
|
263
|
+
assert result["sparse_embedding"].indices
|
|
@@ -1,7 +0,0 @@
|
|
|
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 .text_embedder import HuggingFaceAPITextEmbedder
|
|
6
|
-
|
|
7
|
-
__all__ = ["HuggingFaceAPIDocumentEmbedder", "HuggingFaceAPITextEmbedder"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/tests/test_chat_generator.py
RENAMED
|
File without changes
|
{huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/tests/test_document_embedder.py
RENAMED
|
File without changes
|
{huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/tests/test_files/apple.jpg
RENAMED
|
File without changes
|
|
File without changes
|
{huggingface_api_haystack-0.5.0 → huggingface_api_haystack-0.6.0}/tests/test_text_embedder.py
RENAMED
|
File without changes
|
|
File without changes
|