diff --git a/camel/toolkits/search_toolkit.py b/camel/toolkits/search_toolkit.py index 9b6013d0b9..db577a2fa6 100644 --- a/camel/toolkits/search_toolkit.py +++ b/camel/toolkits/search_toolkit.py @@ -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 @@ -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" @@ -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") @@ -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 @@ -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), diff --git a/docs/mintlify/reference/camel.toolkits.search_toolkit.mdx b/docs/mintlify/reference/camel.toolkits.search_toolkit.mdx index a2ae4ed05d..c979f00910 100644 --- a/docs/mintlify/reference/camel.toolkits.search_toolkit.mdx +++ b/docs/mintlify/reference/camel.toolkits.search_toolkit.mdx @@ -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`) + + +### 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. + ### search_wiki diff --git a/docs/reference/camel.toolkits.search_toolkit.md b/docs/reference/camel.toolkits.search_toolkit.md index 91e1a4c1e9..6c220f7359 100644 --- a/docs/reference/camel.toolkits.search_toolkit.md +++ b/docs/reference/camel.toolkits.search_toolkit.md @@ -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`) + + +### 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. + ### search_wiki diff --git a/examples/toolkits/search_toolkit.py b/examples/toolkits/search_toolkit.py index 7f340247ef..c1fbc7146b 100644 --- a/examples/toolkits/search_toolkit.py +++ b/examples/toolkits/search_toolkit.py @@ -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 @@ -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""", diff --git a/test/toolkits/test_search_functions.py b/test/toolkits/test_search_functions.py index 2a650876cf..596ecf9578 100644 --- a/test/toolkits/test_search_functions.py +++ b/test/toolkits/test_search_functions.py @@ -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)