perplexity-haystack 0.1.1__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.1.1 → perplexity_haystack-0.2.0}/CHANGELOG.md +7 -0
  2. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/PKG-INFO +1 -1
  3. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/perplexity/document_embedder.py +8 -6
  4. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/perplexity/text_embedder.py +4 -4
  5. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/generators/perplexity/chat/chat_generator.py +16 -19
  6. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/tests/test_perplexity_chat_generator.py +72 -9
  7. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/tests/test_perplexity_document_embedder.py +2 -1
  8. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/tests/test_perplexity_text_embedder.py +2 -1
  9. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/.gitignore +0 -0
  10. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/LICENSE.txt +0 -0
  11. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/README.md +0 -0
  12. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/pydoc/config_docusaurus.yml +0 -0
  13. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/pyproject.toml +0 -0
  14. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/perplexity/__init__.py +0 -0
  15. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/perplexity/embedding_encoding.py +0 -0
  16. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/embedders/py.typed +0 -0
  17. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/generators/perplexity/__init__.py +0 -0
  18. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/generators/perplexity/chat/__init__.py +0 -0
  19. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/generators/py.typed +0 -0
  20. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/websearch/perplexity/__init__.py +0 -0
  21. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/websearch/perplexity/perplexity_websearch.py +0 -0
  22. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/src/haystack_integrations/components/websearch/py.typed +0 -0
  23. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/tests/__init__.py +0 -0
  24. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/tests/test_embedding_encoding.py +0 -0
  25. {perplexity_haystack-0.1.1 → perplexity_haystack-0.2.0}/tests/test_perplexity_websearch.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/perplexity-v0.1.1] - 2026-05-27
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - *(perplexity)* Decode embedding API responses (#3344)
8
+
9
+
3
10
  ## [integrations/perplexity-v0.1.0] - 2026-05-13
4
11
 
5
12
  ### 🌀 Miscellaneous
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: perplexity-haystack
3
- Version: 0.1.1
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
@@ -142,9 +142,9 @@ class PerplexityDocumentEmbedder(OpenAIDocumentEmbedder):
142
142
  max_retries=max_retries,
143
143
  http_client_kwargs=_http_client_kwargs_with_attribution(http_client_kwargs),
144
144
  )
145
- self.http_client_kwargs = http_client_kwargs
146
- self.timeout = timeout
147
- 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
148
 
149
149
  def _decode_response_embeddings(self, response: CreateEmbeddingResponse) -> list[list[float]]:
150
150
  return [_decode_embedding(str(el.embedding), self.encoding_format) for el in response.data]
@@ -168,7 +168,8 @@ class PerplexityDocumentEmbedder(OpenAIDocumentEmbedder):
168
168
  }
169
169
 
170
170
  try:
171
- response = self.client.embeddings.create(**args)
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]
172
173
  except APIError as exc:
173
174
  ids = ", ".join(b[0] for b in batch)
174
175
  msg = "Failed embedding of documents {ids} caused by {exc}"
@@ -212,7 +213,8 @@ class PerplexityDocumentEmbedder(OpenAIDocumentEmbedder):
212
213
  }
213
214
 
214
215
  try:
215
- response = await self.async_client.embeddings.create(**args)
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]
216
218
  except APIError as exc:
217
219
  ids = ", ".join(b[0] for b in batch)
218
220
  msg = "Failed embedding of documents {ids} caused by {exc}"
@@ -255,7 +257,7 @@ class PerplexityDocumentEmbedder(OpenAIDocumentEmbedder):
255
257
  encoding_format=self.encoding_format,
256
258
  timeout=self.timeout,
257
259
  max_retries=self.max_retries,
258
- http_client_kwargs=self.http_client_kwargs,
260
+ http_client_kwargs=self._http_client_kwargs,
259
261
  )
260
262
 
261
263
  @classmethod
@@ -114,9 +114,9 @@ class PerplexityTextEmbedder(OpenAITextEmbedder):
114
114
  max_retries=max_retries,
115
115
  http_client_kwargs=_http_client_kwargs_with_attribution(http_client_kwargs),
116
116
  )
117
- self.http_client_kwargs = http_client_kwargs
118
- self.timeout = timeout
119
- 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
120
 
121
121
  def _prepare_input(self, text: str) -> dict[str, Any]:
122
122
  kwargs = OpenAITextEmbedder._prepare_input(self, text=text)
@@ -147,7 +147,7 @@ class PerplexityTextEmbedder(OpenAITextEmbedder):
147
147
  encoding_format=self.encoding_format,
148
148
  timeout=self.timeout,
149
149
  max_retries=self.max_retries,
150
- http_client_kwargs=self.http_client_kwargs,
150
+ http_client_kwargs=self._http_client_kwargs,
151
151
  )
152
152
 
153
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
@@ -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(
@@ -236,7 +236,8 @@ class TestPerplexityDocumentEmbedder:
236
236
  assert component.encoding_format == "base64_binary"
237
237
  assert component.timeout is None
238
238
  assert component.max_retries is None
239
- 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
240
241
 
241
242
  def test_run_sends_attribution_header(self):
242
243
  captured: list[httpx.Request] = []
@@ -179,7 +179,8 @@ class TestPerplexityTextEmbedder:
179
179
  assert component.encoding_format == "base64_binary"
180
180
  assert component.timeout is None
181
181
  assert component.max_retries is None
182
- 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
183
184
 
184
185
  def test_run_sends_attribution_header(self):
185
186
  captured: list[httpx.Request] = []