Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions camel/toolkits/search_toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,55 @@ def __init__(
super().__init__(timeout=timeout)
self.exclude_domains = exclude_domains

@api_keys_required(
[
(None, "SERPER_API_KEY"),
]
)
def search_serper(
self,
query: str,
page: int = 1,
location: str = "United States",
) -> Dict[str, Any]:
r"""Use Serper.dev API to perform Google search.

Args:
query (str): The search query.
page (int): The page number of results to retrieve. (default: :obj:`1`)
location (str): The location for the search results.
(default: :obj:`"United States"`)

Returns:
Dict[str, Any]: The search result dictionary containing 'organic',
'peopleAlsoAsk', etc.
"""
import json

SERPER_API_KEY = os.getenv("SERPER_API_KEY")

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

payload = json.dumps(
{
"q": query,
"location": location,
"page": page,
}
)

headers = {
"X-API-KEY": SERPER_API_KEY,
"Content-Type": "application/json",
}

try:
response = requests.post(url, headers=headers, data=payload)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Error making request to Serper: {e}")

@dependencies_required("wikipedia")
def search_wiki(self, entity: str) -> str:
r"""Search the entity in WikiPedia and return the summary of the
Expand Down Expand Up @@ -376,8 +425,6 @@ def search_brave(
Dict[str, Any]: A dictionary representing a search result.
"""

import requests

BRAVE_API_KEY = os.getenv("BRAVE_API_KEY")

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

import requests

# Validate input parameters
if not isinstance(start_page, int) or start_page < 1:
raise ValueError("start_page must be a positive integer")
Expand Down Expand Up @@ -1171,6 +1216,7 @@ def search_alibaba_tongxiao(
message. Each result contains title, snippet, url and other
metadata.
"""

TONGXIAO_API_KEY = os.getenv("TONGXIAO_API_KEY")

# Validate query length
Expand Down Expand Up @@ -1452,6 +1498,7 @@ def get_tools(self) -> List[FunctionTool]:
representing the functions in the toolkit.
"""
return [
FunctionTool(self.search_serper),
FunctionTool(self.search_wiki),
FunctionTool(self.search_linkup),
FunctionTool(self.search_google),
Expand Down
25 changes: 25 additions & 0 deletions docs/mintlify/reference/camel.toolkits.search_toolkit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,31 @@ Initializes the SearchToolkit.
- **timeout** (float): Timeout for API requests in seconds. (default: :obj:`None`)
- **exclude_domains** (Optional[List[str]]): List of domains to exclude from search results. Currently only supported by the `search_google` function. (default: :obj:`None`)

<a id="camel.toolkits.search_toolkit.SearchToolkit.search_serper"></a>

### search_serper

```python
def search_serper(
self,
query: str,
page: int = 1,
location: str = 'United States'
):
```

Use Serper.dev API to perform Google search.

**Parameters:**

- **query** (str): The search query.
- **page** (int): The page number of results to retrieve. (default: :obj:`1`)
- **location** (str): The location for the search results. (default: :obj:`"United States"`)

**Returns:**

Dict[str, Any]: The search result dictionary containing 'organic', 'peopleAlsoAsk', etc.

<a id="camel.toolkits.search_toolkit.SearchToolkit.search_wiki"></a>

### search_wiki
Expand Down
25 changes: 25 additions & 0 deletions docs/reference/camel.toolkits.search_toolkit.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,31 @@ Initializes the SearchToolkit.
- **timeout** (float): Timeout for API requests in seconds. (default: :obj:`None`)
- **exclude_domains** (Optional[List[str]]): List of domains to exclude from search results. Currently only supported by the `search_google` function. (default: :obj:`None`)

<a id="camel.toolkits.search_toolkit.SearchToolkit.search_serper"></a>

### search_serper

```python
def search_serper(
self,
query: str,
page: int = 1,
location: str = 'United States'
):
```

Use Serper.dev API to perform Google search.

**Parameters:**

- **query** (str): The search query.
- **page** (int): The page number of results to retrieve. (default: :obj:`1`)
- **location** (str): The location for the search results. (default: :obj:`"United States"`)

**Returns:**

Dict[str, Any]: The search result dictionary containing 'organic', 'peopleAlsoAsk', etc.

<a id="camel.toolkits.search_toolkit.SearchToolkit.search_wiki"></a>

### search_wiki
Expand Down
62 changes: 62 additions & 0 deletions examples/toolkits/search_toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# limitations under the License.
# ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. =========

import os

from pydantic import BaseModel

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


# Example using Serper search
if os.getenv("SERPER_API_KEY"):
serper_response = SearchToolkit().search_serper(
query="Apple Inc",
page=1,
)
print(serper_response)
"""
===============================================================================
{'searchParameters': {'q': 'Apple Inc', 'type': 'search', 'page': 1,
'location': 'United States', 'engine': 'google', 'gl': 'us'},
'organic': [{'title': 'Apple', 'link': 'https://www.apple.com/',
'snippet': 'Discover the innovative world of Apple and shop everything
iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories,
entertainment, ...', 'position': 1}, {'title': 'Apple Inc.', 'link':
'https://en.wikipedia.org/wiki/Apple_Inc.', 'snippet': 'Apple Inc. is
an American multinational technology company headquartered in
Cupertino, California, in Silicon Valley, best known for its consumer
electronics, ...', 'position': 2}, {'title': 'AAPL: Apple Inc Stock
Price Quote - NASDAQ GS', 'link':
'https://www.bloomberg.com/quote/AAPL:US', 'snippet': 'Apple Inc.
designs, manufactures, and markets smartphones, personal computers,
tablets, wearables and accessories, and sells a variety of related
accessories.', 'position': 3}, {'title': 'Apple Inc. | History,
Products, Headquarters, & Facts', 'link':
'https://www.britannica.com/money/Apple-Inc', 'snippet': 'Apple Inc. is
an American multinational technology company that revolutionized the
technology sector through its innovation of computer software, personal
...', 'date': '3 days ago', 'position': 4}, {'title': 'Apple Inc.
(AAPL)', 'link': 'https://finance.yahoo.com/quote/AAPL/', 'snippet':
'Apple Inc. designs, manufactures, and markets smartphones, personal
computers, tablets, wearables, and accessories worldwide.', 'position':
5}, {'title': 'What the heck is going on at Apple?', 'link':
'https://www.cnn.com/2025/12/06/tech/apple-tim-cook-leadership-changes',
'snippet': 'Now the company known for its steadiness is going through a
shakeup at the top, as both Apple and the tech industry at large are at
a crossroads ...', 'date': '1 day ago', 'position': 6}, {'title':
'Apple Inc. (AAPL) Stock Price Today - WSJ', 'link':
'https://www.wsj.com/market-data/quotes/AAPL?gaa_at=eafs&gaa_n=AWEtsqeZ
rQcR92j11_hQeDtRlWcl9tKefoYwgR1oId6oHIJbV4gU_v-Mpi48&gaa_ts=6935bd94&ga
a_sig=3tbJ_7ZRrguPYjDxogTwd2ytCG7b70pNDaNogjgUg14icaShrFItmMWpypVli_jwY
m1WncqFLHFta52UOD1ngQ%3D%3D', 'snippet': 'Key Stock Data · P/E Ratio
(TTM). 37.37(12/05/25) · EPS (TTM). .46 · Market Cap. .12 T · Shares
Outstanding. 14.78 B · Public Float. 14.76 B · Yield. 0.37%( ...',
'position': 7}, {'title': 'Apple', 'link':
'https://www.linkedin.com/company/apple', 'snippet': 'Company size:
10,001+ employees. Headquarters: Cupertino, California. Type: Public
Company. Founded: 1976. Specialties: Innovative Product ...',
'position': 8}, {'title': 'Apple | AAPL Stock Price, Company Overview &
News', 'link': 'https://www.forbes.com/companies/apple/', 'snippet':
'Apple Inc. engages in the design, manufacture, and sale of
smartphones, personal computers, tablets, wearables and accessories,
and other variety of related ...', 'position': 9}, {'title': 'iCloud',
'link': 'https://www.icloud.com/', 'snippet': 'Log in to iCloud to
access your photos, mail, notes, documents and more. Sign in with your
Apple Account or create a new account to start using Apple services
...', 'position': 10}], 'credits': 1}
===============================================================================
"""

serpapi_agent = ChatAgent(
system_message="""You are a helpful assistant that helps users with
their queries""",
Expand Down
60 changes: 60 additions & 0 deletions test/toolkits/test_search_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,3 +899,63 @@ def test_search_metaso_invalid_json(mock_https_connection, search_toolkit):
# Verify connection was attempted
mock_https_connection.assert_called_once_with("metaso.cn")
mock_conn.request.assert_called_once()


@patch('requests.post')
def test_search_serper_success(mock_post, search_toolkit):
"""Test successful Serper search."""
import json

mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"searchParameters": {
"q": "apple inc",
"gl": "us",
"hl": "en",
"num": 10,
"type": "search",
},
"organic": [
{
"title": "Apple",
"link": "https://www.apple.com/",
"snippet": "Discover the innovative world of Apple...",
"position": 1,
}
],
}
mock_post.return_value = mock_response

with patch.dict(os.environ, {'SERPER_API_KEY': 'test_key'}):
result = search_toolkit.search_serper(query="apple inc")

assert result == mock_response.json.return_value

# Verify request
mock_post.assert_called_once()
args, kwargs = mock_post.call_args
assert args[0] == "https://google.serper.dev/search"
assert kwargs['headers'] == {
"X-API-KEY": "test_key",
"Content-Type": "application/json",
}
# Verify payload structure matches exactly what user requested
# Note: requests.post(json=payload) sets data to json dump
assert json.loads(kwargs['data']) == {
"q": "apple inc",
"location": "United States",
"page": 1,
}


@patch('requests.post')
def test_search_serper_error(mock_post, search_toolkit):
"""Test error handling in Serper search."""
mock_post.side_effect = requests.exceptions.RequestException("API Error")

with patch.dict(os.environ, {'SERPER_API_KEY': 'test_key'}):
with pytest.raises(RuntimeError) as excinfo:
search_toolkit.search_serper(query="test")

assert "Error making request to Serper" in str(excinfo.value)
Loading