Skip to content

Commit fb8d51e

Browse files
gaivrtfengju0213
andauthored
feat: Add Serper.dev search support to SearchToolkit (#3539)
Co-authored-by: Tao Sun <[email protected]> Co-authored-by: Sun Tao <[email protected]>
1 parent 34207f9 commit fb8d51e

File tree

5 files changed

+223
-4
lines changed

5 files changed

+223
-4
lines changed

camel/toolkits/search_toolkit.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,55 @@ def __init__(
5555
super().__init__(timeout=timeout)
5656
self.exclude_domains = exclude_domains
5757

58+
@api_keys_required(
59+
[
60+
(None, "SERPER_API_KEY"),
61+
]
62+
)
63+
def search_serper(
64+
self,
65+
query: str,
66+
page: int = 1,
67+
location: str = "United States",
68+
) -> Dict[str, Any]:
69+
r"""Use Serper.dev API to perform Google search.
70+
71+
Args:
72+
query (str): The search query.
73+
page (int): The page number of results to retrieve. (default: :obj:`1`)
74+
location (str): The location for the search results.
75+
(default: :obj:`"United States"`)
76+
77+
Returns:
78+
Dict[str, Any]: The search result dictionary containing 'organic',
79+
'peopleAlsoAsk', etc.
80+
"""
81+
import json
82+
83+
SERPER_API_KEY = os.getenv("SERPER_API_KEY")
84+
85+
url = "https://google.serper.dev/search"
86+
87+
payload = json.dumps(
88+
{
89+
"q": query,
90+
"location": location,
91+
"page": page,
92+
}
93+
)
94+
95+
headers = {
96+
"X-API-KEY": SERPER_API_KEY,
97+
"Content-Type": "application/json",
98+
}
99+
100+
try:
101+
response = requests.post(url, headers=headers, data=payload)
102+
response.raise_for_status()
103+
return response.json()
104+
except requests.exceptions.RequestException as e:
105+
raise RuntimeError(f"Error making request to Serper: {e}")
106+
58107
@dependencies_required("wikipedia")
59108
def search_wiki(self, entity: str) -> str:
60109
r"""Search the entity in WikiPedia and return the summary of the
@@ -376,8 +425,6 @@ def search_brave(
376425
Dict[str, Any]: A dictionary representing a search result.
377426
"""
378427

379-
import requests
380-
381428
BRAVE_API_KEY = os.getenv("BRAVE_API_KEY")
382429

383430
url = "https://api.search.brave.com/res/v1/web/search"
@@ -519,8 +566,6 @@ def search_google(
519566
"""
520567
from urllib.parse import quote
521568

522-
import requests
523-
524569
# Validate input parameters
525570
if not isinstance(start_page, int) or start_page < 1:
526571
raise ValueError("start_page must be a positive integer")
@@ -1171,6 +1216,7 @@ def search_alibaba_tongxiao(
11711216
message. Each result contains title, snippet, url and other
11721217
metadata.
11731218
"""
1219+
11741220
TONGXIAO_API_KEY = os.getenv("TONGXIAO_API_KEY")
11751221

11761222
# Validate query length
@@ -1452,6 +1498,7 @@ def get_tools(self) -> List[FunctionTool]:
14521498
representing the functions in the toolkit.
14531499
"""
14541500
return [
1501+
FunctionTool(self.search_serper),
14551502
FunctionTool(self.search_wiki),
14561503
FunctionTool(self.search_linkup),
14571504
FunctionTool(self.search_google),

docs/mintlify/reference/camel.toolkits.search_toolkit.mdx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,31 @@ Initializes the SearchToolkit.
3232
- **timeout** (float): Timeout for API requests in seconds. (default: :obj:`None`)
3333
- **exclude_domains** (Optional[List[str]]): List of domains to exclude from search results. Currently only supported by the `search_google` function. (default: :obj:`None`)
3434

35+
<a id="camel.toolkits.search_toolkit.SearchToolkit.search_serper"></a>
36+
37+
### search_serper
38+
39+
```python
40+
def search_serper(
41+
self,
42+
query: str,
43+
page: int = 1,
44+
location: str = 'United States'
45+
):
46+
```
47+
48+
Use Serper.dev API to perform Google search.
49+
50+
**Parameters:**
51+
52+
- **query** (str): The search query.
53+
- **page** (int): The page number of results to retrieve. (default: :obj:`1`)
54+
- **location** (str): The location for the search results. (default: :obj:`"United States"`)
55+
56+
**Returns:**
57+
58+
Dict[str, Any]: The search result dictionary containing 'organic', 'peopleAlsoAsk', etc.
59+
3560
<a id="camel.toolkits.search_toolkit.SearchToolkit.search_wiki"></a>
3661

3762
### search_wiki

docs/reference/camel.toolkits.search_toolkit.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,31 @@ Initializes the SearchToolkit.
3232
- **timeout** (float): Timeout for API requests in seconds. (default: :obj:`None`)
3333
- **exclude_domains** (Optional[List[str]]): List of domains to exclude from search results. Currently only supported by the `search_google` function. (default: :obj:`None`)
3434

35+
<a id="camel.toolkits.search_toolkit.SearchToolkit.search_serper"></a>
36+
37+
### search_serper
38+
39+
```python
40+
def search_serper(
41+
self,
42+
query: str,
43+
page: int = 1,
44+
location: str = 'United States'
45+
):
46+
```
47+
48+
Use Serper.dev API to perform Google search.
49+
50+
**Parameters:**
51+
52+
- **query** (str): The search query.
53+
- **page** (int): The page number of results to retrieve. (default: :obj:`1`)
54+
- **location** (str): The location for the search results. (default: :obj:`"United States"`)
55+
56+
**Returns:**
57+
58+
Dict[str, Any]: The search result dictionary containing 'organic', 'peopleAlsoAsk', etc.
59+
3560
<a id="camel.toolkits.search_toolkit.SearchToolkit.search_wiki"></a>
3661

3762
### search_wiki

examples/toolkits/search_toolkit.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# limitations under the License.
1313
# ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. =========
1414

15+
import os
16+
1517
from pydantic import BaseModel
1618

1719
from camel.agents import ChatAgent
@@ -419,6 +421,66 @@ class PersonInfo(BaseModel):
419421
""" # noqa: E501
420422

421423

424+
# Example using Serper search
425+
if os.getenv("SERPER_API_KEY"):
426+
serper_response = SearchToolkit().search_serper(
427+
query="Apple Inc",
428+
page=1,
429+
)
430+
print(serper_response)
431+
"""
432+
===============================================================================
433+
{'searchParameters': {'q': 'Apple Inc', 'type': 'search', 'page': 1,
434+
'location': 'United States', 'engine': 'google', 'gl': 'us'},
435+
'organic': [{'title': 'Apple', 'link': 'https://www.apple.com/',
436+
'snippet': 'Discover the innovative world of Apple and shop everything
437+
iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories,
438+
entertainment, ...', 'position': 1}, {'title': 'Apple Inc.', 'link':
439+
'https://en.wikipedia.org/wiki/Apple_Inc.', 'snippet': 'Apple Inc. is
440+
an American multinational technology company headquartered in
441+
Cupertino, California, in Silicon Valley, best known for its consumer
442+
electronics, ...', 'position': 2}, {'title': 'AAPL: Apple Inc Stock
443+
Price Quote - NASDAQ GS', 'link':
444+
'https://www.bloomberg.com/quote/AAPL:US', 'snippet': 'Apple Inc.
445+
designs, manufactures, and markets smartphones, personal computers,
446+
tablets, wearables and accessories, and sells a variety of related
447+
accessories.', 'position': 3}, {'title': 'Apple Inc. | History,
448+
Products, Headquarters, & Facts', 'link':
449+
'https://www.britannica.com/money/Apple-Inc', 'snippet': 'Apple Inc. is
450+
an American multinational technology company that revolutionized the
451+
technology sector through its innovation of computer software, personal
452+
...', 'date': '3 days ago', 'position': 4}, {'title': 'Apple Inc.
453+
(AAPL)', 'link': 'https://finance.yahoo.com/quote/AAPL/', 'snippet':
454+
'Apple Inc. designs, manufactures, and markets smartphones, personal
455+
computers, tablets, wearables, and accessories worldwide.', 'position':
456+
5}, {'title': 'What the heck is going on at Apple?', 'link':
457+
'https://www.cnn.com/2025/12/06/tech/apple-tim-cook-leadership-changes',
458+
'snippet': 'Now the company known for its steadiness is going through a
459+
shakeup at the top, as both Apple and the tech industry at large are at
460+
a crossroads ...', 'date': '1 day ago', 'position': 6}, {'title':
461+
'Apple Inc. (AAPL) Stock Price Today - WSJ', 'link':
462+
'https://www.wsj.com/market-data/quotes/AAPL?gaa_at=eafs&gaa_n=AWEtsqeZ
463+
rQcR92j11_hQeDtRlWcl9tKefoYwgR1oId6oHIJbV4gU_v-Mpi48&gaa_ts=6935bd94&ga
464+
a_sig=3tbJ_7ZRrguPYjDxogTwd2ytCG7b70pNDaNogjgUg14icaShrFItmMWpypVli_jwY
465+
m1WncqFLHFta52UOD1ngQ%3D%3D', 'snippet': 'Key Stock Data · P/E Ratio
466+
(TTM). 37.37(12/05/25) · EPS (TTM). .46 · Market Cap. .12 T · Shares
467+
Outstanding. 14.78 B · Public Float. 14.76 B · Yield. 0.37%( ...',
468+
'position': 7}, {'title': 'Apple', 'link':
469+
'https://www.linkedin.com/company/apple', 'snippet': 'Company size:
470+
10,001+ employees. Headquarters: Cupertino, California. Type: Public
471+
Company. Founded: 1976. Specialties: Innovative Product ...',
472+
'position': 8}, {'title': 'Apple | AAPL Stock Price, Company Overview &
473+
News', 'link': 'https://www.forbes.com/companies/apple/', 'snippet':
474+
'Apple Inc. engages in the design, manufacture, and sale of
475+
smartphones, personal computers, tablets, wearables and accessories,
476+
and other variety of related ...', 'position': 9}, {'title': 'iCloud',
477+
'link': 'https://www.icloud.com/', 'snippet': 'Log in to iCloud to
478+
access your photos, mail, notes, documents and more. Sign in with your
479+
Apple Account or create a new account to start using Apple services
480+
...', 'position': 10}], 'credits': 1}
481+
===============================================================================
482+
"""
483+
422484
serpapi_agent = ChatAgent(
423485
system_message="""You are a helpful assistant that helps users with
424486
their queries""",

test/toolkits/test_search_functions.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,3 +899,63 @@ def test_search_metaso_invalid_json(mock_https_connection, search_toolkit):
899899
# Verify connection was attempted
900900
mock_https_connection.assert_called_once_with("metaso.cn")
901901
mock_conn.request.assert_called_once()
902+
903+
904+
@patch('requests.post')
905+
def test_search_serper_success(mock_post, search_toolkit):
906+
"""Test successful Serper search."""
907+
import json
908+
909+
mock_response = MagicMock()
910+
mock_response.status_code = 200
911+
mock_response.json.return_value = {
912+
"searchParameters": {
913+
"q": "apple inc",
914+
"gl": "us",
915+
"hl": "en",
916+
"num": 10,
917+
"type": "search",
918+
},
919+
"organic": [
920+
{
921+
"title": "Apple",
922+
"link": "https://www.apple.com/",
923+
"snippet": "Discover the innovative world of Apple...",
924+
"position": 1,
925+
}
926+
],
927+
}
928+
mock_post.return_value = mock_response
929+
930+
with patch.dict(os.environ, {'SERPER_API_KEY': 'test_key'}):
931+
result = search_toolkit.search_serper(query="apple inc")
932+
933+
assert result == mock_response.json.return_value
934+
935+
# Verify request
936+
mock_post.assert_called_once()
937+
args, kwargs = mock_post.call_args
938+
assert args[0] == "https://google.serper.dev/search"
939+
assert kwargs['headers'] == {
940+
"X-API-KEY": "test_key",
941+
"Content-Type": "application/json",
942+
}
943+
# Verify payload structure matches exactly what user requested
944+
# Note: requests.post(json=payload) sets data to json dump
945+
assert json.loads(kwargs['data']) == {
946+
"q": "apple inc",
947+
"location": "United States",
948+
"page": 1,
949+
}
950+
951+
952+
@patch('requests.post')
953+
def test_search_serper_error(mock_post, search_toolkit):
954+
"""Test error handling in Serper search."""
955+
mock_post.side_effect = requests.exceptions.RequestException("API Error")
956+
957+
with patch.dict(os.environ, {'SERPER_API_KEY': 'test_key'}):
958+
with pytest.raises(RuntimeError) as excinfo:
959+
search_toolkit.search_serper(query="test")
960+
961+
assert "Error making request to Serper" in str(excinfo.value)

0 commit comments

Comments
 (0)