diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index 6e79c4bb01..4d37bccec3 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -157,6 +157,12 @@ from crewai_tools.tools.scrapfly_scrape_website_tool.scrapfly_scrape_website_tool import ( ScrapflyScrapeWebsiteTool, ) +from crewai_tools.tools.searchapi_tool.searchapi_google_search_tool import ( + SearchApiGoogleSearchTool, +) +from crewai_tools.tools.searchapi_tool.searchapi_google_shopping_tool import ( + SearchApiGoogleShoppingTool, +) from crewai_tools.tools.selenium_scraping_tool.selenium_scraping_tool import ( SeleniumScrapingTool, ) @@ -300,6 +306,8 @@ "ScrapegraphScrapeTool", "ScrapegraphScrapeToolSchema", "ScrapflyScrapeWebsiteTool", + "SearchApiGoogleSearchTool", + "SearchApiGoogleShoppingTool", "SeleniumScrapingTool", "SerpApiGoogleSearchTool", "SerpApiGoogleShoppingTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 18bf4e5638..574cbb789a 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -145,6 +145,12 @@ from crewai_tools.tools.scrapfly_scrape_website_tool.scrapfly_scrape_website_tool import ( ScrapflyScrapeWebsiteTool, ) +from crewai_tools.tools.searchapi_tool.searchapi_google_search_tool import ( + SearchApiGoogleSearchTool, +) +from crewai_tools.tools.searchapi_tool.searchapi_google_shopping_tool import ( + SearchApiGoogleShoppingTool, +) from crewai_tools.tools.selenium_scraping_tool.selenium_scraping_tool import ( SeleniumScrapingTool, ) @@ -282,6 +288,8 @@ "ScrapegraphScrapeTool", "ScrapegraphScrapeToolSchema", "ScrapflyScrapeWebsiteTool", + "SearchApiGoogleSearchTool", + "SearchApiGoogleShoppingTool", "SeleniumScrapingTool", "SerpApiGoogleSearchTool", "SerpApiGoogleShoppingTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md new file mode 100644 index 0000000000..936eea4eb2 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md @@ -0,0 +1,32 @@ +# SearchApi Tools + +## Description +[SearchApi](https://www.searchapi.io/) tools are built for searching information on the internet. SearchApi is a real-time SERP API delivering structured data from 100+ search engines and sources. These tools currently support: +- Google Search +- Google Shopping + +To successfully make use of SearchApi tools, you have to have `SEARCHAPI_API_KEY` set in the environment. To get the API key, register a free account at [SearchApi](https://www.searchapi.io/). + +## Installation +To start using the SearchApi Tools, you must first install the `crewai_tools` package. This can be easily done with the following command: + +```shell +pip install 'crewai[tools]' +``` + +## Examples +The following example demonstrates how to initialize the tool + +### Google Search +```python +from crewai_tools import SearchApiGoogleSearchTool + +tool = SearchApiGoogleSearchTool() +``` + +### Google Shopping +```python +from crewai_tools import SearchApiGoogleShoppingTool + +tool = SearchApiGoogleShoppingTool() +``` diff --git a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_base_tool.py b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_base_tool.py new file mode 100644 index 0000000000..39fa92ee4e --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_base_tool.py @@ -0,0 +1,52 @@ +import os +from typing import Any + +from crewai.tools import BaseTool, EnvVar +from pydantic import Field, PrivateAttr +import requests + + +SEARCH_URL = "https://www.searchapi.io/api/v1/search" + + +class SearchApiBaseTool(BaseTool): + """Base class for SearchApi functionality with shared capabilities.""" + + package_dependencies: list[str] = Field(default_factory=lambda: ["requests"]) + env_vars: list[EnvVar] = Field( + default_factory=lambda: [ + EnvVar( + name="SEARCHAPI_API_KEY", + description="API key for SearchApi searches", + required=True, + ), + ] + ) + + # Stored as a private attribute so the key is never included in the model's + # serialization (model_dump) or repr output. + _api_key: str | None = PrivateAttr(default=None) + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + api_key = os.getenv("SEARCHAPI_API_KEY") + if not api_key: + raise ValueError( + "Missing API key, you can get the key from https://www.searchapi.io" + ) + self._api_key = api_key + + def _search(self, params: dict[str, Any]) -> dict[str, Any]: + """Perform a request against the SearchApi search endpoint.""" + headers = {"Authorization": f"Bearer {self._api_key}"} + response = requests.get(SEARCH_URL, params=params, headers=headers, timeout=30) + response.raise_for_status() + data: dict[str, Any] = response.json() + return data + + def _omit_fields( + self, data: dict[str, Any], omit_fields: list[str] + ) -> dict[str, Any]: + """Return a copy of the response without noisy metadata fields.""" + return {k: v for k, v in data.items() if k not in omit_fields} diff --git a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_google_search_tool.py b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_google_search_tool.py new file mode 100644 index 0000000000..9a980af4e2 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_google_search_tool.py @@ -0,0 +1,46 @@ +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field +import requests + +from crewai_tools.tools.searchapi_tool.searchapi_base_tool import SearchApiBaseTool + + +OMIT_FIELDS = ["search_metadata", "search_parameters", "pagination"] + + +class SearchApiGoogleSearchToolSchema(BaseModel): + """Input for Google Search.""" + + search_query: str = Field( + ..., description="Mandatory search query you want to use to Google search." + ) + location: str | None = Field( + None, description="Location you want the search to be performed in." + ) + + +class SearchApiGoogleSearchTool(SearchApiBaseTool): + model_config = ConfigDict( + arbitrary_types_allowed=True, validate_assignment=True, frozen=False + ) + name: str = "SearchApi Google Search" + description: str = ( + "A tool to perform a Google search with a search_query using SearchApi." + ) + args_schema: type[BaseModel] = SearchApiGoogleSearchToolSchema + + def _run(self, **kwargs: Any) -> Any: + try: + params = { + "engine": "google", + "q": kwargs.get("search_query"), + } + location = kwargs.get("location") + if location: + params["location"] = location + + results = self._search(params) + return self._omit_fields(results, OMIT_FIELDS) + except requests.RequestException as e: + return f"An error occurred while performing the search: {e!s}" diff --git a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_google_shopping_tool.py b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_google_shopping_tool.py new file mode 100644 index 0000000000..6c785358b3 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_google_shopping_tool.py @@ -0,0 +1,45 @@ +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field +import requests + +from crewai_tools.tools.searchapi_tool.searchapi_base_tool import SearchApiBaseTool + + +OMIT_FIELDS = ["search_metadata", "search_parameters", "pagination"] + + +class SearchApiGoogleShoppingToolSchema(BaseModel): + """Input for Google Shopping.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use for Google Shopping.", + ) + location: str | None = Field( + None, description="Location you want the search to be performed in." + ) + + +class SearchApiGoogleShoppingTool(SearchApiBaseTool): + model_config = ConfigDict( + arbitrary_types_allowed=True, validate_assignment=True, frozen=False + ) + name: str = "SearchApi Google Shopping" + description: str = "A tool to perform a Google Shopping search with a search_query using SearchApi." + args_schema: type[BaseModel] = SearchApiGoogleShoppingToolSchema + + def _run(self, **kwargs: Any) -> Any: + try: + params = { + "engine": "google_shopping", + "q": kwargs.get("search_query"), + } + location = kwargs.get("location") + if location: + params["location"] = location + + results = self._search(params) + return self._omit_fields(results, OMIT_FIELDS) + except requests.RequestException as e: + return f"An error occurred while performing the search: {e!s}" diff --git a/lib/crewai-tools/tests/tools/searchapi_tool_test.py b/lib/crewai-tools/tests/tools/searchapi_tool_test.py new file mode 100644 index 0000000000..8e12588971 --- /dev/null +++ b/lib/crewai-tools/tests/tools/searchapi_tool_test.py @@ -0,0 +1,81 @@ +import os +from unittest.mock import MagicMock, patch + +from crewai_tools.tools.searchapi_tool.searchapi_google_search_tool import ( + SearchApiGoogleSearchTool, +) +from crewai_tools.tools.searchapi_tool.searchapi_google_shopping_tool import ( + SearchApiGoogleShoppingTool, +) +import pytest + + +@pytest.fixture(autouse=True) +def mock_searchapi_api_key(): + with patch.dict(os.environ, {"SEARCHAPI_API_KEY": "test_key"}): + yield + + +def test_google_search_tool_initialization(): + tool = SearchApiGoogleSearchTool() + assert tool.name == "SearchApi Google Search" + assert tool._api_key == "test_key" + + +def test_api_key_not_serialized(): + """The API key must never leak via the model's serialization.""" + tool = SearchApiGoogleSearchTool() + assert "test_key" not in str(tool.model_dump(mode="json")) + assert "api_key" not in tool.model_dump(mode="json") + + +def test_google_shopping_tool_initialization(): + tool = SearchApiGoogleShoppingTool() + assert tool.name == "SearchApi Google Shopping" + + +def test_missing_api_key_raises(): + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ValueError): + SearchApiGoogleSearchTool() + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_base_tool.requests.get") +def test_google_search_run(mock_get): + mock_response = MagicMock() + mock_response.json.return_value = { + "search_metadata": {"id": "abc"}, + "organic_results": [ + {"title": "T1", "link": "http://t1.com", "snippet": "S1"}, + ], + } + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + tool = SearchApiGoogleSearchTool() + result = tool._run(search_query="best electric cars 2026", location="New York") + + # noisy metadata is omitted, real results are kept + assert "organic_results" in result + assert "search_metadata" not in result + + # request is shaped correctly + _, kwargs = mock_get.call_args + assert kwargs["params"]["engine"] == "google" + assert kwargs["params"]["q"] == "best electric cars 2026" + assert kwargs["params"]["location"] == "New York" + assert kwargs["headers"]["Authorization"] == "Bearer test_key" + + +@patch("crewai_tools.tools.searchapi_tool.searchapi_base_tool.requests.get") +def test_search_handles_network_error(mock_get): + """A timeout/connection failure is returned as a message, not raised.""" + import requests + + mock_get.side_effect = requests.Timeout("connection timed out") + + tool = SearchApiGoogleSearchTool() + result = tool._run(search_query="anything") + + assert isinstance(result, str) + assert "error occurred" in result.lower() diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json index 795fa932c4..4dd996c686 100644 --- a/lib/crewai-tools/tool.specs.json +++ b/lib/crewai-tools/tool.specs.json @@ -20158,6 +20158,180 @@ "type": "object" } }, + { + "description": "A tool to perform a Google search with a search_query using SearchApi.", + "env_vars": [ + { + "default": null, + "description": "API key for SearchApi searches", + "name": "SEARCHAPI_API_KEY", + "required": true + } + ], + "humanized_name": "SearchApi Google Search", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": {}, + "required": [], + "title": "SearchApiGoogleSearchTool", + "type": "object" + }, + "name": "SearchApiGoogleSearchTool", + "package_dependencies": [ + "requests" + ], + "run_params_schema": { + "description": "Input for Google Search.", + "properties": { + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Location you want the search to be performed in.", + "title": "Location" + }, + "search_query": { + "description": "Mandatory search query you want to use to Google search.", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "SearchApiGoogleSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool to perform a Google Shopping search with a search_query using SearchApi.", + "env_vars": [ + { + "default": null, + "description": "API key for SearchApi searches", + "name": "SEARCHAPI_API_KEY", + "required": true + } + ], + "humanized_name": "SearchApi Google Shopping", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": {}, + "required": [], + "title": "SearchApiGoogleShoppingTool", + "type": "object" + }, + "name": "SearchApiGoogleShoppingTool", + "package_dependencies": [ + "requests" + ], + "run_params_schema": { + "description": "Input for Google Shopping.", + "properties": { + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Location you want the search to be performed in.", + "title": "Location" + }, + "search_query": { + "description": "Mandatory search query you want to use for Google Shopping.", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "SearchApiGoogleShoppingToolSchema", + "type": "object" + } + }, { "description": "A tool that can be used to read a website content.", "env_vars": [],