perplexity-haystack 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. perplexity_haystack-0.1.1/CHANGELOG.md +9 -0
  2. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/PKG-INFO +1 -1
  3. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/src/haystack_integrations/components/embedders/perplexity/document_embedder.py +107 -1
  4. perplexity_haystack-0.1.1/src/haystack_integrations/components/embedders/perplexity/embedding_encoding.py +34 -0
  5. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/src/haystack_integrations/components/embedders/perplexity/text_embedder.py +23 -0
  6. perplexity_haystack-0.1.1/tests/test_embedding_encoding.py +24 -0
  7. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/tests/test_perplexity_document_embedder.py +208 -8
  8. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/tests/test_perplexity_text_embedder.py +69 -7
  9. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/.gitignore +0 -0
  10. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/LICENSE.txt +0 -0
  11. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/README.md +0 -0
  12. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/pydoc/config_docusaurus.yml +0 -0
  13. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/pyproject.toml +0 -0
  14. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/src/haystack_integrations/components/embedders/perplexity/__init__.py +0 -0
  15. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/src/haystack_integrations/components/embedders/py.typed +0 -0
  16. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/src/haystack_integrations/components/generators/perplexity/__init__.py +0 -0
  17. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/src/haystack_integrations/components/generators/perplexity/chat/__init__.py +0 -0
  18. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/src/haystack_integrations/components/generators/perplexity/chat/chat_generator.py +0 -0
  19. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/src/haystack_integrations/components/generators/py.typed +0 -0
  20. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/src/haystack_integrations/components/websearch/perplexity/__init__.py +0 -0
  21. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/src/haystack_integrations/components/websearch/perplexity/perplexity_websearch.py +0 -0
  22. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/src/haystack_integrations/components/websearch/py.typed +0 -0
  23. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/tests/__init__.py +0 -0
  24. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/tests/test_perplexity_chat_generator.py +0 -0
  25. {perplexity_haystack-0.1.0 → perplexity_haystack-0.1.1}/tests/test_perplexity_websearch.py +0 -0
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## [integrations/perplexity-v0.1.0] - 2026-05-13
4
+
5
+ ### 🌀 Miscellaneous
6
+
7
+ - Add Perplexity integration package (#3262)
8
+
9
+ <!-- generated by git-cliff -->
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: perplexity-haystack
3
- Version: 0.1.0
3
+ Version: 0.1.1
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,
@@ -129,6 +146,94 @@ class PerplexityDocumentEmbedder(OpenAIDocumentEmbedder):
129
146
  self.timeout = timeout
130
147
  self.max_retries = max_retries
131
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
+ response = self.client.embeddings.create(**args)
172
+ except APIError as exc:
173
+ ids = ", ".join(b[0] for b in batch)
174
+ msg = "Failed embedding of documents {ids} caused by {exc}"
175
+ logger.exception(msg, ids=ids, exc=exc)
176
+ if self.raise_on_failure:
177
+ raise exc
178
+ continue
179
+
180
+ embeddings = self._decode_response_embeddings(response)
181
+ doc_ids_to_embeddings.update(dict(zip((b[0] for b in batch), embeddings, strict=True)))
182
+
183
+ if "model" not in meta:
184
+ meta["model"] = response.model
185
+ if "usage" not in meta:
186
+ meta["usage"] = dict(response.usage)
187
+ else:
188
+ meta["usage"]["prompt_tokens"] += response.usage.prompt_tokens
189
+ meta["usage"]["total_tokens"] += response.usage.total_tokens
190
+
191
+ return doc_ids_to_embeddings, meta
192
+
193
+ async def _embed_batch_async(
194
+ self, texts_to_embed: dict[str, str], batch_size: int
195
+ ) -> tuple[dict[str, list[float]], dict[str, Any]]:
196
+ """
197
+ Embed a list of texts in batches asynchronously.
198
+ """
199
+
200
+ doc_ids_to_embeddings: dict[str, list[float]] = {}
201
+ meta: dict[str, Any] = {}
202
+
203
+ batches: Iterable[tuple[tuple[str, str], ...]] = list(batched(texts_to_embed.items(), batch_size))
204
+ if self.progress_bar:
205
+ batches = async_tqdm(batches, desc="Calculating embeddings")
206
+
207
+ for batch in batches:
208
+ args: dict[str, Any] = {
209
+ "model": self.model,
210
+ "input": [b[1] for b in batch],
211
+ "encoding_format": self.encoding_format,
212
+ }
213
+
214
+ try:
215
+ response = await self.async_client.embeddings.create(**args)
216
+ except APIError as exc:
217
+ ids = ", ".join(b[0] for b in batch)
218
+ msg = "Failed embedding of documents {ids} caused by {exc}"
219
+ logger.exception(msg, ids=ids, exc=exc)
220
+ if self.raise_on_failure:
221
+ raise exc
222
+ continue
223
+
224
+ embeddings = self._decode_response_embeddings(response)
225
+ doc_ids_to_embeddings.update(dict(zip((b[0] for b in batch), embeddings, strict=True)))
226
+
227
+ if "model" not in meta:
228
+ meta["model"] = response.model
229
+ if "usage" not in meta:
230
+ meta["usage"] = dict(response.usage)
231
+ else:
232
+ meta["usage"]["prompt_tokens"] += response.usage.prompt_tokens
233
+ meta["usage"]["total_tokens"] += response.usage.total_tokens
234
+
235
+ return doc_ids_to_embeddings, meta
236
+
132
237
  def to_dict(self) -> dict[str, Any]:
133
238
  """
134
239
  Serializes the component to a dictionary.
@@ -147,6 +252,7 @@ class PerplexityDocumentEmbedder(OpenAIDocumentEmbedder):
147
252
  progress_bar=self.progress_bar,
148
253
  meta_fields_to_embed=self.meta_fields_to_embed,
149
254
  embedding_separator=self.embedding_separator,
255
+ encoding_format=self.encoding_format,
150
256
  timeout=self.timeout,
151
257
  max_retries=self.max_retries,
152
258
  http_client_kwargs=self.http_client_kwargs,
@@ -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,
@@ -108,6 +118,18 @@ class PerplexityTextEmbedder(OpenAITextEmbedder):
108
118
  self.timeout = timeout
109
119
  self.max_retries = max_retries
110
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
+ }
132
+
111
133
  def to_dict(self) -> dict[str, Any]:
112
134
  """
113
135
  Serializes the component to a dictionary.
@@ -122,6 +144,7 @@ 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
150
  http_client_kwargs=self.http_client_kwargs,
@@ -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'."
@@ -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
- def _make_transport(captured: list[httpx.Request]) -> httpx.MockTransport:
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": [0.1, 0.2, 0.3], "index": 0},
30
- {"object": "embedding", "embedding": [0.4, 0.5, 0.6], "index": 1},
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,6 +233,7 @@ 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
  assert component.http_client_kwargs is None
@@ -222,8 +256,8 @@ class TestPerplexityDocumentEmbedder:
222
256
  result = embedder.run(docs)
223
257
 
224
258
  docs_with_embeddings = result["documents"]
225
- assert docs_with_embeddings[0].embedding == [0.1, 0.2, 0.3]
226
- assert docs_with_embeddings[1].embedding == [0.4, 0.5, 0.6]
259
+ assert docs_with_embeddings[0].embedding == INT8_EMBEDDING
260
+ assert docs_with_embeddings[1].embedding == INT8_EMBEDDING
227
261
  assert len(captured) == 1
228
262
  request = captured[0]
229
263
  assert request.headers["Authorization"] == "Bearer test-api-key"
@@ -234,7 +268,172 @@ class TestPerplexityDocumentEmbedder:
234
268
  "I love cheese",
235
269
  "A transformer is a deep learning architecture",
236
270
  ]
237
- assert body["encoding_format"] == "float"
271
+ assert body["encoding_format"] == "base64_int8"
272
+
273
+ @pytest.mark.parametrize(
274
+ ("encoding_format", "payload", "expected_embedding"),
275
+ [
276
+ ("base64_int8", INT8_EMBEDDING_PAYLOAD, INT8_EMBEDDING),
277
+ ("base64_binary", BINARY_EMBEDDING_PAYLOAD, BINARY_EMBEDDING),
278
+ ],
279
+ )
280
+ def test_run_decodes_base64_embeddings(self, encoding_format, payload, expected_embedding):
281
+ captured: list[httpx.Request] = []
282
+ embedder = PerplexityDocumentEmbedder(
283
+ api_key=Secret.from_token("test-api-key"),
284
+ encoding_format=encoding_format,
285
+ progress_bar=False,
286
+ http_client_kwargs={"transport": _make_transport(captured, embeddings=[payload])},
287
+ )
288
+
289
+ result = embedder.run([Document(content="I love cheese")])
290
+
291
+ embedding = result["documents"][0].embedding
292
+ assert embedding == expected_embedding
293
+ assert len(embedding) == len(expected_embedding)
294
+ assert all(isinstance(value, float) for value in embedding)
295
+ body = json.loads(captured[0].content)
296
+ assert body["encoding_format"] == encoding_format
297
+
298
+ def test_run_accumulates_usage_across_batches(self):
299
+ captured: list[httpx.Request] = []
300
+ embedder = PerplexityDocumentEmbedder(
301
+ api_key=Secret.from_token("test-api-key"),
302
+ batch_size=1,
303
+ progress_bar=False,
304
+ http_client_kwargs={"transport": _make_transport(captured)},
305
+ )
306
+
307
+ result = embedder.run(
308
+ [
309
+ Document(content="I love cheese"),
310
+ Document(content="A transformer is a deep learning architecture"),
311
+ ]
312
+ )
313
+
314
+ assert [document.embedding for document in result["documents"]] == [INT8_EMBEDDING, INT8_EMBEDDING]
315
+ assert result["meta"]["usage"] == {"prompt_tokens": 12, "total_tokens": 12}
316
+ assert len(captured) == 2
317
+
318
+ def test_embed_batch_skips_api_error_when_raise_on_failure_is_false(self):
319
+ api_error = _make_api_error()
320
+
321
+ def create(**_kwargs):
322
+ raise api_error
323
+
324
+ embedder = PerplexityDocumentEmbedder(
325
+ api_key=Secret.from_token("test-api-key"),
326
+ progress_bar=False,
327
+ )
328
+ embedder.raise_on_failure = False
329
+ embedder.client = SimpleNamespace(embeddings=SimpleNamespace(create=create))
330
+
331
+ embeddings, meta = embedder._embed_batch({"doc-1": "I love cheese"}, batch_size=1)
332
+
333
+ assert embeddings == {}
334
+ assert meta == {}
335
+
336
+ def test_embed_batch_raises_api_error_when_raise_on_failure_is_true(self):
337
+ api_error = _make_api_error()
338
+
339
+ def create(**_kwargs):
340
+ raise api_error
341
+
342
+ embedder = PerplexityDocumentEmbedder(
343
+ api_key=Secret.from_token("test-api-key"),
344
+ progress_bar=False,
345
+ )
346
+ embedder.raise_on_failure = True
347
+ embedder.client = SimpleNamespace(embeddings=SimpleNamespace(create=create))
348
+
349
+ with pytest.raises(APIError) as exc_info:
350
+ embedder._embed_batch({"doc-1": "I love cheese"}, batch_size=1)
351
+
352
+ assert exc_info.value is api_error
353
+
354
+ @pytest.mark.asyncio
355
+ async def test_run_async_decodes_base64_embeddings(self):
356
+ captured: list[httpx.Request] = []
357
+ embedder = PerplexityDocumentEmbedder(
358
+ api_key=Secret.from_token("test-api-key"),
359
+ progress_bar=False,
360
+ http_client_kwargs={"transport": _make_transport(captured, embeddings=[BINARY_EMBEDDING_PAYLOAD])},
361
+ encoding_format="base64_binary",
362
+ )
363
+
364
+ result = await embedder.run_async([Document(content="I love cheese")])
365
+
366
+ assert result["documents"][0].embedding == BINARY_EMBEDDING
367
+ body = json.loads(captured[0].content)
368
+ assert body["encoding_format"] == "base64_binary"
369
+
370
+ @pytest.mark.asyncio
371
+ async def test_run_async_uses_progress_bar_and_accumulates_usage_across_batches(self, monkeypatch):
372
+ captured: list[httpx.Request] = []
373
+ batches_seen = []
374
+
375
+ def fake_async_tqdm(batches, desc):
376
+ assert desc == "Calculating embeddings"
377
+ batches_seen.extend(batches)
378
+ return batches
379
+
380
+ monkeypatch.setattr(document_embedder_module, "async_tqdm", fake_async_tqdm)
381
+ embedder = PerplexityDocumentEmbedder(
382
+ api_key=Secret.from_token("test-api-key"),
383
+ batch_size=1,
384
+ progress_bar=True,
385
+ http_client_kwargs={"transport": _make_transport(captured)},
386
+ )
387
+
388
+ result = await embedder.run_async(
389
+ [
390
+ Document(content="I love cheese"),
391
+ Document(content="A transformer is a deep learning architecture"),
392
+ ]
393
+ )
394
+
395
+ assert [document.embedding for document in result["documents"]] == [INT8_EMBEDDING, INT8_EMBEDDING]
396
+ assert result["meta"]["usage"] == {"prompt_tokens": 12, "total_tokens": 12}
397
+ assert len(batches_seen) == 2
398
+ assert len(captured) == 2
399
+
400
+ @pytest.mark.asyncio
401
+ async def test_embed_batch_async_skips_api_error_when_raise_on_failure_is_false(self):
402
+ api_error = _make_api_error()
403
+
404
+ async def create(**_kwargs):
405
+ raise api_error
406
+
407
+ embedder = PerplexityDocumentEmbedder(
408
+ api_key=Secret.from_token("test-api-key"),
409
+ progress_bar=False,
410
+ )
411
+ embedder.raise_on_failure = False
412
+ embedder.async_client = SimpleNamespace(embeddings=SimpleNamespace(create=create))
413
+
414
+ embeddings, meta = await embedder._embed_batch_async({"doc-1": "I love cheese"}, batch_size=1)
415
+
416
+ assert embeddings == {}
417
+ assert meta == {}
418
+
419
+ @pytest.mark.asyncio
420
+ async def test_embed_batch_async_raises_api_error_when_raise_on_failure_is_true(self):
421
+ api_error = _make_api_error()
422
+
423
+ async def create(**_kwargs):
424
+ raise api_error
425
+
426
+ embedder = PerplexityDocumentEmbedder(
427
+ api_key=Secret.from_token("test-api-key"),
428
+ progress_bar=False,
429
+ )
430
+ embedder.raise_on_failure = True
431
+ embedder.async_client = SimpleNamespace(embeddings=SimpleNamespace(create=create))
432
+
433
+ with pytest.raises(APIError) as exc_info:
434
+ await embedder._embed_batch_async({"doc-1": "I love cheese"}, batch_size=1)
435
+
436
+ assert exc_info.value is api_error
238
437
 
239
438
  def test_run_wrong_input_format(self):
240
439
  embedder = PerplexityDocumentEmbedder(api_key=Secret.from_token("test-api-key"))
@@ -269,7 +468,7 @@ class TestPerplexityDocumentEmbedderInference:
269
468
  assert len(embedded_docs) == len(docs)
270
469
  for doc in embedded_docs:
271
470
  assert isinstance(doc.embedding, list)
272
- assert len(doc.embedding) > 0
471
+ assert len(doc.embedding) == 1024
273
472
  assert all(isinstance(x, float) for x in doc.embedding)
274
473
 
275
474
  @pytest.mark.asyncio
@@ -285,4 +484,5 @@ class TestPerplexityDocumentEmbedderInference:
285
484
  assert len(embedded_docs) == len(docs)
286
485
  for doc in embedded_docs:
287
486
  assert isinstance(doc.embedding, list)
288
- assert len(doc.embedding) > 0
487
+ assert len(doc.embedding) == 1024
488
+ assert all(isinstance(x, float) for x in doc.embedding)
@@ -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
- def _make_transport(captured: list[httpx.Request]) -> httpx.MockTransport:
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": [0.1, 0.2, 0.3], "index": 0}],
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,6 +176,7 @@ 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
  assert component.http_client_kwargs is None
@@ -168,15 +190,54 @@ class TestPerplexityTextEmbedder:
168
190
 
169
191
  result = embedder.run(text="The food was delicious")
170
192
 
171
- assert result["embedding"] == [0.1, 0.2, 0.3]
193
+ assert result["embedding"] == INT8_EMBEDDING
172
194
  assert len(captured) == 1
173
195
  request = captured[0]
174
196
  assert request.headers["Authorization"] == "Bearer test-api-key"
175
197
  assert request.headers["X-Pplx-Integration"].startswith("haystack/")
176
198
  body = json.loads(request.content)
177
199
  assert body["model"] == "pplx-embed-v1-0.6b"
178
- assert body["input"] == "The food was delicious"
179
- assert body["encoding_format"] == "float"
200
+ assert body["input"] == ["The food was delicious"]
201
+ assert body["encoding_format"] == "base64_int8"
202
+
203
+ @pytest.mark.parametrize(
204
+ ("encoding_format", "payload", "expected_embedding"),
205
+ [
206
+ ("base64_int8", INT8_EMBEDDING_PAYLOAD, INT8_EMBEDDING),
207
+ ("base64_binary", BINARY_EMBEDDING_PAYLOAD, BINARY_EMBEDDING),
208
+ ],
209
+ )
210
+ def test_run_decodes_base64_embeddings(self, encoding_format, payload, expected_embedding):
211
+ captured: list[httpx.Request] = []
212
+ embedder = PerplexityTextEmbedder(
213
+ api_key=Secret.from_token("test-api-key"),
214
+ encoding_format=encoding_format,
215
+ http_client_kwargs={"transport": _make_transport(captured, embedding=payload)},
216
+ )
217
+
218
+ result = embedder.run(text="The food was delicious")
219
+
220
+ embedding = result["embedding"]
221
+ assert embedding == expected_embedding
222
+ assert len(embedding) == len(expected_embedding)
223
+ assert all(isinstance(value, float) for value in embedding)
224
+ body = json.loads(captured[0].content)
225
+ assert body["encoding_format"] == encoding_format
226
+
227
+ @pytest.mark.asyncio
228
+ async def test_run_async_decodes_base64_embeddings(self):
229
+ captured: list[httpx.Request] = []
230
+ embedder = PerplexityTextEmbedder(
231
+ api_key=Secret.from_token("test-api-key"),
232
+ encoding_format="base64_binary",
233
+ http_client_kwargs={"transport": _make_transport(captured, embedding=BINARY_EMBEDDING_PAYLOAD)},
234
+ )
235
+
236
+ result = await embedder.run_async(text="The food was delicious")
237
+
238
+ assert result["embedding"] == BINARY_EMBEDDING
239
+ body = json.loads(captured[0].content)
240
+ assert body["encoding_format"] == "base64_binary"
180
241
 
181
242
  def test_run_wrong_input_format(self):
182
243
  embedder = PerplexityTextEmbedder(api_key=Secret.from_token("test-api-key"))
@@ -201,7 +262,7 @@ class TestPerplexityTextEmbedderInference:
201
262
  result = embedder.run(text=text)
202
263
 
203
264
  assert isinstance(result["embedding"], list)
204
- assert len(result["embedding"]) > 0
265
+ assert len(result["embedding"]) == 1024
205
266
  assert all(isinstance(x, float) for x in result["embedding"])
206
267
 
207
268
  @pytest.mark.asyncio
@@ -211,4 +272,5 @@ class TestPerplexityTextEmbedderInference:
211
272
  result = await embedder.run_async(text=text)
212
273
 
213
274
  assert isinstance(result["embedding"], list)
214
- assert len(result["embedding"]) > 0
275
+ assert len(result["embedding"]) == 1024
276
+ assert all(isinstance(x, float) for x in result["embedding"])