perplexity-haystack 0.1.0__tar.gz → 0.2.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.
- perplexity_haystack-0.2.0/CHANGELOG.md +16 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/PKG-INFO +1 -1
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/perplexity/document_embedder.py +113 -5
- perplexity_haystack-0.2.0/src/haystack_integrations/components/embedders/perplexity/embedding_encoding.py +34 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/perplexity/text_embedder.py +27 -4
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/generators/perplexity/chat/chat_generator.py +16 -19
- perplexity_haystack-0.2.0/tests/test_embedding_encoding.py +24 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/tests/test_perplexity_chat_generator.py +72 -9
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/tests/test_perplexity_document_embedder.py +210 -9
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/tests/test_perplexity_text_embedder.py +71 -8
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/.gitignore +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/LICENSE.txt +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/README.md +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/pydoc/config_docusaurus.yml +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/pyproject.toml +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/perplexity/__init__.py +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/py.typed +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/generators/perplexity/__init__.py +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/generators/perplexity/chat/__init__.py +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/generators/py.typed +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/websearch/perplexity/__init__.py +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/websearch/perplexity/perplexity_websearch.py +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/websearch/py.typed +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/tests/__init__.py +0 -0
- {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/tests/test_perplexity_websearch.py +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [integrations/perplexity-v0.1.1] - 2026-05-27
|
|
4
|
+
|
|
5
|
+
### 🐛 Bug Fixes
|
|
6
|
+
|
|
7
|
+
- *(perplexity)* Decode embedding API responses (#3344)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [integrations/perplexity-v0.1.0] - 2026-05-13
|
|
11
|
+
|
|
12
|
+
### 🌀 Miscellaneous
|
|
13
|
+
|
|
14
|
+
- Add Perplexity integration package (#3262)
|
|
15
|
+
|
|
16
|
+
<!-- generated by git-cliff -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: perplexity-haystack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Haystack integration for Perplexity Web Search
|
|
5
5
|
Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/perplexity#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
|
|
@@ -3,11 +3,24 @@
|
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
5
|
import importlib.metadata
|
|
6
|
+
from collections.abc import Iterable
|
|
6
7
|
from typing import Any, ClassVar
|
|
7
8
|
|
|
8
|
-
from haystack import component, default_from_dict, default_to_dict
|
|
9
|
+
from haystack import component, default_from_dict, default_to_dict, logging
|
|
9
10
|
from haystack.components.embedders import OpenAIDocumentEmbedder
|
|
10
11
|
from haystack.utils.auth import Secret
|
|
12
|
+
from more_itertools import batched
|
|
13
|
+
from openai import APIError
|
|
14
|
+
from openai.types.create_embedding_response import CreateEmbeddingResponse
|
|
15
|
+
from tqdm import tqdm
|
|
16
|
+
from tqdm.asyncio import tqdm as async_tqdm
|
|
17
|
+
|
|
18
|
+
from haystack_integrations.components.embedders.perplexity.embedding_encoding import (
|
|
19
|
+
_decode_embedding,
|
|
20
|
+
_validate_encoding_format,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
11
24
|
|
|
12
25
|
_INTEGRATION_SLUG = "haystack"
|
|
13
26
|
_PACKAGE_NAME = "perplexity-haystack"
|
|
@@ -73,6 +86,7 @@ class PerplexityDocumentEmbedder(OpenAIDocumentEmbedder):
|
|
|
73
86
|
progress_bar: bool = True,
|
|
74
87
|
meta_fields_to_embed: list[str] | None = None,
|
|
75
88
|
embedding_separator: str = "\n",
|
|
89
|
+
encoding_format: str = "base64_int8",
|
|
76
90
|
timeout: float | None = None,
|
|
77
91
|
max_retries: int | None = None,
|
|
78
92
|
http_client_kwargs: dict[str, Any] | None = None,
|
|
@@ -99,6 +113,8 @@ class PerplexityDocumentEmbedder(OpenAIDocumentEmbedder):
|
|
|
99
113
|
List of meta fields that should be embedded along with the Document text.
|
|
100
114
|
:param embedding_separator:
|
|
101
115
|
Separator used to concatenate the meta fields to the Document text.
|
|
116
|
+
:param encoding_format:
|
|
117
|
+
The Perplexity embedding encoding format. Supported values are `base64_int8` and `base64_binary`.
|
|
102
118
|
:param timeout:
|
|
103
119
|
Timeout for Perplexity client calls. If not set, it defaults to either the `OPENAI_TIMEOUT` environment
|
|
104
120
|
variable, or 30 seconds.
|
|
@@ -109,6 +125,7 @@ class PerplexityDocumentEmbedder(OpenAIDocumentEmbedder):
|
|
|
109
125
|
A dictionary of keyword arguments to configure a custom `httpx.Client`or `httpx.AsyncClient`.
|
|
110
126
|
For more information, see the [HTTPX documentation](https://www.python-httpx.org/api/#client).
|
|
111
127
|
"""
|
|
128
|
+
self.encoding_format = _validate_encoding_format(encoding_format)
|
|
112
129
|
super(PerplexityDocumentEmbedder, self).__init__( # noqa: UP008
|
|
113
130
|
api_key=api_key,
|
|
114
131
|
model=model,
|
|
@@ -125,9 +142,99 @@ class PerplexityDocumentEmbedder(OpenAIDocumentEmbedder):
|
|
|
125
142
|
max_retries=max_retries,
|
|
126
143
|
http_client_kwargs=_http_client_kwargs_with_attribution(http_client_kwargs),
|
|
127
144
|
)
|
|
128
|
-
self.http_client_kwargs
|
|
129
|
-
|
|
130
|
-
self.
|
|
145
|
+
# self.http_client_kwargs keeps the attribution header so that haystack-ai >= 3.0 builds the clients
|
|
146
|
+
# with it at warm-up; the user-provided value is preserved for serialization
|
|
147
|
+
self._http_client_kwargs = http_client_kwargs
|
|
148
|
+
|
|
149
|
+
def _decode_response_embeddings(self, response: CreateEmbeddingResponse) -> list[list[float]]:
|
|
150
|
+
return [_decode_embedding(str(el.embedding), self.encoding_format) for el in response.data]
|
|
151
|
+
|
|
152
|
+
def _embed_batch(
|
|
153
|
+
self, texts_to_embed: dict[str, str], batch_size: int
|
|
154
|
+
) -> tuple[dict[str, list[float]], dict[str, Any]]:
|
|
155
|
+
"""
|
|
156
|
+
Embed a list of texts in batches.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
doc_ids_to_embeddings: dict[str, list[float]] = {}
|
|
160
|
+
meta: dict[str, Any] = {}
|
|
161
|
+
for batch in tqdm(
|
|
162
|
+
batched(texts_to_embed.items(), batch_size), disable=not self.progress_bar, desc="Calculating embeddings"
|
|
163
|
+
):
|
|
164
|
+
args: dict[str, Any] = {
|
|
165
|
+
"model": self.model,
|
|
166
|
+
"input": [b[1] for b in batch],
|
|
167
|
+
"encoding_format": self.encoding_format,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
# with haystack-ai >= 3.0 the client is Optional and built by the warm_up call in run
|
|
172
|
+
response = self.client.embeddings.create(**args) # type: ignore[union-attr]
|
|
173
|
+
except APIError as exc:
|
|
174
|
+
ids = ", ".join(b[0] for b in batch)
|
|
175
|
+
msg = "Failed embedding of documents {ids} caused by {exc}"
|
|
176
|
+
logger.exception(msg, ids=ids, exc=exc)
|
|
177
|
+
if self.raise_on_failure:
|
|
178
|
+
raise exc
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
embeddings = self._decode_response_embeddings(response)
|
|
182
|
+
doc_ids_to_embeddings.update(dict(zip((b[0] for b in batch), embeddings, strict=True)))
|
|
183
|
+
|
|
184
|
+
if "model" not in meta:
|
|
185
|
+
meta["model"] = response.model
|
|
186
|
+
if "usage" not in meta:
|
|
187
|
+
meta["usage"] = dict(response.usage)
|
|
188
|
+
else:
|
|
189
|
+
meta["usage"]["prompt_tokens"] += response.usage.prompt_tokens
|
|
190
|
+
meta["usage"]["total_tokens"] += response.usage.total_tokens
|
|
191
|
+
|
|
192
|
+
return doc_ids_to_embeddings, meta
|
|
193
|
+
|
|
194
|
+
async def _embed_batch_async(
|
|
195
|
+
self, texts_to_embed: dict[str, str], batch_size: int
|
|
196
|
+
) -> tuple[dict[str, list[float]], dict[str, Any]]:
|
|
197
|
+
"""
|
|
198
|
+
Embed a list of texts in batches asynchronously.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
doc_ids_to_embeddings: dict[str, list[float]] = {}
|
|
202
|
+
meta: dict[str, Any] = {}
|
|
203
|
+
|
|
204
|
+
batches: Iterable[tuple[tuple[str, str], ...]] = list(batched(texts_to_embed.items(), batch_size))
|
|
205
|
+
if self.progress_bar:
|
|
206
|
+
batches = async_tqdm(batches, desc="Calculating embeddings")
|
|
207
|
+
|
|
208
|
+
for batch in batches:
|
|
209
|
+
args: dict[str, Any] = {
|
|
210
|
+
"model": self.model,
|
|
211
|
+
"input": [b[1] for b in batch],
|
|
212
|
+
"encoding_format": self.encoding_format,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# with haystack-ai >= 3.0 the client is Optional and built by the warm_up_async call in run_async
|
|
217
|
+
response = await self.async_client.embeddings.create(**args) # type: ignore[union-attr]
|
|
218
|
+
except APIError as exc:
|
|
219
|
+
ids = ", ".join(b[0] for b in batch)
|
|
220
|
+
msg = "Failed embedding of documents {ids} caused by {exc}"
|
|
221
|
+
logger.exception(msg, ids=ids, exc=exc)
|
|
222
|
+
if self.raise_on_failure:
|
|
223
|
+
raise exc
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
embeddings = self._decode_response_embeddings(response)
|
|
227
|
+
doc_ids_to_embeddings.update(dict(zip((b[0] for b in batch), embeddings, strict=True)))
|
|
228
|
+
|
|
229
|
+
if "model" not in meta:
|
|
230
|
+
meta["model"] = response.model
|
|
231
|
+
if "usage" not in meta:
|
|
232
|
+
meta["usage"] = dict(response.usage)
|
|
233
|
+
else:
|
|
234
|
+
meta["usage"]["prompt_tokens"] += response.usage.prompt_tokens
|
|
235
|
+
meta["usage"]["total_tokens"] += response.usage.total_tokens
|
|
236
|
+
|
|
237
|
+
return doc_ids_to_embeddings, meta
|
|
131
238
|
|
|
132
239
|
def to_dict(self) -> dict[str, Any]:
|
|
133
240
|
"""
|
|
@@ -147,9 +254,10 @@ class PerplexityDocumentEmbedder(OpenAIDocumentEmbedder):
|
|
|
147
254
|
progress_bar=self.progress_bar,
|
|
148
255
|
meta_fields_to_embed=self.meta_fields_to_embed,
|
|
149
256
|
embedding_separator=self.embedding_separator,
|
|
257
|
+
encoding_format=self.encoding_format,
|
|
150
258
|
timeout=self.timeout,
|
|
151
259
|
max_retries=self.max_retries,
|
|
152
|
-
http_client_kwargs=self.
|
|
260
|
+
http_client_kwargs=self._http_client_kwargs,
|
|
153
261
|
)
|
|
154
262
|
|
|
155
263
|
@classmethod
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
SUPPORTED_ENCODING_FORMATS = {"base64_int8", "base64_binary"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _validate_encoding_format(encoding_format: str) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Validate Perplexity's embedding encoding format.
|
|
15
|
+
"""
|
|
16
|
+
if encoding_format not in SUPPORTED_ENCODING_FORMATS:
|
|
17
|
+
supported_formats = "', '".join(sorted(SUPPORTED_ENCODING_FORMATS))
|
|
18
|
+
msg = f"Unsupported encoding_format='{encoding_format}'. Use '{supported_formats}'."
|
|
19
|
+
raise ValueError(msg)
|
|
20
|
+
return encoding_format
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _decode_embedding(embedding: str, encoding_format: str) -> list[float]:
|
|
24
|
+
"""
|
|
25
|
+
Decode a Perplexity base64 embedding into Haystack's list[float] representation.
|
|
26
|
+
"""
|
|
27
|
+
raw_embedding = base64.b64decode(embedding)
|
|
28
|
+
if encoding_format == "base64_int8":
|
|
29
|
+
return np.frombuffer(raw_embedding, dtype=np.int8).astype(np.float32).tolist()
|
|
30
|
+
if encoding_format == "base64_binary":
|
|
31
|
+
return np.unpackbits(np.frombuffer(raw_embedding, dtype=np.uint8)).astype(np.float32).tolist()
|
|
32
|
+
|
|
33
|
+
msg = f"Unsupported encoding_format='{encoding_format}'."
|
|
34
|
+
raise ValueError(msg)
|
|
@@ -8,6 +8,12 @@ from typing import Any, ClassVar
|
|
|
8
8
|
from haystack import component, default_from_dict, default_to_dict
|
|
9
9
|
from haystack.components.embedders import OpenAITextEmbedder
|
|
10
10
|
from haystack.utils.auth import Secret
|
|
11
|
+
from openai.types.create_embedding_response import CreateEmbeddingResponse
|
|
12
|
+
|
|
13
|
+
from haystack_integrations.components.embedders.perplexity.embedding_encoding import (
|
|
14
|
+
_decode_embedding,
|
|
15
|
+
_validate_encoding_format,
|
|
16
|
+
)
|
|
11
17
|
|
|
12
18
|
_INTEGRATION_SLUG = "haystack"
|
|
13
19
|
_PACKAGE_NAME = "perplexity-haystack"
|
|
@@ -64,6 +70,7 @@ class PerplexityTextEmbedder(OpenAITextEmbedder):
|
|
|
64
70
|
api_base_url: str | None = "https://api.perplexity.ai/v1",
|
|
65
71
|
prefix: str = "",
|
|
66
72
|
suffix: str = "",
|
|
73
|
+
encoding_format: str = "base64_int8",
|
|
67
74
|
timeout: float | None = None,
|
|
68
75
|
max_retries: int | None = None,
|
|
69
76
|
http_client_kwargs: dict[str, Any] | None = None,
|
|
@@ -81,6 +88,8 @@ class PerplexityTextEmbedder(OpenAITextEmbedder):
|
|
|
81
88
|
A string to add to the beginning of each text.
|
|
82
89
|
:param suffix:
|
|
83
90
|
A string to add to the end of each text.
|
|
91
|
+
:param encoding_format:
|
|
92
|
+
The Perplexity embedding encoding format. Supported values are `base64_int8` and `base64_binary`.
|
|
84
93
|
:param timeout:
|
|
85
94
|
Timeout for Perplexity client calls. If not set, it defaults to either the `OPENAI_TIMEOUT` environment
|
|
86
95
|
variable, or 30 seconds.
|
|
@@ -92,6 +101,7 @@ class PerplexityTextEmbedder(OpenAITextEmbedder):
|
|
|
92
101
|
For more information, see the [HTTPX documentation](https://www.python-httpx.org/api/#client).
|
|
93
102
|
"""
|
|
94
103
|
|
|
104
|
+
self.encoding_format = _validate_encoding_format(encoding_format)
|
|
95
105
|
super(PerplexityTextEmbedder, self).__init__( # noqa: UP008
|
|
96
106
|
api_key=api_key,
|
|
97
107
|
model=model,
|
|
@@ -104,9 +114,21 @@ class PerplexityTextEmbedder(OpenAITextEmbedder):
|
|
|
104
114
|
max_retries=max_retries,
|
|
105
115
|
http_client_kwargs=_http_client_kwargs_with_attribution(http_client_kwargs),
|
|
106
116
|
)
|
|
107
|
-
self.http_client_kwargs
|
|
108
|
-
|
|
109
|
-
self.
|
|
117
|
+
# self.http_client_kwargs keeps the attribution header so that haystack-ai >= 3.0 builds the clients
|
|
118
|
+
# with it at warm-up; the user-provided value is preserved for serialization
|
|
119
|
+
self._http_client_kwargs = http_client_kwargs
|
|
120
|
+
|
|
121
|
+
def _prepare_input(self, text: str) -> dict[str, Any]:
|
|
122
|
+
kwargs = OpenAITextEmbedder._prepare_input(self, text=text)
|
|
123
|
+
kwargs["input"] = [kwargs["input"]]
|
|
124
|
+
kwargs["encoding_format"] = self.encoding_format
|
|
125
|
+
return kwargs
|
|
126
|
+
|
|
127
|
+
def _prepare_output(self, result: CreateEmbeddingResponse) -> dict[str, Any]:
|
|
128
|
+
return {
|
|
129
|
+
"embedding": _decode_embedding(str(result.data[0].embedding), self.encoding_format),
|
|
130
|
+
"meta": {"model": result.model, "usage": dict(result.usage)},
|
|
131
|
+
}
|
|
110
132
|
|
|
111
133
|
def to_dict(self) -> dict[str, Any]:
|
|
112
134
|
"""
|
|
@@ -122,9 +144,10 @@ class PerplexityTextEmbedder(OpenAITextEmbedder):
|
|
|
122
144
|
api_base_url=self.api_base_url,
|
|
123
145
|
prefix=self.prefix,
|
|
124
146
|
suffix=self.suffix,
|
|
147
|
+
encoding_format=self.encoding_format,
|
|
125
148
|
timeout=self.timeout,
|
|
126
149
|
max_retries=self.max_retries,
|
|
127
|
-
http_client_kwargs=self.
|
|
150
|
+
http_client_kwargs=self._http_client_kwargs,
|
|
128
151
|
)
|
|
129
152
|
|
|
130
153
|
@classmethod
|
|
@@ -25,20 +25,15 @@ def _attribution_header() -> str:
|
|
|
25
25
|
return f"{_INTEGRATION_SLUG}/{version}"
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if with_options is not None:
|
|
38
|
-
return with_options(default_headers=headers)
|
|
39
|
-
|
|
40
|
-
client._custom_headers = {**getattr(client, "_custom_headers", {}), **headers}
|
|
41
|
-
return client
|
|
28
|
+
def _http_client_kwargs_with_headers(
|
|
29
|
+
http_client_kwargs: dict[str, Any] | None,
|
|
30
|
+
extra_headers: dict[str, Any] | None,
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
kwargs = dict(http_client_kwargs or {})
|
|
33
|
+
headers = {**kwargs.get("headers", {}), **(extra_headers or {})}
|
|
34
|
+
headers["X-Pplx-Integration"] = _attribution_header()
|
|
35
|
+
kwargs["headers"] = headers
|
|
36
|
+
return kwargs
|
|
42
37
|
|
|
43
38
|
|
|
44
39
|
@component
|
|
@@ -133,12 +128,12 @@ class PerplexityChatGenerator(OpenAIResponsesChatGenerator):
|
|
|
133
128
|
max_retries=max_retries,
|
|
134
129
|
tools=tools,
|
|
135
130
|
tools_strict=tools_strict,
|
|
136
|
-
http_client_kwargs=http_client_kwargs,
|
|
131
|
+
http_client_kwargs=_http_client_kwargs_with_headers(http_client_kwargs, extra_headers),
|
|
137
132
|
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
self.
|
|
133
|
+
# self.http_client_kwargs carries the attribution (and extra) headers so that the parent bakes them into
|
|
134
|
+
# the httpx client whether it is built eagerly (haystack-ai 2.x) or at warm-up (haystack-ai >= 3.0);
|
|
135
|
+
# the user-provided value is preserved for serialization
|
|
136
|
+
self._http_client_kwargs = http_client_kwargs
|
|
142
137
|
|
|
143
138
|
def to_dict(self) -> dict[str, Any]:
|
|
144
139
|
"""
|
|
@@ -150,6 +145,8 @@ class PerplexityChatGenerator(OpenAIResponsesChatGenerator):
|
|
|
150
145
|
data = super(PerplexityChatGenerator, self).to_dict() # noqa: UP008
|
|
151
146
|
data["type"] = generate_qualified_class_name(type(self))
|
|
152
147
|
data["init_parameters"]["extra_headers"] = self.extra_headers
|
|
148
|
+
# serialize the user-provided value, not the internal one enriched with attribution headers
|
|
149
|
+
data["init_parameters"]["http_client_kwargs"] = self._http_client_kwargs
|
|
153
150
|
return data
|
|
154
151
|
|
|
155
152
|
@classmethod
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from haystack_integrations.components.embedders.perplexity.embedding_encoding import (
|
|
8
|
+
_decode_embedding,
|
|
9
|
+
_validate_encoding_format,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_validate_encoding_format_rejects_unsupported_format():
|
|
14
|
+
with pytest.raises(ValueError) as exc_info:
|
|
15
|
+
_validate_encoding_format("base64_float16")
|
|
16
|
+
|
|
17
|
+
assert str(exc_info.value) == "Unsupported encoding_format='base64_float16'. Use 'base64_binary', 'base64_int8'."
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_decode_embedding_rejects_unsupported_format():
|
|
21
|
+
with pytest.raises(ValueError) as exc_info:
|
|
22
|
+
_decode_embedding("", "base64_float16")
|
|
23
|
+
|
|
24
|
+
assert str(exc_info.value) == "Unsupported encoding_format='base64_float16'."
|
{perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/tests/test_perplexity_chat_generator.py
RENAMED
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
7
|
from unittest.mock import patch
|
|
8
8
|
|
|
9
|
+
import httpx
|
|
9
10
|
import pytest
|
|
10
11
|
from haystack.components.generators.utils import print_streaming_chunk
|
|
11
12
|
from haystack.dataclasses import ChatMessage
|
|
@@ -37,6 +38,27 @@ def _make_response() -> Response:
|
|
|
37
38
|
return Response.construct(**response_data)
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
def _make_transport(captured: list[httpx.Request]) -> httpx.MockTransport:
|
|
42
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
43
|
+
captured.append(request)
|
|
44
|
+
return httpx.Response(
|
|
45
|
+
200,
|
|
46
|
+
json={
|
|
47
|
+
"id": "resp_test",
|
|
48
|
+
"created_at": datetime.now(tz=timezone.utc).timestamp(),
|
|
49
|
+
"model": "openai/gpt-5.4",
|
|
50
|
+
"object": "response",
|
|
51
|
+
"output": [],
|
|
52
|
+
"parallel_tool_calls": True,
|
|
53
|
+
"tool_choice": "auto",
|
|
54
|
+
"tools": [],
|
|
55
|
+
},
|
|
56
|
+
headers={"Content-Type": "application/json"},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return httpx.MockTransport(handler)
|
|
60
|
+
|
|
61
|
+
|
|
40
62
|
@pytest.fixture
|
|
41
63
|
def chat_messages() -> list[ChatMessage]:
|
|
42
64
|
return [
|
|
@@ -63,7 +85,7 @@ class TestPerplexityChatGenerator:
|
|
|
63
85
|
|
|
64
86
|
component = PerplexityChatGenerator()
|
|
65
87
|
|
|
66
|
-
assert component.
|
|
88
|
+
assert component.api_key.resolve_value() == "test-api-key"
|
|
67
89
|
assert component.model == "openai/gpt-5.4"
|
|
68
90
|
assert component.api_base_url == "https://api.perplexity.ai/v1"
|
|
69
91
|
assert component.streaming_callback is None
|
|
@@ -84,7 +106,7 @@ class TestPerplexityChatGenerator:
|
|
|
84
106
|
http_client_kwargs={"proxy": "http://localhost:8080"},
|
|
85
107
|
)
|
|
86
108
|
|
|
87
|
-
assert component.
|
|
109
|
+
assert component.api_key.resolve_value() == "test-api-key"
|
|
88
110
|
assert component.model == "anthropic/claude-sonnet-4-6"
|
|
89
111
|
assert component.streaming_callback is print_streaming_chunk
|
|
90
112
|
assert component.generation_kwargs == {
|
|
@@ -96,7 +118,16 @@ class TestPerplexityChatGenerator:
|
|
|
96
118
|
assert component.extra_headers == {"test-header": "test-value"}
|
|
97
119
|
assert component.timeout == 10
|
|
98
120
|
assert component.max_retries == 2
|
|
99
|
-
|
|
121
|
+
|
|
122
|
+
assert component._http_client_kwargs == {"proxy": "http://localhost:8080"}
|
|
123
|
+
assert component.http_client_kwargs["headers"]["test-header"] == "test-value"
|
|
124
|
+
assert component.http_client_kwargs["headers"]["X-Pplx-Integration"].startswith("haystack/")
|
|
125
|
+
|
|
126
|
+
def test_warm_up(self, monkeypatch):
|
|
127
|
+
monkeypatch.setenv("PERPLEXITY_API_KEY", "test-api-key")
|
|
128
|
+
component = PerplexityChatGenerator()
|
|
129
|
+
component.warm_up() # with haystack-ai >= 3.0 the client is created during warm-up
|
|
130
|
+
assert component.client.api_key == "test-api-key"
|
|
100
131
|
|
|
101
132
|
def test_to_dict_default_round_trip(self, monkeypatch):
|
|
102
133
|
monkeypatch.setenv("PERPLEXITY_API_KEY", "test-api-key")
|
|
@@ -177,7 +208,7 @@ class TestPerplexityChatGenerator:
|
|
|
177
208
|
assert deserialized.extra_headers == {"test-header": "test-value"}
|
|
178
209
|
assert deserialized.timeout == 10
|
|
179
210
|
assert deserialized.max_retries == 2
|
|
180
|
-
assert deserialized.
|
|
211
|
+
assert deserialized._http_client_kwargs == {"proxy": "http://localhost:8080"}
|
|
181
212
|
|
|
182
213
|
def test_run_uses_responses_create(self, chat_messages):
|
|
183
214
|
component = PerplexityChatGenerator(api_key=Secret.from_token("test-api-key"))
|
|
@@ -193,16 +224,48 @@ class TestPerplexityChatGenerator:
|
|
|
193
224
|
assert "messages" not in call_kwargs
|
|
194
225
|
assert call_kwargs["stream"] is False
|
|
195
226
|
|
|
196
|
-
def
|
|
227
|
+
def test_http_client_kwargs_with_headers_merges_extra_and_attribution(self):
|
|
228
|
+
kwargs = chat_generator_module._http_client_kwargs_with_headers(
|
|
229
|
+
{"headers": {"existing-header": "existing-value"}},
|
|
230
|
+
{"test-header": "test-value"},
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
assert kwargs["headers"]["existing-header"] == "existing-value"
|
|
234
|
+
assert kwargs["headers"]["test-header"] == "test-value"
|
|
235
|
+
assert kwargs["headers"]["X-Pplx-Integration"].startswith("haystack/")
|
|
236
|
+
|
|
237
|
+
def test_run_sends_attribution_header(self, chat_messages):
|
|
238
|
+
captured: list[httpx.Request] = []
|
|
197
239
|
component = PerplexityChatGenerator(
|
|
198
240
|
api_key=Secret.from_token("test-api-key"),
|
|
199
241
|
extra_headers={"test-header": "test-value"},
|
|
242
|
+
http_client_kwargs={"transport": _make_transport(captured)},
|
|
200
243
|
)
|
|
201
244
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
assert
|
|
205
|
-
|
|
245
|
+
component.run(chat_messages)
|
|
246
|
+
|
|
247
|
+
assert len(captured) == 1
|
|
248
|
+
request = captured[0]
|
|
249
|
+
assert request.headers["Authorization"] == "Bearer test-api-key"
|
|
250
|
+
assert request.headers["X-Pplx-Integration"].startswith("haystack/")
|
|
251
|
+
assert request.headers["test-header"] == "test-value"
|
|
252
|
+
|
|
253
|
+
@pytest.mark.asyncio
|
|
254
|
+
async def test_run_async_sends_attribution_header(self, chat_messages):
|
|
255
|
+
captured: list[httpx.Request] = []
|
|
256
|
+
component = PerplexityChatGenerator(
|
|
257
|
+
api_key=Secret.from_token("test-api-key"),
|
|
258
|
+
extra_headers={"test-header": "test-value"},
|
|
259
|
+
http_client_kwargs={"transport": _make_transport(captured)},
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
await component.run_async(chat_messages)
|
|
263
|
+
|
|
264
|
+
assert len(captured) == 1
|
|
265
|
+
request = captured[0]
|
|
266
|
+
assert request.headers["Authorization"] == "Bearer test-api-key"
|
|
267
|
+
assert request.headers["X-Pplx-Integration"].startswith("haystack/")
|
|
268
|
+
assert request.headers["test-header"] == "test-value"
|
|
206
269
|
|
|
207
270
|
|
|
208
271
|
@pytest.mark.skipif(
|
{perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/tests/test_perplexity_document_embedder.py
RENAMED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
+
import base64
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
8
|
+
from types import SimpleNamespace
|
|
7
9
|
|
|
8
10
|
import httpx
|
|
11
|
+
import numpy as np
|
|
9
12
|
import pytest
|
|
10
13
|
from haystack import Document
|
|
11
14
|
from haystack.utils import Secret
|
|
15
|
+
from openai import APIError
|
|
12
16
|
|
|
13
17
|
from haystack_integrations.components.embedders.perplexity import (
|
|
14
18
|
document_embedder as document_embedder_module,
|
|
@@ -17,17 +21,24 @@ from haystack_integrations.components.embedders.perplexity.document_embedder imp
|
|
|
17
21
|
PerplexityDocumentEmbedder,
|
|
18
22
|
)
|
|
19
23
|
|
|
24
|
+
INT8_EMBEDDING = [-2.0, -1.0, 0.0, 1.0, 2.0]
|
|
25
|
+
INT8_EMBEDDING_PAYLOAD = base64.b64encode(np.array(INT8_EMBEDDING, dtype=np.int8).tobytes()).decode()
|
|
26
|
+
BINARY_EMBEDDING = [1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0]
|
|
27
|
+
BINARY_EMBEDDING_PAYLOAD = base64.b64encode(np.packbits(np.array(BINARY_EMBEDDING, dtype=np.uint8)).tobytes()).decode()
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
|
|
30
|
+
def _make_transport(captured: list[httpx.Request], embeddings: list[str] | None = None) -> httpx.MockTransport:
|
|
22
31
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
23
32
|
captured.append(request)
|
|
33
|
+
inputs = json.loads(request.content)["input"]
|
|
34
|
+
response_embeddings = embeddings or [INT8_EMBEDDING_PAYLOAD] * len(inputs)
|
|
24
35
|
return httpx.Response(
|
|
25
36
|
200,
|
|
26
37
|
json={
|
|
27
38
|
"object": "list",
|
|
28
39
|
"data": [
|
|
29
|
-
{"object": "embedding", "embedding":
|
|
30
|
-
|
|
40
|
+
{"object": "embedding", "embedding": embedding, "index": index}
|
|
41
|
+
for index, embedding in enumerate(response_embeddings)
|
|
31
42
|
],
|
|
32
43
|
"model": "pplx-embed-v1-0.6b",
|
|
33
44
|
"usage": {"prompt_tokens": 6, "total_tokens": 6},
|
|
@@ -38,6 +49,14 @@ def _make_transport(captured: list[httpx.Request]) -> httpx.MockTransport:
|
|
|
38
49
|
return httpx.MockTransport(handler)
|
|
39
50
|
|
|
40
51
|
|
|
52
|
+
def _make_api_error() -> APIError:
|
|
53
|
+
return APIError(
|
|
54
|
+
"Failed embedding",
|
|
55
|
+
request=httpx.Request("POST", "https://api.perplexity.ai/v1/embeddings"),
|
|
56
|
+
body=None,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
41
60
|
class TestPerplexityDocumentEmbedder:
|
|
42
61
|
def test_attribution_header_falls_back_when_package_is_not_installed(self, monkeypatch):
|
|
43
62
|
def raise_package_not_found(_package_name: str) -> str:
|
|
@@ -77,6 +96,7 @@ class TestPerplexityDocumentEmbedder:
|
|
|
77
96
|
assert embedder.progress_bar is True
|
|
78
97
|
assert embedder.meta_fields_to_embed == []
|
|
79
98
|
assert embedder.embedding_separator == "\n"
|
|
99
|
+
assert embedder.encoding_format == "base64_int8"
|
|
80
100
|
|
|
81
101
|
def test_init_with_parameters(self):
|
|
82
102
|
embedder = PerplexityDocumentEmbedder(
|
|
@@ -89,6 +109,7 @@ class TestPerplexityDocumentEmbedder:
|
|
|
89
109
|
progress_bar=False,
|
|
90
110
|
meta_fields_to_embed=["test_field"],
|
|
91
111
|
embedding_separator="-",
|
|
112
|
+
encoding_format="base64_binary",
|
|
92
113
|
)
|
|
93
114
|
|
|
94
115
|
assert embedder.api_key == Secret.from_token("test-api-key")
|
|
@@ -100,6 +121,14 @@ class TestPerplexityDocumentEmbedder:
|
|
|
100
121
|
assert embedder.progress_bar is False
|
|
101
122
|
assert embedder.meta_fields_to_embed == ["test_field"]
|
|
102
123
|
assert embedder.embedding_separator == "-"
|
|
124
|
+
assert embedder.encoding_format == "base64_binary"
|
|
125
|
+
|
|
126
|
+
def test_init_rejects_float_encoding_format(self):
|
|
127
|
+
with pytest.raises(ValueError):
|
|
128
|
+
PerplexityDocumentEmbedder(
|
|
129
|
+
api_key=Secret.from_token("test-api-key"),
|
|
130
|
+
encoding_format="float",
|
|
131
|
+
)
|
|
103
132
|
|
|
104
133
|
def test_to_dict(self, monkeypatch):
|
|
105
134
|
monkeypatch.setenv("PERPLEXITY_API_KEY", "test-api-key")
|
|
@@ -122,6 +151,7 @@ class TestPerplexityDocumentEmbedder:
|
|
|
122
151
|
"progress_bar": True,
|
|
123
152
|
"meta_fields_to_embed": [],
|
|
124
153
|
"embedding_separator": "\n",
|
|
154
|
+
"encoding_format": "base64_int8",
|
|
125
155
|
"timeout": None,
|
|
126
156
|
"max_retries": None,
|
|
127
157
|
"http_client_kwargs": None,
|
|
@@ -140,6 +170,7 @@ class TestPerplexityDocumentEmbedder:
|
|
|
140
170
|
progress_bar=False,
|
|
141
171
|
meta_fields_to_embed=["test_field"],
|
|
142
172
|
embedding_separator="-",
|
|
173
|
+
encoding_format="base64_binary",
|
|
143
174
|
timeout=10.0,
|
|
144
175
|
max_retries=2,
|
|
145
176
|
http_client_kwargs={"proxy": "http://localhost:8080"},
|
|
@@ -161,6 +192,7 @@ class TestPerplexityDocumentEmbedder:
|
|
|
161
192
|
"progress_bar": False,
|
|
162
193
|
"meta_fields_to_embed": ["test_field"],
|
|
163
194
|
"embedding_separator": "-",
|
|
195
|
+
"encoding_format": "base64_binary",
|
|
164
196
|
"timeout": 10.0,
|
|
165
197
|
"max_retries": 2,
|
|
166
198
|
"http_client_kwargs": {"proxy": "http://localhost:8080"},
|
|
@@ -183,6 +215,7 @@ class TestPerplexityDocumentEmbedder:
|
|
|
183
215
|
"progress_bar": True,
|
|
184
216
|
"meta_fields_to_embed": [],
|
|
185
217
|
"embedding_separator": "\n",
|
|
218
|
+
"encoding_format": "base64_binary",
|
|
186
219
|
"timeout": None,
|
|
187
220
|
"max_retries": None,
|
|
188
221
|
"http_client_kwargs": None,
|
|
@@ -200,9 +233,11 @@ class TestPerplexityDocumentEmbedder:
|
|
|
200
233
|
assert component.progress_bar is True
|
|
201
234
|
assert component.meta_fields_to_embed == []
|
|
202
235
|
assert component.embedding_separator == "\n"
|
|
236
|
+
assert component.encoding_format == "base64_binary"
|
|
203
237
|
assert component.timeout is None
|
|
204
238
|
assert component.max_retries is None
|
|
205
|
-
|
|
239
|
+
# the user-provided value; component.http_client_kwargs carries the attribution header
|
|
240
|
+
assert component._http_client_kwargs is None
|
|
206
241
|
|
|
207
242
|
def test_run_sends_attribution_header(self):
|
|
208
243
|
captured: list[httpx.Request] = []
|
|
@@ -222,8 +257,8 @@ class TestPerplexityDocumentEmbedder:
|
|
|
222
257
|
result = embedder.run(docs)
|
|
223
258
|
|
|
224
259
|
docs_with_embeddings = result["documents"]
|
|
225
|
-
assert docs_with_embeddings[0].embedding ==
|
|
226
|
-
assert docs_with_embeddings[1].embedding ==
|
|
260
|
+
assert docs_with_embeddings[0].embedding == INT8_EMBEDDING
|
|
261
|
+
assert docs_with_embeddings[1].embedding == INT8_EMBEDDING
|
|
227
262
|
assert len(captured) == 1
|
|
228
263
|
request = captured[0]
|
|
229
264
|
assert request.headers["Authorization"] == "Bearer test-api-key"
|
|
@@ -234,7 +269,172 @@ class TestPerplexityDocumentEmbedder:
|
|
|
234
269
|
"I love cheese",
|
|
235
270
|
"A transformer is a deep learning architecture",
|
|
236
271
|
]
|
|
237
|
-
assert body["encoding_format"] == "
|
|
272
|
+
assert body["encoding_format"] == "base64_int8"
|
|
273
|
+
|
|
274
|
+
@pytest.mark.parametrize(
|
|
275
|
+
("encoding_format", "payload", "expected_embedding"),
|
|
276
|
+
[
|
|
277
|
+
("base64_int8", INT8_EMBEDDING_PAYLOAD, INT8_EMBEDDING),
|
|
278
|
+
("base64_binary", BINARY_EMBEDDING_PAYLOAD, BINARY_EMBEDDING),
|
|
279
|
+
],
|
|
280
|
+
)
|
|
281
|
+
def test_run_decodes_base64_embeddings(self, encoding_format, payload, expected_embedding):
|
|
282
|
+
captured: list[httpx.Request] = []
|
|
283
|
+
embedder = PerplexityDocumentEmbedder(
|
|
284
|
+
api_key=Secret.from_token("test-api-key"),
|
|
285
|
+
encoding_format=encoding_format,
|
|
286
|
+
progress_bar=False,
|
|
287
|
+
http_client_kwargs={"transport": _make_transport(captured, embeddings=[payload])},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
result = embedder.run([Document(content="I love cheese")])
|
|
291
|
+
|
|
292
|
+
embedding = result["documents"][0].embedding
|
|
293
|
+
assert embedding == expected_embedding
|
|
294
|
+
assert len(embedding) == len(expected_embedding)
|
|
295
|
+
assert all(isinstance(value, float) for value in embedding)
|
|
296
|
+
body = json.loads(captured[0].content)
|
|
297
|
+
assert body["encoding_format"] == encoding_format
|
|
298
|
+
|
|
299
|
+
def test_run_accumulates_usage_across_batches(self):
|
|
300
|
+
captured: list[httpx.Request] = []
|
|
301
|
+
embedder = PerplexityDocumentEmbedder(
|
|
302
|
+
api_key=Secret.from_token("test-api-key"),
|
|
303
|
+
batch_size=1,
|
|
304
|
+
progress_bar=False,
|
|
305
|
+
http_client_kwargs={"transport": _make_transport(captured)},
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
result = embedder.run(
|
|
309
|
+
[
|
|
310
|
+
Document(content="I love cheese"),
|
|
311
|
+
Document(content="A transformer is a deep learning architecture"),
|
|
312
|
+
]
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
assert [document.embedding for document in result["documents"]] == [INT8_EMBEDDING, INT8_EMBEDDING]
|
|
316
|
+
assert result["meta"]["usage"] == {"prompt_tokens": 12, "total_tokens": 12}
|
|
317
|
+
assert len(captured) == 2
|
|
318
|
+
|
|
319
|
+
def test_embed_batch_skips_api_error_when_raise_on_failure_is_false(self):
|
|
320
|
+
api_error = _make_api_error()
|
|
321
|
+
|
|
322
|
+
def create(**_kwargs):
|
|
323
|
+
raise api_error
|
|
324
|
+
|
|
325
|
+
embedder = PerplexityDocumentEmbedder(
|
|
326
|
+
api_key=Secret.from_token("test-api-key"),
|
|
327
|
+
progress_bar=False,
|
|
328
|
+
)
|
|
329
|
+
embedder.raise_on_failure = False
|
|
330
|
+
embedder.client = SimpleNamespace(embeddings=SimpleNamespace(create=create))
|
|
331
|
+
|
|
332
|
+
embeddings, meta = embedder._embed_batch({"doc-1": "I love cheese"}, batch_size=1)
|
|
333
|
+
|
|
334
|
+
assert embeddings == {}
|
|
335
|
+
assert meta == {}
|
|
336
|
+
|
|
337
|
+
def test_embed_batch_raises_api_error_when_raise_on_failure_is_true(self):
|
|
338
|
+
api_error = _make_api_error()
|
|
339
|
+
|
|
340
|
+
def create(**_kwargs):
|
|
341
|
+
raise api_error
|
|
342
|
+
|
|
343
|
+
embedder = PerplexityDocumentEmbedder(
|
|
344
|
+
api_key=Secret.from_token("test-api-key"),
|
|
345
|
+
progress_bar=False,
|
|
346
|
+
)
|
|
347
|
+
embedder.raise_on_failure = True
|
|
348
|
+
embedder.client = SimpleNamespace(embeddings=SimpleNamespace(create=create))
|
|
349
|
+
|
|
350
|
+
with pytest.raises(APIError) as exc_info:
|
|
351
|
+
embedder._embed_batch({"doc-1": "I love cheese"}, batch_size=1)
|
|
352
|
+
|
|
353
|
+
assert exc_info.value is api_error
|
|
354
|
+
|
|
355
|
+
@pytest.mark.asyncio
|
|
356
|
+
async def test_run_async_decodes_base64_embeddings(self):
|
|
357
|
+
captured: list[httpx.Request] = []
|
|
358
|
+
embedder = PerplexityDocumentEmbedder(
|
|
359
|
+
api_key=Secret.from_token("test-api-key"),
|
|
360
|
+
progress_bar=False,
|
|
361
|
+
http_client_kwargs={"transport": _make_transport(captured, embeddings=[BINARY_EMBEDDING_PAYLOAD])},
|
|
362
|
+
encoding_format="base64_binary",
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
result = await embedder.run_async([Document(content="I love cheese")])
|
|
366
|
+
|
|
367
|
+
assert result["documents"][0].embedding == BINARY_EMBEDDING
|
|
368
|
+
body = json.loads(captured[0].content)
|
|
369
|
+
assert body["encoding_format"] == "base64_binary"
|
|
370
|
+
|
|
371
|
+
@pytest.mark.asyncio
|
|
372
|
+
async def test_run_async_uses_progress_bar_and_accumulates_usage_across_batches(self, monkeypatch):
|
|
373
|
+
captured: list[httpx.Request] = []
|
|
374
|
+
batches_seen = []
|
|
375
|
+
|
|
376
|
+
def fake_async_tqdm(batches, desc):
|
|
377
|
+
assert desc == "Calculating embeddings"
|
|
378
|
+
batches_seen.extend(batches)
|
|
379
|
+
return batches
|
|
380
|
+
|
|
381
|
+
monkeypatch.setattr(document_embedder_module, "async_tqdm", fake_async_tqdm)
|
|
382
|
+
embedder = PerplexityDocumentEmbedder(
|
|
383
|
+
api_key=Secret.from_token("test-api-key"),
|
|
384
|
+
batch_size=1,
|
|
385
|
+
progress_bar=True,
|
|
386
|
+
http_client_kwargs={"transport": _make_transport(captured)},
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
result = await embedder.run_async(
|
|
390
|
+
[
|
|
391
|
+
Document(content="I love cheese"),
|
|
392
|
+
Document(content="A transformer is a deep learning architecture"),
|
|
393
|
+
]
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
assert [document.embedding for document in result["documents"]] == [INT8_EMBEDDING, INT8_EMBEDDING]
|
|
397
|
+
assert result["meta"]["usage"] == {"prompt_tokens": 12, "total_tokens": 12}
|
|
398
|
+
assert len(batches_seen) == 2
|
|
399
|
+
assert len(captured) == 2
|
|
400
|
+
|
|
401
|
+
@pytest.mark.asyncio
|
|
402
|
+
async def test_embed_batch_async_skips_api_error_when_raise_on_failure_is_false(self):
|
|
403
|
+
api_error = _make_api_error()
|
|
404
|
+
|
|
405
|
+
async def create(**_kwargs):
|
|
406
|
+
raise api_error
|
|
407
|
+
|
|
408
|
+
embedder = PerplexityDocumentEmbedder(
|
|
409
|
+
api_key=Secret.from_token("test-api-key"),
|
|
410
|
+
progress_bar=False,
|
|
411
|
+
)
|
|
412
|
+
embedder.raise_on_failure = False
|
|
413
|
+
embedder.async_client = SimpleNamespace(embeddings=SimpleNamespace(create=create))
|
|
414
|
+
|
|
415
|
+
embeddings, meta = await embedder._embed_batch_async({"doc-1": "I love cheese"}, batch_size=1)
|
|
416
|
+
|
|
417
|
+
assert embeddings == {}
|
|
418
|
+
assert meta == {}
|
|
419
|
+
|
|
420
|
+
@pytest.mark.asyncio
|
|
421
|
+
async def test_embed_batch_async_raises_api_error_when_raise_on_failure_is_true(self):
|
|
422
|
+
api_error = _make_api_error()
|
|
423
|
+
|
|
424
|
+
async def create(**_kwargs):
|
|
425
|
+
raise api_error
|
|
426
|
+
|
|
427
|
+
embedder = PerplexityDocumentEmbedder(
|
|
428
|
+
api_key=Secret.from_token("test-api-key"),
|
|
429
|
+
progress_bar=False,
|
|
430
|
+
)
|
|
431
|
+
embedder.raise_on_failure = True
|
|
432
|
+
embedder.async_client = SimpleNamespace(embeddings=SimpleNamespace(create=create))
|
|
433
|
+
|
|
434
|
+
with pytest.raises(APIError) as exc_info:
|
|
435
|
+
await embedder._embed_batch_async({"doc-1": "I love cheese"}, batch_size=1)
|
|
436
|
+
|
|
437
|
+
assert exc_info.value is api_error
|
|
238
438
|
|
|
239
439
|
def test_run_wrong_input_format(self):
|
|
240
440
|
embedder = PerplexityDocumentEmbedder(api_key=Secret.from_token("test-api-key"))
|
|
@@ -269,7 +469,7 @@ class TestPerplexityDocumentEmbedderInference:
|
|
|
269
469
|
assert len(embedded_docs) == len(docs)
|
|
270
470
|
for doc in embedded_docs:
|
|
271
471
|
assert isinstance(doc.embedding, list)
|
|
272
|
-
assert len(doc.embedding)
|
|
472
|
+
assert len(doc.embedding) == 1024
|
|
273
473
|
assert all(isinstance(x, float) for x in doc.embedding)
|
|
274
474
|
|
|
275
475
|
@pytest.mark.asyncio
|
|
@@ -285,4 +485,5 @@ class TestPerplexityDocumentEmbedderInference:
|
|
|
285
485
|
assert len(embedded_docs) == len(docs)
|
|
286
486
|
for doc in embedded_docs:
|
|
287
487
|
assert isinstance(doc.embedding, list)
|
|
288
|
-
assert len(doc.embedding)
|
|
488
|
+
assert len(doc.embedding) == 1024
|
|
489
|
+
assert all(isinstance(x, float) for x in doc.embedding)
|
{perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/tests/test_perplexity_text_embedder.py
RENAMED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
+
import base64
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
7
8
|
|
|
8
9
|
import httpx
|
|
10
|
+
import numpy as np
|
|
9
11
|
import pytest
|
|
10
12
|
from haystack.utils import Secret
|
|
11
13
|
|
|
@@ -16,15 +18,20 @@ from haystack_integrations.components.embedders.perplexity.text_embedder import
|
|
|
16
18
|
PerplexityTextEmbedder,
|
|
17
19
|
)
|
|
18
20
|
|
|
21
|
+
INT8_EMBEDDING = [-2.0, -1.0, 0.0, 1.0, 2.0]
|
|
22
|
+
INT8_EMBEDDING_PAYLOAD = base64.b64encode(np.array(INT8_EMBEDDING, dtype=np.int8).tobytes()).decode()
|
|
23
|
+
BINARY_EMBEDDING = [1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0]
|
|
24
|
+
BINARY_EMBEDDING_PAYLOAD = base64.b64encode(np.packbits(np.array(BINARY_EMBEDDING, dtype=np.uint8)).tobytes()).decode()
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
|
|
27
|
+
def _make_transport(captured: list[httpx.Request], embedding: str = INT8_EMBEDDING_PAYLOAD) -> httpx.MockTransport:
|
|
21
28
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
22
29
|
captured.append(request)
|
|
23
30
|
return httpx.Response(
|
|
24
31
|
200,
|
|
25
32
|
json={
|
|
26
33
|
"object": "list",
|
|
27
|
-
"data": [{"object": "embedding", "embedding":
|
|
34
|
+
"data": [{"object": "embedding", "embedding": embedding, "index": 0}],
|
|
28
35
|
"model": "pplx-embed-v1-0.6b",
|
|
29
36
|
"usage": {"prompt_tokens": 3, "total_tokens": 3},
|
|
30
37
|
},
|
|
@@ -67,6 +74,7 @@ class TestPerplexityTextEmbedder:
|
|
|
67
74
|
assert embedder.model == "pplx-embed-v1-0.6b"
|
|
68
75
|
assert embedder.prefix == ""
|
|
69
76
|
assert embedder.suffix == ""
|
|
77
|
+
assert embedder.encoding_format == "base64_int8"
|
|
70
78
|
|
|
71
79
|
def test_init_with_parameters(self):
|
|
72
80
|
embedder = PerplexityTextEmbedder(
|
|
@@ -74,6 +82,7 @@ class TestPerplexityTextEmbedder:
|
|
|
74
82
|
model="pplx-embed-v1-4b",
|
|
75
83
|
prefix="START",
|
|
76
84
|
suffix="END",
|
|
85
|
+
encoding_format="base64_binary",
|
|
77
86
|
)
|
|
78
87
|
|
|
79
88
|
assert embedder.api_key == Secret.from_token("test-api-key")
|
|
@@ -81,6 +90,14 @@ class TestPerplexityTextEmbedder:
|
|
|
81
90
|
assert embedder.model == "pplx-embed-v1-4b"
|
|
82
91
|
assert embedder.prefix == "START"
|
|
83
92
|
assert embedder.suffix == "END"
|
|
93
|
+
assert embedder.encoding_format == "base64_binary"
|
|
94
|
+
|
|
95
|
+
def test_init_rejects_float_encoding_format(self):
|
|
96
|
+
with pytest.raises(ValueError):
|
|
97
|
+
PerplexityTextEmbedder(
|
|
98
|
+
api_key=Secret.from_token("test-api-key"),
|
|
99
|
+
encoding_format="float",
|
|
100
|
+
)
|
|
84
101
|
|
|
85
102
|
def test_to_dict(self, monkeypatch):
|
|
86
103
|
monkeypatch.setenv("PERPLEXITY_API_KEY", "test-api-key")
|
|
@@ -97,6 +114,7 @@ class TestPerplexityTextEmbedder:
|
|
|
97
114
|
"api_base_url": "https://api.perplexity.ai/v1",
|
|
98
115
|
"prefix": "",
|
|
99
116
|
"suffix": "",
|
|
117
|
+
"encoding_format": "base64_int8",
|
|
100
118
|
"timeout": None,
|
|
101
119
|
"max_retries": None,
|
|
102
120
|
"http_client_kwargs": None,
|
|
@@ -111,6 +129,7 @@ class TestPerplexityTextEmbedder:
|
|
|
111
129
|
api_base_url="https://custom-api-base-url.com",
|
|
112
130
|
prefix="START",
|
|
113
131
|
suffix="END",
|
|
132
|
+
encoding_format="base64_binary",
|
|
114
133
|
timeout=10.0,
|
|
115
134
|
max_retries=2,
|
|
116
135
|
http_client_kwargs={"proxy": "http://localhost:8080"},
|
|
@@ -126,6 +145,7 @@ class TestPerplexityTextEmbedder:
|
|
|
126
145
|
"api_base_url": "https://custom-api-base-url.com",
|
|
127
146
|
"prefix": "START",
|
|
128
147
|
"suffix": "END",
|
|
148
|
+
"encoding_format": "base64_binary",
|
|
129
149
|
"timeout": 10.0,
|
|
130
150
|
"max_retries": 2,
|
|
131
151
|
"http_client_kwargs": {"proxy": "http://localhost:8080"},
|
|
@@ -142,6 +162,7 @@ class TestPerplexityTextEmbedder:
|
|
|
142
162
|
"api_base_url": "https://api.perplexity.ai/v1",
|
|
143
163
|
"prefix": "",
|
|
144
164
|
"suffix": "",
|
|
165
|
+
"encoding_format": "base64_binary",
|
|
145
166
|
"timeout": None,
|
|
146
167
|
"max_retries": None,
|
|
147
168
|
"http_client_kwargs": None,
|
|
@@ -155,9 +176,11 @@ class TestPerplexityTextEmbedder:
|
|
|
155
176
|
assert component.model == "pplx-embed-v1-0.6b"
|
|
156
177
|
assert component.prefix == ""
|
|
157
178
|
assert component.suffix == ""
|
|
179
|
+
assert component.encoding_format == "base64_binary"
|
|
158
180
|
assert component.timeout is None
|
|
159
181
|
assert component.max_retries is None
|
|
160
|
-
|
|
182
|
+
# the user-provided value; component.http_client_kwargs carries the attribution header
|
|
183
|
+
assert component._http_client_kwargs is None
|
|
161
184
|
|
|
162
185
|
def test_run_sends_attribution_header(self):
|
|
163
186
|
captured: list[httpx.Request] = []
|
|
@@ -168,15 +191,54 @@ class TestPerplexityTextEmbedder:
|
|
|
168
191
|
|
|
169
192
|
result = embedder.run(text="The food was delicious")
|
|
170
193
|
|
|
171
|
-
assert result["embedding"] ==
|
|
194
|
+
assert result["embedding"] == INT8_EMBEDDING
|
|
172
195
|
assert len(captured) == 1
|
|
173
196
|
request = captured[0]
|
|
174
197
|
assert request.headers["Authorization"] == "Bearer test-api-key"
|
|
175
198
|
assert request.headers["X-Pplx-Integration"].startswith("haystack/")
|
|
176
199
|
body = json.loads(request.content)
|
|
177
200
|
assert body["model"] == "pplx-embed-v1-0.6b"
|
|
178
|
-
assert body["input"] == "The food was delicious"
|
|
179
|
-
assert body["encoding_format"] == "
|
|
201
|
+
assert body["input"] == ["The food was delicious"]
|
|
202
|
+
assert body["encoding_format"] == "base64_int8"
|
|
203
|
+
|
|
204
|
+
@pytest.mark.parametrize(
|
|
205
|
+
("encoding_format", "payload", "expected_embedding"),
|
|
206
|
+
[
|
|
207
|
+
("base64_int8", INT8_EMBEDDING_PAYLOAD, INT8_EMBEDDING),
|
|
208
|
+
("base64_binary", BINARY_EMBEDDING_PAYLOAD, BINARY_EMBEDDING),
|
|
209
|
+
],
|
|
210
|
+
)
|
|
211
|
+
def test_run_decodes_base64_embeddings(self, encoding_format, payload, expected_embedding):
|
|
212
|
+
captured: list[httpx.Request] = []
|
|
213
|
+
embedder = PerplexityTextEmbedder(
|
|
214
|
+
api_key=Secret.from_token("test-api-key"),
|
|
215
|
+
encoding_format=encoding_format,
|
|
216
|
+
http_client_kwargs={"transport": _make_transport(captured, embedding=payload)},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
result = embedder.run(text="The food was delicious")
|
|
220
|
+
|
|
221
|
+
embedding = result["embedding"]
|
|
222
|
+
assert embedding == expected_embedding
|
|
223
|
+
assert len(embedding) == len(expected_embedding)
|
|
224
|
+
assert all(isinstance(value, float) for value in embedding)
|
|
225
|
+
body = json.loads(captured[0].content)
|
|
226
|
+
assert body["encoding_format"] == encoding_format
|
|
227
|
+
|
|
228
|
+
@pytest.mark.asyncio
|
|
229
|
+
async def test_run_async_decodes_base64_embeddings(self):
|
|
230
|
+
captured: list[httpx.Request] = []
|
|
231
|
+
embedder = PerplexityTextEmbedder(
|
|
232
|
+
api_key=Secret.from_token("test-api-key"),
|
|
233
|
+
encoding_format="base64_binary",
|
|
234
|
+
http_client_kwargs={"transport": _make_transport(captured, embedding=BINARY_EMBEDDING_PAYLOAD)},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
result = await embedder.run_async(text="The food was delicious")
|
|
238
|
+
|
|
239
|
+
assert result["embedding"] == BINARY_EMBEDDING
|
|
240
|
+
body = json.loads(captured[0].content)
|
|
241
|
+
assert body["encoding_format"] == "base64_binary"
|
|
180
242
|
|
|
181
243
|
def test_run_wrong_input_format(self):
|
|
182
244
|
embedder = PerplexityTextEmbedder(api_key=Secret.from_token("test-api-key"))
|
|
@@ -201,7 +263,7 @@ class TestPerplexityTextEmbedderInference:
|
|
|
201
263
|
result = embedder.run(text=text)
|
|
202
264
|
|
|
203
265
|
assert isinstance(result["embedding"], list)
|
|
204
|
-
assert len(result["embedding"])
|
|
266
|
+
assert len(result["embedding"]) == 1024
|
|
205
267
|
assert all(isinstance(x, float) for x in result["embedding"])
|
|
206
268
|
|
|
207
269
|
@pytest.mark.asyncio
|
|
@@ -211,4 +273,5 @@ class TestPerplexityTextEmbedderInference:
|
|
|
211
273
|
result = await embedder.run_async(text=text)
|
|
212
274
|
|
|
213
275
|
assert isinstance(result["embedding"], list)
|
|
214
|
-
assert len(result["embedding"])
|
|
276
|
+
assert len(result["embedding"]) == 1024
|
|
277
|
+
assert all(isinstance(x, float) for x in result["embedding"])
|
|
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
|