Skip to content

Commit f3da19f

Browse files
authored
enhance: Add Serper.dev search support to SearchToolkit PR3539 (#3617)
1 parent c46730c commit f3da19f

File tree

2 files changed

+94
-68
lines changed

2 files changed

+94
-68
lines changed

camel/toolkits/search_toolkit.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -63,46 +63,51 @@ def __init__(
6363
def search_serper(
6464
self,
6565
query: str,
66-
page: int = 1,
66+
page: int = 10,
6767
location: str = "United States",
6868
) -> Dict[str, Any]:
6969
r"""Use Serper.dev API to perform Google search.
7070
7171
Args:
7272
query (str): The search query.
73-
page (int): The page number of results to retrieve. (default: :obj:`1`)
73+
page (int): The page number of results to retrieve.
74+
(default: :obj:`10`)
7475
location (str): The location for the search results.
7576
(default: :obj:`"United States"`)
7677
7778
Returns:
7879
Dict[str, Any]: The search result dictionary containing 'organic',
7980
'peopleAlsoAsk', etc.
8081
"""
81-
import json
82-
8382
SERPER_API_KEY = os.getenv("SERPER_API_KEY")
8483

8584
url = "https://google.serper.dev/search"
8685

87-
payload = json.dumps(
88-
{
89-
"q": query,
90-
"location": location,
91-
"page": page,
92-
}
93-
)
86+
payload = {
87+
"q": query,
88+
"location": location,
89+
"page": page,
90+
}
9491

9592
headers = {
9693
"X-API-KEY": SERPER_API_KEY,
9794
"Content-Type": "application/json",
9895
}
9996

10097
try:
101-
response = requests.post(url, headers=headers, data=payload)
102-
response.raise_for_status()
98+
response = requests.post(
99+
url, headers=headers, json=payload, timeout=self.timeout
100+
)
101+
if response.status_code != 200:
102+
return {
103+
"error": (
104+
f"Serper API failed with status {response.status_code}: "
105+
f"{response.text}"
106+
)
107+
}
103108
return response.json()
104109
except requests.exceptions.RequestException as e:
105-
raise RuntimeError(f"Error making request to Serper: {e}")
110+
return {"error": f"Serper search failed: {e!s}"}
106111

107112
@dependencies_required("wikipedia")
108113
def search_wiki(self, entity: str) -> str:
@@ -458,7 +463,9 @@ def search_brave(
458463
}
459464
params = {k: v for k, v in params.items() if v is not None}
460465

461-
response = requests.get(url, headers=headers, params=params)
466+
response = requests.get(
467+
url, headers=headers, params=params, timeout=self.timeout
468+
)
462469
try:
463470
response.raise_for_status()
464471
except requests.HTTPError as e:
@@ -628,7 +635,7 @@ def search_google(
628635
# Fetch the results given the URL
629636
try:
630637
# Make the get
631-
result = requests.get(url)
638+
result = requests.get(url, timeout=self.timeout)
632639
data = result.json()
633640

634641
# Get the result items
@@ -834,7 +841,9 @@ def search_bocha(
834841
ensure_ascii=False,
835842
)
836843
try:
837-
response = requests.post(url, headers=headers, data=payload)
844+
response = requests.post(
845+
url, headers=headers, data=payload, timeout=self.timeout
846+
)
838847
if response.status_code != 200:
839848
return {
840849
"error": (
@@ -878,7 +887,9 @@ def search_baidu(
878887
}
879888
params = {"wd": query, "rn": str(number_of_result_pages)}
880889

881-
response = requests.get(url, headers=headers, params=params)
890+
response = requests.get(
891+
url, headers=headers, params=params, timeout=self.timeout
892+
)
882893
response.encoding = "utf-8"
883894

884895
soup = BeautifulSoup(response.text, "html.parser")
@@ -962,8 +973,7 @@ def search_bing(
962973
"Chrome/120.0.0.0 Safari/537.36"
963974
),
964975
}
965-
# Add timeout to prevent hanging
966-
response = requests.get(url, headers=headers, timeout=10)
976+
response = requests.get(url, headers=headers, timeout=self.timeout)
967977

968978
# Check if the request was successful
969979
if response.status_code != 200:
@@ -1248,7 +1258,7 @@ def search_alibaba_tongxiao(
12481258
try:
12491259
# Send GET request with proper typing for params
12501260
response = requests.get(
1251-
base_url, headers=headers, params=params, timeout=10
1261+
base_url, headers=headers, params=params, timeout=self.timeout
12521262
)
12531263

12541264
# Check response status

test/toolkits/test_search_functions.py

Lines changed: 63 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -508,18 +508,19 @@ def test_search_baidu(mock_get, search_toolkit):
508508

509509
# Assertions
510510
assert result == expected_output
511-
mock_get.assert_called_once_with(
512-
"https://www.baidu.com/s",
513-
headers={
514-
"User-Agent": (
515-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
516-
"AppleWebKit/537.36 (KHTML, like Gecko) "
517-
"Chrome/120.0.0.0 Safari/537.36"
518-
),
519-
"Referer": "https://www.baidu.com",
520-
},
521-
params={"wd": "test query", "rn": "10"},
522-
)
511+
mock_get.assert_called_once()
512+
args, kwargs = mock_get.call_args
513+
assert args[0] == "https://www.baidu.com/s"
514+
assert kwargs['headers'] == {
515+
"User-Agent": (
516+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
517+
"AppleWebKit/537.36 (KHTML, like Gecko) "
518+
"Chrome/120.0.0.0 Safari/537.36"
519+
),
520+
"Referer": "https://www.baidu.com",
521+
}
522+
assert kwargs['params'] == {"wd": "test query", "rn": "10"}
523+
assert 'timeout' in kwargs
523524

524525

525526
@patch('requests.get')
@@ -570,17 +571,17 @@ def test_search_bing(mock_get, search_toolkit):
570571

571572
# Assertions
572573
assert result == expected_output
573-
mock_get.assert_called_once_with(
574-
"https://cn.bing.com/search?q=test+query",
575-
headers={
576-
"User-Agent": (
577-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
578-
"AppleWebKit/537.36 (KHTML, like Gecko) "
579-
"Chrome/120.0.0.0 Safari/537.36"
580-
),
581-
},
582-
timeout=10,
583-
)
574+
mock_get.assert_called_once()
575+
args, kwargs = mock_get.call_args
576+
assert args[0] == "https://cn.bing.com/search?q=test+query"
577+
assert kwargs['headers'] == {
578+
"User-Agent": (
579+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
580+
"AppleWebKit/537.36 (KHTML, like Gecko) "
581+
"Chrome/120.0.0.0 Safari/537.36"
582+
),
583+
}
584+
assert 'timeout' in kwargs
584585

585586

586587
class MockSearchResult:
@@ -750,19 +751,19 @@ def test_search_alibaba_tongxiao(mock_get, search_toolkit):
750751
)
751752

752753
# Verify the request was made correctly
753-
mock_get.assert_called_once_with(
754-
"https://cloud-iqs.aliyuncs.com/search/genericSearch",
755-
headers={"X-API-Key": "fake_api_key"},
756-
params={
757-
"query": "test query",
758-
"timeRange": "NoLimit",
759-
"page": 10,
760-
"returnMainText": "false",
761-
"returnMarkdownText": "true",
762-
"enableRerank": "true",
763-
},
764-
timeout=10,
765-
)
754+
mock_get.assert_called_once()
755+
args, kwargs = mock_get.call_args
756+
assert args[0] == "https://cloud-iqs.aliyuncs.com/search/genericSearch"
757+
assert kwargs['headers'] == {"X-API-Key": "fake_api_key"}
758+
assert kwargs['params'] == {
759+
"query": "test query",
760+
"timeRange": "NoLimit",
761+
"page": 10,
762+
"returnMainText": "false",
763+
"returnMarkdownText": "true",
764+
"enableRerank": "true",
765+
}
766+
assert 'timeout' in kwargs
766767

767768
# Check if the result is as expected
768769
assert result == {
@@ -904,8 +905,6 @@ def test_search_metaso_invalid_json(mock_https_connection, search_toolkit):
904905
@patch('requests.post')
905906
def test_search_serper_success(mock_post, search_toolkit):
906907
"""Test successful Serper search."""
907-
import json
908-
909908
mock_response = MagicMock()
910909
mock_response.status_code = 200
911910
mock_response.json.return_value = {
@@ -940,22 +939,39 @@ def test_search_serper_success(mock_post, search_toolkit):
940939
"X-API-KEY": "test_key",
941940
"Content-Type": "application/json",
942941
}
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']) == {
942+
# Verify payload structure - using json= parameter now
943+
assert kwargs['json'] == {
946944
"q": "apple inc",
947945
"location": "United States",
948-
"page": 1,
946+
"page": 10,
949947
}
948+
# Verify timeout is passed
949+
assert 'timeout' in kwargs
950950

951951

952952
@patch('requests.post')
953-
def test_search_serper_error(mock_post, search_toolkit):
954-
"""Test error handling in Serper search."""
953+
def test_search_serper_request_error(mock_post, search_toolkit):
954+
"""Test request error handling in Serper search."""
955955
mock_post.side_effect = requests.exceptions.RequestException("API Error")
956956

957957
with patch.dict(os.environ, {'SERPER_API_KEY': 'test_key'}):
958-
with pytest.raises(RuntimeError) as excinfo:
959-
search_toolkit.search_serper(query="test")
958+
result = search_toolkit.search_serper(query="test")
959+
960+
assert "error" in result
961+
assert "Serper search failed" in result["error"]
962+
960963

961-
assert "Error making request to Serper" in str(excinfo.value)
964+
@patch('requests.post')
965+
def test_search_serper_http_error(mock_post, search_toolkit):
966+
"""Test HTTP error handling in Serper search."""
967+
mock_response = MagicMock()
968+
mock_response.status_code = 401
969+
mock_response.text = '{"error": "Invalid API key"}'
970+
mock_post.return_value = mock_response
971+
972+
with patch.dict(os.environ, {'SERPER_API_KEY': 'invalid_key'}):
973+
result = search_toolkit.search_serper(query="test")
974+
975+
assert "error" in result
976+
assert "Serper API failed with status 401" in result["error"]
977+
assert "Invalid API key" in result["error"]

0 commit comments

Comments
 (0)