Skip to content

Commit 7a5374d

Browse files
jliounisPSI Bot
authored andcommitted
Switch PerplexityChatGenerator to Agent API (v1/agent, Responses-compatible, default openai/gpt-5.4)
1 parent 2147c5e commit 7a5374d

3 files changed

Lines changed: 202 additions & 308 deletions

File tree

integrations/perplexity/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ links = result["links"]
3333

3434
See the [Perplexity Search API reference](https://docs.perplexity.ai/api-reference/search-post) for the full list of supported parameters.
3535

36+
Use `PerplexityChatGenerator` for chat generation through the Perplexity Agent API:
37+
38+
```python
39+
from haystack.dataclasses import ChatMessage
40+
from haystack_integrations.components.generators.perplexity import PerplexityChatGenerator
41+
42+
chat_generator = PerplexityChatGenerator(model="openai/gpt-5.4")
43+
response = chat_generator.run([ChatMessage.from_user("What is Haystack by deepset?")])
44+
print(response["replies"][0].text)
45+
```
46+
47+
See the [Perplexity Agent API quickstart](https://docs.perplexity.ai/docs/agent-api/quickstart) for supported parameters.
48+
3649
## Contributing
3750

3851
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).

integrations/perplexity/src/haystack_integrations/components/generators/perplexity/chat/chat_generator.py

Lines changed: 98 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,21 @@
33
# SPDX-License-Identifier: Apache-2.0
44

55
import importlib.metadata
6-
from typing import Any
7-
8-
from haystack import component, default_to_dict, logging
9-
from haystack.components.generators.chat import OpenAIChatGenerator
10-
from haystack.dataclasses import ChatMessage, StreamingCallbackT
11-
from haystack.tools import (
12-
ToolsType,
13-
_check_duplicate_tool_names,
14-
flatten_tools_or_toolsets,
15-
serialize_tools_or_toolset,
16-
)
17-
from haystack.utils import serialize_callable
18-
from haystack.utils.auth import Secret
6+
from typing import Any, ClassVar
197

20-
logger = logging.getLogger(__name__)
8+
from haystack import component, default_from_dict
9+
from haystack.components.generators.chat import OpenAIResponsesChatGenerator
10+
from haystack.dataclasses import StreamingCallbackT
11+
from haystack.tools import ToolsType, deserialize_tools_or_toolset_inplace
12+
from haystack.utils import deserialize_callable
13+
from haystack.utils.auth import Secret
2114

2215
_INTEGRATION_SLUG = "haystack"
2316
_PACKAGE_NAME = "perplexity-haystack"
17+
_PERPLEXITY_COMPONENT_PATH = "haystack_integrations.components.generators.perplexity.PerplexityChatGenerator"
18+
_PERPLEXITY_INTERNAL_COMPONENT_PATH = (
19+
"haystack_integrations.components.generators.perplexity.chat.chat_generator.PerplexityChatGenerator"
20+
)
2421

2522

2623
def _attribution_header() -> str:
@@ -31,31 +28,38 @@ def _attribution_header() -> str:
3128
return f"{_INTEGRATION_SLUG}/{version}"
3229

3330

34-
@component
35-
class PerplexityChatGenerator(OpenAIChatGenerator):
36-
"""
37-
Enables text generation using the Perplexity Agent API.
31+
def _perplexity_headers(extra_headers: dict[str, Any] | None = None) -> dict[str, Any]:
32+
return {
33+
**(extra_headers or {}),
34+
"X-Pplx-Integration": _attribution_header(),
35+
}
36+
37+
38+
def _with_default_headers(client: Any, headers: dict[str, Any]) -> Any:
39+
with_options = getattr(client, "with_options", None)
40+
if with_options is not None:
41+
return with_options(default_headers=headers)
3842

39-
For supported models, see [Perplexity docs](https://docs.perplexity.ai/).
43+
client._custom_headers = {**getattr(client, "_custom_headers", {}), **headers}
44+
return client
4045

41-
Users can pass any text generation parameters valid for the Perplexity chat completion API
42-
directly to this component using the `generation_kwargs` parameter in `__init__` or the `generation_kwargs`
43-
parameter in `run` method.
4446

45-
Key Features and Compatibility:
46-
- **Primary Compatibility**: Designed to work seamlessly with the Perplexity chat completion endpoint.
47-
- **Streaming Support**: Supports streaming responses from the Perplexity chat completion endpoint.
48-
- **Customizability**: Supports all parameters supported by the Perplexity chat completion endpoint.
47+
@component
48+
class PerplexityChatGenerator(OpenAIResponsesChatGenerator):
49+
"""
50+
Completes chats using Perplexity models.
4951
50-
This component uses the ChatMessage format for structuring both input and output,
51-
ensuring coherent and contextually relevant responses in chat-based text generation scenarios.
52-
Details on the ChatMessage format can be found in the
53-
[Haystack docs](https://docs.haystack.deepset.ai/docs/chatmessage)
52+
Powered by the Perplexity Agent API (`POST /v1/agent`, OpenAI Responses-compatible).
53+
See the [Perplexity Agent API quickstart](https://docs.perplexity.ai/docs/agent-api/quickstart)
54+
for details.
5455
55-
Usage example:
56+
It uses the [ChatMessage](https://docs.haystack.deepset.ai/docs/chatmessage) format in input and output.
57+
You can customize generation by passing Perplexity Agent API parameters through `generation_kwargs`.
58+
59+
### Usage example
5660
```python
57-
from haystack_integrations.components.generators.perplexity import PerplexityChatGenerator
5861
from haystack.dataclasses import ChatMessage
62+
from haystack_integrations.components.generators.perplexity import PerplexityChatGenerator
5963
6064
messages = [ChatMessage.from_user("What's Natural Language Processing?")]
6165
@@ -65,62 +69,79 @@ class PerplexityChatGenerator(OpenAIChatGenerator):
6569
```
6670
"""
6771

72+
SUPPORTED_MODELS: ClassVar[list[str]] = [
73+
"openai/gpt-5.5",
74+
"openai/gpt-5.4",
75+
"openai/gpt-4o",
76+
"anthropic/claude-sonnet-4-6",
77+
"xai/grok-4-1",
78+
"google/gemini-3-flash-preview",
79+
]
80+
"""A non-exhaustive list of Agent API models supported by this component.
81+
See https://docs.perplexity.ai/docs/agent-api/models for the full and current list."""
82+
6883
def __init__(
6984
self,
7085
*,
7186
api_key: Secret = Secret.from_env_var("PERPLEXITY_API_KEY"),
72-
model: str = "sonar-pro",
87+
model: str = "openai/gpt-5.4",
88+
api_base_url: str | None = "https://api.perplexity.ai/v1",
7389
streaming_callback: StreamingCallbackT | None = None,
74-
api_base_url: str | None = "https://api.perplexity.ai",
90+
organization: str | None = None,
7591
generation_kwargs: dict[str, Any] | None = None,
76-
tools: ToolsType | None = None,
92+
tools: ToolsType | list[dict[str, Any]] | None = None,
93+
tools_strict: bool = False,
7794
timeout: float | None = None,
7895
extra_headers: dict[str, Any] | None = None,
7996
max_retries: int | None = None,
8097
http_client_kwargs: dict[str, Any] | None = None,
8198
) -> None:
8299
"""
83-
Creates an instance of PerplexityChatGenerator.
100+
Initialize the PerplexityChatGenerator component.
84101
85102
:param api_key:
86103
The Perplexity API key.
87104
:param model:
88-
The name of the Perplexity chat completion model to use.
89-
:param streaming_callback:
90-
A callback function that is called when a new token is received from the stream.
91-
The callback function accepts StreamingChunk as an argument.
105+
The Perplexity Agent API model to use.
92106
:param api_base_url:
93107
The Perplexity API base URL.
108+
:param streaming_callback:
109+
A callback function called when a new token is received from the stream.
110+
:param organization:
111+
Organization ID forwarded to the OpenAI-compatible client.
94112
:param generation_kwargs:
95-
Other parameters to use for the model. These parameters are all sent directly to
96-
the Perplexity endpoint.
113+
Additional parameters sent directly to the Perplexity Agent API.
97114
:param tools:
98-
A list of tools or a Toolset for which the model can prepare calls. This parameter can accept either a
99-
list of `Tool` objects or a `Toolset` instance.
115+
A list of Haystack tools, a Toolset, or OpenAI-compatible tool definitions.
116+
:param tools_strict:
117+
Whether to enable strict schema adherence for Haystack tool calls.
100118
:param timeout:
101-
The timeout for the Perplexity API call.
119+
Timeout for Perplexity API calls.
102120
:param extra_headers:
103121
Additional HTTP headers to include in requests to the Perplexity API.
104122
:param max_retries:
105123
Maximum number of retries to contact Perplexity after an internal error.
106-
If not set, it defaults to either the `OPENAI_MAX_RETRIES` environment variable, or set to 5.
107124
:param http_client_kwargs:
108-
A dictionary of keyword arguments to configure a custom `httpx.Client`or `httpx.AsyncClient`.
109-
For more information, see the [HTTPX documentation](https://www.python-httpx.org/api/#client).
110-
125+
A dictionary of keyword arguments to configure a custom `httpx.Client` or `httpx.AsyncClient`.
111126
"""
127+
self.extra_headers = extra_headers
112128
super(PerplexityChatGenerator, self).__init__( # noqa: UP008
113129
api_key=api_key,
114130
model=model,
115131
streaming_callback=streaming_callback,
116132
api_base_url=api_base_url,
133+
organization=organization,
117134
generation_kwargs=generation_kwargs,
118-
tools=tools,
119135
timeout=timeout,
120136
max_retries=max_retries,
137+
tools=tools,
138+
tools_strict=tools_strict,
121139
http_client_kwargs=http_client_kwargs,
122140
)
123-
self.extra_headers = extra_headers
141+
142+
default_headers = _perplexity_headers(extra_headers)
143+
self.client = _with_default_headers(self.client, default_headers)
144+
self.async_client = _with_default_headers(self.async_client, default_headers)
124145

125146
def to_dict(self) -> dict[str, Any]:
126147
"""
@@ -129,90 +150,30 @@ def to_dict(self) -> dict[str, Any]:
129150
:returns:
130151
The serialized component as a dictionary.
131152
"""
132-
callback_name = serialize_callable(self.streaming_callback) if self.streaming_callback else None
133-
134-
return default_to_dict(
135-
self,
136-
model=self.model,
137-
streaming_callback=callback_name,
138-
api_base_url=self.api_base_url,
139-
generation_kwargs=self.generation_kwargs,
140-
api_key=self.api_key.to_dict(),
141-
tools=serialize_tools_or_toolset(self.tools),
142-
extra_headers=self.extra_headers,
143-
timeout=self.timeout,
144-
max_retries=self.max_retries,
145-
http_client_kwargs=self.http_client_kwargs,
146-
)
153+
data = super(PerplexityChatGenerator, self).to_dict() # noqa: UP008
154+
data["type"] = _PERPLEXITY_COMPONENT_PATH
155+
data["init_parameters"]["extra_headers"] = self.extra_headers
156+
return data
147157

148-
def _prepare_api_call(
149-
self,
150-
*,
151-
messages: list[ChatMessage],
152-
streaming_callback: StreamingCallbackT | None = None,
153-
generation_kwargs: dict[str, Any] | None = None,
154-
tools: ToolsType | None = None,
155-
tools_strict: bool | None = None,
156-
) -> dict[str, Any]:
157-
# update generation kwargs by merging with the generation kwargs passed to the run method
158-
generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})}
159-
extra_headers = {
160-
**(self.extra_headers or {}),
161-
"X-Pplx-Integration": _attribution_header(),
162-
}
163-
164-
is_streaming = streaming_callback is not None
165-
num_responses = generation_kwargs.pop("n", 1)
166-
167-
if is_streaming and num_responses > 1:
168-
msg = "Cannot stream multiple responses, please set n=1."
169-
raise ValueError(msg)
170-
response_format = generation_kwargs.pop("response_format", None)
171-
172-
# adapt ChatMessage(s) to the format expected by the OpenAI API
173-
openai_formatted_messages = [message.to_openai_dict_format() for message in messages]
174-
175-
flattened_tools = flatten_tools_or_toolsets(tools or self.tools)
176-
tools_strict = tools_strict if tools_strict is not None else self.tools_strict
177-
_check_duplicate_tool_names(flattened_tools)
178-
179-
openai_tools = {}
180-
if flattened_tools:
181-
tool_definitions = []
182-
for t in flattened_tools:
183-
function_spec = {**t.tool_spec}
184-
if tools_strict:
185-
function_spec["strict"] = True
186-
function_spec["parameters"]["additionalProperties"] = False
187-
tool_definitions.append({"type": "function", "function": function_spec})
188-
openai_tools = {"tools": tool_definitions}
189-
190-
base_args = {
191-
"model": self.model,
192-
"messages": openai_formatted_messages,
193-
"n": num_responses,
194-
**openai_tools,
195-
"extra_headers": {**extra_headers},
196-
"extra_body": {**generation_kwargs},
197-
}
198-
199-
if response_format and not is_streaming:
200-
# for structured outputs without streaming, we use openai's parse endpoint
201-
# Note: `stream` cannot be passed to chat.completions.parse
202-
# we pass a key `openai_endpoint` as a hint to the run method to use the parse endpoint
203-
# this key will be removed before the API call is made
204-
return {
205-
**base_args,
206-
"response_format": response_format,
207-
"openai_endpoint": "parse",
208-
}
209-
210-
# for structured outputs with streaming, we use openai's create endpoint
211-
# we pass a key `openai_endpoint` as a hint to the run method to use the create endpoint
212-
# this key will be removed before the API call is made
213-
final_args = {**base_args, "stream": is_streaming, "openai_endpoint": "create"}
214-
215-
# We only set the response_format parameter if it's not None since None is not a valid value in the API.
216-
if response_format:
217-
final_args["response_format"] = response_format
218-
return final_args
158+
@classmethod
159+
def from_dict(cls, data: dict[str, Any]) -> "PerplexityChatGenerator":
160+
"""
161+
Deserialize this component from a dictionary.
162+
163+
:param data: The dictionary representation of this component.
164+
:returns:
165+
The deserialized component instance.
166+
"""
167+
tools = data["init_parameters"].get("tools")
168+
if tools and (
169+
(isinstance(tools, dict) and tools.get("type") == "haystack.tools.toolset.Toolset")
170+
or (isinstance(tools, list) and tools[0].get("type") == "haystack.tools.tool.Tool")
171+
):
172+
deserialize_tools_or_toolset_inplace(data["init_parameters"], key="tools")
173+
174+
serialized_callback_handler = data.get("init_parameters", {}).get("streaming_callback")
175+
if serialized_callback_handler:
176+
data["init_parameters"]["streaming_callback"] = deserialize_callable(serialized_callback_handler)
177+
178+
data["type"] = _PERPLEXITY_INTERNAL_COMPONENT_PATH
179+
return default_from_dict(cls, data)

0 commit comments

Comments
 (0)