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.
Files changed (25) hide show
  1. perplexity_haystack-0.2.0/CHANGELOG.md +16 -0
  2. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/PKG-INFO +1 -1
  3. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/perplexity/document_embedder.py +113 -5
  4. perplexity_haystack-0.2.0/src/haystack_integrations/components/embedders/perplexity/embedding_encoding.py +34 -0
  5. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/perplexity/text_embedder.py +27 -4
  6. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/generators/perplexity/chat/chat_generator.py +16 -19
  7. perplexity_haystack-0.2.0/tests/test_embedding_encoding.py +24 -0
  8. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/tests/test_perplexity_chat_generator.py +72 -9
  9. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/tests/test_perplexity_document_embedder.py +210 -9
  10. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/tests/test_perplexity_text_embedder.py +71 -8
  11. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/.gitignore +0 -0
  12. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/LICENSE.txt +0 -0
  13. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/README.md +0 -0
  14. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/pydoc/config_docusaurus.yml +0 -0
  15. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/pyproject.toml +0 -0
  16. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/perplexity/__init__.py +0 -0
  17. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/py.typed +0 -0
  18. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/generators/perplexity/__init__.py +0 -0
  19. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/generators/perplexity/chat/__init__.py +0 -0
  20. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/generators/py.typed +0 -0
  21. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/websearch/perplexity/__init__.py +0 -0
  22. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/websearch/perplexity/perplexity_websearch.py +0 -0
  23. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/websearch/py.typed +0 -0
  24. {perplexity_haystack-0.1.0 → perplexity_haystack-0.2.0}/tests/__init__.py +0 -0
  25. {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.1.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 = http_client_kwargs
129
- self.timeout = timeout
130
- self.max_retries = max_retries
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.http_client_kwargs,
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 = http_client_kwargs
108
- self.timeout = timeout
109
- self.max_retries = max_retries
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.http_client_kwargs,
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 _perplexity_headers(extra_headers: dict[str, Any] | None = None) -> dict[str, Any]:
29
- return {
30
- **(extra_headers or {}),
31
- "X-Pplx-Integration": _attribution_header(),
32
- }
33
-
34
-
35
- def _with_default_headers(client: Any, headers: dict[str, Any]) -> Any:
36
- with_options = getattr(client, "with_options", None)
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
- default_headers = _perplexity_headers(extra_headers)
140
- self.client = _with_default_headers(self.client, default_headers)
141
- self.async_client = _with_default_headers(self.async_client, default_headers)
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'."
@@ -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.client.api_key == "test-api-key"
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.client.api_key == "test-api-key"
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
- assert component.http_client_kwargs == {"proxy": "http://localhost:8080"}
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.http_client_kwargs == {"proxy": "http://localhost:8080"}
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 test_default_headers_include_perplexity_attribution(self):
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
- assert component.client.default_headers["X-Pplx-Integration"].startswith("haystack/")
203
- assert component.client.default_headers["test-header"] == "test-value"
204
- assert component.async_client.default_headers["X-Pplx-Integration"].startswith("haystack/")
205
- assert component.async_client.default_headers["test-header"] == "test-value"
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(
@@ -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,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
- assert component.http_client_kwargs is None
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 == [0.1, 0.2, 0.3]
226
- assert docs_with_embeddings[1].embedding == [0.4, 0.5, 0.6]
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"] == "float"
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) > 0
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) > 0
488
+ assert len(doc.embedding) == 1024
489
+ 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,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
- assert component.http_client_kwargs is None
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"] == [0.1, 0.2, 0.3]
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"] == "float"
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"]) > 0
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"]) > 0
276
+ assert len(result["embedding"]) == 1024
277
+ assert all(isinstance(x, float) for x in result["embedding"])