Skip to content

Commit ea04eca

Browse files
author
james-pplx
committed
feat(tools): add Perplexity Search tool
Add a new built-in tool, PerplexitySearchTool, that calls the Perplexity Search API (POST https://api.perplexity.ai/search) via httpx.AsyncClient. The tool wraps a single `query` argument for the model and lets developers pin server-side options (max_results, recency, domain filter, etc.) at construction time, mirroring the pattern of DiscoveryEngineSearchTool. Every outgoing request includes the X-Pplx-Integration header set to google-adk/<package-version> for attribution. The API key can be supplied via constructor or PERPLEXITY_API_KEY. - src/google/adk/tools/perplexity_search_tool.py: tool implementation - src/google/adk/tools/__init__.py: lazy export - tests/unittests/tools/test_perplexity_search_tool.py: 12 tests with a mocked httpx transport, including an assertion that the attribution header is sent - contributing/samples/perplexity_search_agent: example agent Docs: https://docs.perplexity.ai/api-reference/search-post Signed-off-by: james-pplx <james-pplx@users.noreply.github.com>
1 parent 2343973 commit ea04eca

5 files changed

Lines changed: 503 additions & 0 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from . import agent
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Sample agent using the Perplexity Search tool.
16+
17+
Set the PERPLEXITY_API_KEY environment variable before running this agent.
18+
See https://docs.perplexity.ai/api-reference/search-post for API details.
19+
"""
20+
21+
from google.adk import Agent
22+
from google.adk.tools.perplexity_search_tool import PerplexitySearchTool
23+
24+
perplexity_search = PerplexitySearchTool(max_results=5)
25+
26+
root_agent = Agent(
27+
model='gemini-2.5-flash',
28+
name='root_agent',
29+
description=(
30+
'an agent whose job it is to answer questions by searching the web'
31+
' via the Perplexity Search API.'
32+
),
33+
instruction=(
34+
'You are an agent whose job is to answer questions by searching the'
35+
' web with the perplexity_search tool. Cite the URLs of the sources'
36+
' you used in your final answer.'
37+
),
38+
tools=[perplexity_search],
39+
)

src/google/adk/tools/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from .load_artifacts_tool import load_artifacts_tool as load_artifacts
3737
from .load_memory_tool import load_memory_tool as load_memory
3838
from .long_running_tool import LongRunningFunctionTool
39+
from .perplexity_search_tool import PerplexitySearchTool
3940
from .preload_memory_tool import preload_memory_tool as preload_memory
4041
from .tool_context import ToolContext
4142
from .transfer_to_agent_tool import transfer_to_agent
@@ -79,6 +80,10 @@
7980
'.long_running_tool',
8081
'LongRunningFunctionTool',
8182
),
83+
'PerplexitySearchTool': (
84+
'.perplexity_search_tool',
85+
'PerplexitySearchTool',
86+
),
8287
'preload_memory': ('.preload_memory_tool', 'preload_memory_tool'),
8388
'ToolContext': ('.tool_context', 'ToolContext'),
8489
'transfer_to_agent': ('.transfer_to_agent_tool', 'transfer_to_agent'),
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import importlib.metadata
18+
import logging
19+
import os
20+
from typing import Any
21+
from typing import Optional
22+
23+
import httpx
24+
25+
from ..version import __version__ as _ADK_VERSION
26+
from .function_tool import FunctionTool
27+
28+
logger = logging.getLogger('google_adk.' + __name__)
29+
30+
_PERPLEXITY_SEARCH_URL = 'https://api.perplexity.ai/search'
31+
_INTEGRATION_SLUG = 'google-adk'
32+
_PACKAGE_NAME = 'google-adk'
33+
_DEFAULT_TIMEOUT_SECONDS = 30.0
34+
35+
36+
def _resolve_package_version() -> str:
37+
"""Returns the installed ADK package version, falling back to the in-tree version."""
38+
try:
39+
return importlib.metadata.version(_PACKAGE_NAME)
40+
except importlib.metadata.PackageNotFoundError:
41+
return _ADK_VERSION
42+
43+
44+
class PerplexitySearchTool(FunctionTool):
45+
"""Tool that performs web search via the Perplexity Search API.
46+
47+
This tool wraps the `POST https://api.perplexity.ai/search` endpoint and
48+
exposes a single `query` argument to the model, while letting the developer
49+
pin server-side options (recency, domain filter, max results, etc.) at
50+
construction time.
51+
52+
See https://docs.perplexity.ai/api-reference/search-post for the request
53+
and response schema.
54+
55+
Example:
56+
57+
```python
58+
from google.adk.agents import LlmAgent
59+
from google.adk.tools.perplexity_search_tool import PerplexitySearchTool
60+
61+
perplexity_search = PerplexitySearchTool()
62+
agent = LlmAgent(
63+
model='gemini-2.5-flash',
64+
name='research_agent',
65+
tools=[perplexity_search],
66+
)
67+
```
68+
"""
69+
70+
def __init__(
71+
self,
72+
api_key: Optional[str] = None,
73+
*,
74+
max_results: Optional[int] = None,
75+
max_tokens_per_page: Optional[int] = None,
76+
country: Optional[str] = None,
77+
search_recency_filter: Optional[str] = None,
78+
search_domain_filter: Optional[list[str]] = None,
79+
search_language_filter: Optional[list[str]] = None,
80+
last_updated_after_filter: Optional[str] = None,
81+
last_updated_before_filter: Optional[str] = None,
82+
search_after_date_filter: Optional[str] = None,
83+
search_before_date_filter: Optional[str] = None,
84+
timeout: float = _DEFAULT_TIMEOUT_SECONDS,
85+
):
86+
"""Initializes the PerplexitySearchTool.
87+
88+
Args:
89+
api_key: The Perplexity API key. If not provided, the value of the
90+
`PERPLEXITY_API_KEY` environment variable is used.
91+
max_results: Maximum number of results to return (1-20). Defaults to the
92+
API default (10) when not set.
93+
max_tokens_per_page: Maximum tokens per page (1-1,000,000). Defaults to
94+
the API default (4096) when not set.
95+
country: Optional ISO 3166-1 alpha-2 country code for localization.
96+
search_recency_filter: Optional recency filter. One of `hour`, `day`,
97+
`week`, `month`, or `year`.
98+
search_domain_filter: Optional list of up to 20 domains to restrict the
99+
search to.
100+
search_language_filter: Optional list of ISO 639-1 language codes.
101+
last_updated_after_filter: Optional `MM/DD/YYYY` lower bound on the
102+
`last_updated` field of results.
103+
last_updated_before_filter: Optional `MM/DD/YYYY` upper bound on the
104+
`last_updated` field of results.
105+
search_after_date_filter: Optional `MM/DD/YYYY` lower bound on the
106+
result publication date.
107+
search_before_date_filter: Optional `MM/DD/YYYY` upper bound on the
108+
result publication date.
109+
timeout: HTTP timeout in seconds for each search request.
110+
111+
Raises:
112+
ValueError: If no API key is supplied and `PERPLEXITY_API_KEY` is not
113+
set in the environment.
114+
"""
115+
super().__init__(self.perplexity_search)
116+
resolved_api_key = api_key or os.environ.get('PERPLEXITY_API_KEY')
117+
if not resolved_api_key:
118+
raise ValueError(
119+
'Perplexity API key is required: pass `api_key` to '
120+
'PerplexitySearchTool or set the PERPLEXITY_API_KEY '
121+
'environment variable.'
122+
)
123+
self._api_key = resolved_api_key
124+
self._max_results = max_results
125+
self._max_tokens_per_page = max_tokens_per_page
126+
self._country = country
127+
self._search_recency_filter = search_recency_filter
128+
self._search_domain_filter = search_domain_filter
129+
self._search_language_filter = search_language_filter
130+
self._last_updated_after_filter = last_updated_after_filter
131+
self._last_updated_before_filter = last_updated_before_filter
132+
self._search_after_date_filter = search_after_date_filter
133+
self._search_before_date_filter = search_before_date_filter
134+
self._timeout = timeout
135+
136+
def _build_headers(self) -> dict[str, str]:
137+
return {
138+
'Authorization': f'Bearer {self._api_key}',
139+
'Content-Type': 'application/json',
140+
'X-Pplx-Integration': (
141+
f'{_INTEGRATION_SLUG}/{_resolve_package_version()}'
142+
),
143+
}
144+
145+
def _build_body(self, query: str) -> dict[str, Any]:
146+
body: dict[str, Any] = {'query': query}
147+
optional_fields: dict[str, Any] = {
148+
'max_results': self._max_results,
149+
'max_tokens_per_page': self._max_tokens_per_page,
150+
'country': self._country,
151+
'search_recency_filter': self._search_recency_filter,
152+
'search_domain_filter': self._search_domain_filter,
153+
'search_language_filter': self._search_language_filter,
154+
'last_updated_after_filter': self._last_updated_after_filter,
155+
'last_updated_before_filter': self._last_updated_before_filter,
156+
'search_after_date_filter': self._search_after_date_filter,
157+
'search_before_date_filter': self._search_before_date_filter,
158+
}
159+
for key, value in optional_fields.items():
160+
if value is not None:
161+
body[key] = value
162+
return body
163+
164+
async def perplexity_search(self, query: str) -> dict[str, Any]:
165+
"""Searches the web via the Perplexity Search API.
166+
167+
Args:
168+
query: The search query.
169+
170+
Returns:
171+
A dictionary with `status` set to either `success` or `error`. On
172+
success, `results` contains a list of result entries with `title`,
173+
`url`, `snippet`, `date`, and `last_updated` fields, along with the
174+
raw `id` and `server_time` returned by the API.
175+
"""
176+
headers = self._build_headers()
177+
body = self._build_body(query)
178+
179+
try:
180+
async with httpx.AsyncClient(timeout=self._timeout) as client:
181+
response = await client.post(
182+
_PERPLEXITY_SEARCH_URL, headers=headers, json=body
183+
)
184+
response.raise_for_status()
185+
payload = response.json()
186+
except httpx.HTTPStatusError as e:
187+
logger.exception('Perplexity Search request failed.')
188+
return {
189+
'status': 'error',
190+
'error_message': (
191+
f'Perplexity Search returned HTTP {e.response.status_code}.'
192+
),
193+
}
194+
except httpx.HTTPError as e:
195+
logger.exception('Perplexity Search request failed.')
196+
return {'status': 'error', 'error_message': str(e)}
197+
198+
return {
199+
'status': 'success',
200+
'results': payload.get('results', []),
201+
'id': payload.get('id'),
202+
'server_time': payload.get('server_time'),
203+
}

0 commit comments

Comments
 (0)