From 42a6dc8088c5c1557ec0163c091e2bdbefcf4a47 Mon Sep 17 00:00:00 2001 From: Isaac Hernandez Date: Thu, 2 Jul 2026 09:41:02 -0400 Subject: [PATCH 1/3] feat(tools): add SearchApi tool with multi-engine support Add SearchApiSearchTool supporting 7 search engines (google, google_news, google_shopping, google_jobs, youtube, bing, baidu) through a single configurable tool class. Key design decisions: - Single tool with engine parameter vs separate classes per engine - Bearer token auth with PrivateAttr to prevent key serialization leaks - Per-query location parameter for runtime flexibility - Comprehensive test suite (19 tests) covering initialization, request construction, multi-engine execution, and error handling --- lib/crewai-tools/src/crewai_tools/__init__.py | 3 + .../src/crewai_tools/tools/__init__.py | 4 + .../tools/searchapi_tool/README.md | 81 +++++ .../tools/searchapi_tool/__init__.py | 0 .../searchapi_tool/searchapi_search_tool.py | 124 ++++++++ .../tests/tools/searchapi_tool_test.py | 286 ++++++++++++++++++ lib/crewai-tools/tool.specs.json | 122 ++++++++ 7 files changed, 620 insertions(+) create mode 100644 lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md create mode 100644 lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/__init__.py create mode 100644 lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_search_tool.py create mode 100644 lib/crewai-tools/tests/tools/searchapi_tool_test.py diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index cb71de1d64..003b3d4b92 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -160,6 +160,9 @@ from crewai_tools.tools.selenium_scraping_tool.selenium_scraping_tool import ( SeleniumScrapingTool, ) +from crewai_tools.tools.searchapi_tool.searchapi_search_tool import ( + SearchApiSearchTool, +) from crewai_tools.tools.serpapi_tool.serpapi_google_search_tool import ( SerpApiGoogleSearchTool, ) diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 18bf4e5638..40e2a6d07c 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -148,6 +148,9 @@ from crewai_tools.tools.selenium_scraping_tool.selenium_scraping_tool import ( SeleniumScrapingTool, ) +from crewai_tools.tools.searchapi_tool.searchapi_search_tool import ( + SearchApiSearchTool, +) from crewai_tools.tools.serpapi_tool.serpapi_google_search_tool import ( SerpApiGoogleSearchTool, ) @@ -283,6 +286,7 @@ "ScrapegraphScrapeToolSchema", "ScrapflyScrapeWebsiteTool", "SeleniumScrapingTool", + "SearchApiSearchTool", "SerpApiGoogleSearchTool", "SerpApiGoogleShoppingTool", "SerperDevTool", 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..c232ff9805 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md @@ -0,0 +1,81 @@ +# SearchApi Tool + +## Description +[SearchApi](https://www.searchapi.io) is a real-time SERP API that delivers structured data from 100+ search engines and sources. A single tool class supports multiple engines — no need for separate tool classes per search type. + +To use this tool, set `SEARCHAPI_API_KEY` in your environment. Get your API key at [searchapi.io](https://www.searchapi.io). + +## Installation +```shell +pip install 'crewai[tools]' +``` + +## Supported Engines +| Engine | Description | +|--------|-------------| +| `google` | Google web search (default) | +| `google_news` | Google News | +| `google_shopping` | Google Shopping | +| `google_jobs` | Google Jobs | +| `youtube` | YouTube video search | +| `bing` | Bing web search | +| `baidu` | Baidu search | + +## Usage + +### Google Search (default) +```python +from crewai_tools import SearchApiSearchTool + +tool = SearchApiSearchTool() +``` + +### Google News +```python +from crewai_tools import SearchApiSearchTool + +tool = SearchApiSearchTool(engine="google_news") +``` + +### Google Shopping +```python +from crewai_tools import SearchApiSearchTool + +tool = SearchApiSearchTool(engine="google_shopping") +``` + +### YouTube Search +```python +from crewai_tools import SearchApiSearchTool + +tool = SearchApiSearchTool(engine="youtube") +``` + +### With Location, Country and Language +```python +from crewai_tools import SearchApiSearchTool + +tool = SearchApiSearchTool( + engine="google", + n_results=5, + country="us", + language="en", +) + +# Location can also be passed per-query at runtime +result = tool.run(search_query="coffee shops", location="San Francisco") +``` + +## Configuration +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `engine` | `str` | `"google"` | Search engine to use | +| `n_results` | `int` | `10` | Number of results to return | +| `country` | `str \| None` | `None` | Country code (e.g., `"us"`, `"uk"`) | +| `language` | `str \| None` | `None` | Language code (e.g., `"en"`, `"es"`) | + +## Runtime Parameters +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `search_query` | `str` | Yes | The search query to execute | +| `location` | `str \| None` | No | Location for the search (e.g., `"New York"`) | 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_search_tool.py b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_search_tool.py new file mode 100644 index 0000000000..ba63e82e6e --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_search_tool.py @@ -0,0 +1,124 @@ +"""SearchApi search tool for CrewAI agents.""" + +import logging +import os +from typing import Any + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr +import requests + + +logger = logging.getLogger(__name__) + +BASE_URL = "https://www.searchapi.io/api/v1/search" + +SUPPORTED_ENGINES = [ + "google", + "google_news", + "google_shopping", + "google_jobs", + "youtube", + "bing", + "baidu", +] + + +class SearchApiSearchToolSchema(BaseModel): + """Input schema for SearchApi search tool.""" + + search_query: str = Field( + ..., description="Mandatory search query to perform the search." + ) + location: str | None = Field( + None, description="Location to perform the search from (e.g., 'New York')." + ) + + +class SearchApiSearchTool(BaseTool): + """Search the internet using SearchApi. + + Supports multiple engines including Google, Google News, Google Shopping, + Google Jobs, YouTube, Bing, and Baidu. Configure the engine at initialization. + """ + + model_config = ConfigDict( + arbitrary_types_allowed=True, validate_assignment=True, frozen=False + ) + name: str = "SearchApi Search" + description: str = ( + "A tool that searches the internet using SearchApi. " + "Supports multiple engines: google, google_news, google_shopping, " + "google_jobs, youtube, bing, and baidu." + ) + args_schema: type[BaseModel] = SearchApiSearchToolSchema + 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 (https://www.searchapi.io)", + required=True, + ), + ] + ) + + engine: str = "google" + n_results: int = 10 + country: str | None = None + language: str | None = None + + _api_key: str | None = PrivateAttr(default=None) + + def __init__(self, **kwargs: Any) -> None: + """Initialize the SearchApi tool and validate configuration.""" + super().__init__(**kwargs) + if self.engine not in SUPPORTED_ENGINES: + raise ValueError( + f"Invalid engine: {self.engine}. " + f"Must be one of: {', '.join(SUPPORTED_ENGINES)}" + ) + api_key = os.getenv("SEARCHAPI_API_KEY") + if not api_key: + raise ValueError( + "Missing SEARCHAPI_API_KEY. Get your key at https://www.searchapi.io" + ) + self._api_key = api_key + + def _run(self, **kwargs: Any) -> Any: + """Execute a search query against the configured SearchApi engine.""" + search_query: str | None = kwargs.get("search_query") or kwargs.get("query") + if not search_query: + raise ValueError("search_query is required") + + params: dict[str, Any] = { + "engine": self.engine, + "q": search_query, + "num": self.n_results, + } + + location = kwargs.get("location") + if location: + params["location"] = location + if self.country: + params["gl"] = self.country + if self.language: + params["hl"] = self.language + + headers = {"Authorization": f"Bearer {self._api_key}"} + + try: + response = requests.get( + BASE_URL, params=params, headers=headers, timeout=30 + ) + response.raise_for_status() + results: dict[str, Any] = response.json() + except requests.RequestException as e: + error_msg = f"An error occurred while performing the search: {e!s}" + logger.error(error_msg) + return error_msg + + for key in ["search_metadata", "search_parameters", "pagination"]: + results.pop(key, None) + + return results 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..22f939478b --- /dev/null +++ b/lib/crewai-tools/tests/tools/searchapi_tool_test.py @@ -0,0 +1,286 @@ +"""Tests for SearchApiSearchTool.""" + +import os +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from crewai_tools.tools.searchapi_tool.searchapi_search_tool import ( + SearchApiSearchTool, +) + + +@pytest.fixture(autouse=True) +def mock_searchapi_api_key(): + with patch.dict(os.environ, {"SEARCHAPI_API_KEY": "test_key"}): + yield + + +class TestInitialization: + """Test tool initialization and configuration.""" + + def test_default_initialization(self): + tool = SearchApiSearchTool() + assert tool.name == "SearchApi Search" + assert tool.engine == "google" + assert tool.n_results == 10 + assert tool.country is None + assert tool.language is None + + def test_custom_engine(self): + tool = SearchApiSearchTool(engine="google_news") + assert tool.engine == "google_news" + + def test_custom_parameters(self): + tool = SearchApiSearchTool( + engine="youtube", + n_results=5, + country="us", + language="en", + ) + assert tool.engine == "youtube" + assert tool.n_results == 5 + assert tool.country == "us" + assert tool.language == "en" + + def test_invalid_engine_raises(self): + with pytest.raises(ValueError, match="Invalid engine"): + SearchApiSearchTool(engine="invalid_engine") + + def test_missing_api_key_raises(self): + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ValueError, match="Missing SEARCHAPI_API_KEY"): + SearchApiSearchTool() + + def test_api_key_not_serialized(self): + """The API key must never leak via model serialization.""" + tool = SearchApiSearchTool() + dumped = tool.model_dump(mode="json") + assert "test_key" not in str(dumped) + assert "_api_key" not in dumped + + def test_all_supported_engines(self): + engines = [ + "google", + "google_news", + "google_shopping", + "google_jobs", + "youtube", + "bing", + "baidu", + ] + for engine in engines: + tool = SearchApiSearchTool(engine=engine) + assert tool.engine == engine + + +class TestSearchExecution: + """Test the _run method with mocked HTTP requests.""" + + @patch( + "crewai_tools.tools.searchapi_tool.searchapi_search_tool.requests.get" + ) + def test_google_search(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = { + "search_metadata": {"id": "abc"}, + "search_parameters": {"q": "test"}, + "pagination": {"next": "..."}, + "organic_results": [ + {"title": "Result 1", "link": "http://r1.com", "snippet": "Snippet 1"}, + {"title": "Result 2", "link": "http://r2.com", "snippet": "Snippet 2"}, + ], + } + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + tool = SearchApiSearchTool() + result = tool._run(search_query="best electric cars") + + assert "organic_results" in result + assert len(result["organic_results"]) == 2 + assert "search_metadata" not in result + assert "search_parameters" not in result + assert "pagination" not in result + + @patch( + "crewai_tools.tools.searchapi_tool.searchapi_search_tool.requests.get" + ) + def test_google_news_search(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = { + "search_metadata": {"id": "def"}, + "news_results": [ + {"title": "News 1", "link": "http://n1.com", "date": "2026-01-01"}, + ], + } + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + tool = SearchApiSearchTool(engine="google_news") + result = tool._run(search_query="AI news") + + assert "news_results" in result + assert "search_metadata" not in result + + @patch( + "crewai_tools.tools.searchapi_tool.searchapi_search_tool.requests.get" + ) + def test_google_shopping_search(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = { + "search_metadata": {"id": "ghi"}, + "shopping_results": [ + {"title": "Product 1", "price": "$99", "link": "http://p1.com"}, + ], + } + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + tool = SearchApiSearchTool(engine="google_shopping") + result = tool._run(search_query="wireless headphones") + + assert "shopping_results" in result + assert "search_metadata" not in result + + @patch( + "crewai_tools.tools.searchapi_tool.searchapi_search_tool.requests.get" + ) + def test_youtube_search(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = { + "search_metadata": {"id": "jkl"}, + "video_results": [ + {"title": "Video 1", "link": "http://yt.com/1"}, + ], + } + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + tool = SearchApiSearchTool(engine="youtube") + result = tool._run(search_query="python tutorial") + + assert "video_results" in result + assert "search_metadata" not in result + + +class TestRequestConstruction: + """Test that requests are constructed correctly.""" + + @patch( + "crewai_tools.tools.searchapi_tool.searchapi_search_tool.requests.get" + ) + def test_request_params(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = {} + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + tool = SearchApiSearchTool(engine="google", n_results=5) + tool._run(search_query="test query") + + _, kwargs = mock_get.call_args + assert kwargs["params"]["engine"] == "google" + assert kwargs["params"]["q"] == "test query" + assert kwargs["params"]["num"] == 5 + assert kwargs["headers"]["Authorization"] == "Bearer test_key" + + @patch( + "crewai_tools.tools.searchapi_tool.searchapi_search_tool.requests.get" + ) + def test_request_with_location(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = {} + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + tool = SearchApiSearchTool() + tool._run(search_query="coffee shops", location="San Francisco") + + _, kwargs = mock_get.call_args + assert kwargs["params"]["location"] == "San Francisco" + + @patch( + "crewai_tools.tools.searchapi_tool.searchapi_search_tool.requests.get" + ) + def test_request_with_country_and_language(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = {} + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + tool = SearchApiSearchTool(country="us", language="en") + tool._run(search_query="test") + + _, kwargs = mock_get.call_args + assert kwargs["params"]["gl"] == "us" + assert kwargs["params"]["hl"] == "en" + + @patch( + "crewai_tools.tools.searchapi_tool.searchapi_search_tool.requests.get" + ) + def test_location_not_sent_when_none(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = {} + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + tool = SearchApiSearchTool() + tool._run(search_query="test") + + _, kwargs = mock_get.call_args + assert "location" not in kwargs["params"] + assert "gl" not in kwargs["params"] + assert "hl" not in kwargs["params"] + + +class TestErrorHandling: + """Test error handling for various failure modes.""" + + @patch( + "crewai_tools.tools.searchapi_tool.searchapi_search_tool.requests.get" + ) + def test_timeout_returns_message(self, mock_get): + mock_get.side_effect = requests.Timeout("connection timed out") + + tool = SearchApiSearchTool() + result = tool._run(search_query="anything") + + assert isinstance(result, str) + assert "error occurred" in result.lower() + + @patch( + "crewai_tools.tools.searchapi_tool.searchapi_search_tool.requests.get" + ) + def test_connection_error_returns_message(self, mock_get): + mock_get.side_effect = requests.ConnectionError("failed to connect") + + tool = SearchApiSearchTool() + result = tool._run(search_query="anything") + + assert isinstance(result, str) + assert "error occurred" in result.lower() + + @patch( + "crewai_tools.tools.searchapi_tool.searchapi_search_tool.requests.get" + ) + def test_http_error_returns_message(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = requests.HTTPError("401 Unauthorized") + mock_get.return_value = mock_response + + tool = SearchApiSearchTool() + result = tool._run(search_query="anything") + + assert isinstance(result, str) + assert "error occurred" in result.lower() + + def test_missing_search_query_raises(self): + tool = SearchApiSearchTool() + with pytest.raises(ValueError, match="search_query is required"): + tool._run() + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json index 795fa932c4..c67981a418 100644 --- a/lib/crewai-tools/tool.specs.json +++ b/lib/crewai-tools/tool.specs.json @@ -20304,6 +20304,128 @@ "type": "object" } }, + { + "description": "A tool that searches the internet using SearchApi. Supports multiple engines: google, google_news, google_shopping, google_jobs, youtube, bing, and baidu.", + "env_vars": [ + { + "default": null, + "description": "API key for SearchApi (https://www.searchapi.io)", + "name": "SEARCHAPI_API_KEY", + "required": true + } + ], + "humanized_name": "SearchApi 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": { + "engine": { + "default": "google", + "title": "Engine", + "type": "string" + }, + "n_results": { + "default": 10, + "title": "N Results", + "type": "integer" + }, + "country": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Country" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Language" + } + }, + "required": [], + "title": "SearchApiSearchTool", + "type": "object" + }, + "name": "SearchApiSearchTool", + "package_dependencies": [ + "requests" + ], + "run_params_schema": { + "description": "Input schema for SearchApi search tool.", + "properties": { + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Location to perform the search from (e.g., 'New York').", + "title": "Location" + }, + "search_query": { + "description": "Mandatory search query to perform the search.", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "SearchApiSearchToolSchema", + "type": "object" + } + }, { "description": "A tool to perform to perform a Google search with a search_query.", "env_vars": [ From 34e9f0fea3aac34b0e6ebdebe46cda879d9e8254 Mon Sep 17 00:00:00 2001 From: Isaac Hernandez Date: Thu, 2 Jul 2026 09:55:28 -0400 Subject: [PATCH 2/3] fix(tools): address review findings for searchapi tool - Add SearchApiSearchTool to __all__ in crewai_tools/__init__.py - Fix MD058: add blank lines before tables in README - Move engine validation to field_validator for assignment-time checks --- examples/searchapi_tool_demo.py | 51 +++++++++++++++++++ lib/crewai-tools/src/crewai_tools/__init__.py | 1 + .../tools/searchapi_tool/README.md | 3 ++ .../searchapi_tool/searchapi_search_tool.py | 17 ++++--- 4 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 examples/searchapi_tool_demo.py diff --git a/examples/searchapi_tool_demo.py b/examples/searchapi_tool_demo.py new file mode 100644 index 0000000000..5d0c5aaab5 --- /dev/null +++ b/examples/searchapi_tool_demo.py @@ -0,0 +1,51 @@ +"""Live demonstration of SearchApiSearchTool with multiple engines. + +Run with: SEARCHAPI_API_KEY=your_key uv run python examples/searchapi_tool_demo.py +""" + +import json +import os +import sys + +sys.path.insert(0, "lib/crewai-tools/src") +sys.path.insert(0, "lib/crewai/src") + +from crewai_tools import SearchApiSearchTool + + +def demo_engine(engine: str, query: str) -> None: + print(f"\n{'='*60}") + print(f"Engine: {engine}") + print(f"Query: {query}") + print("=" * 60) + + tool = SearchApiSearchTool(engine=engine, n_results=3) + result = tool.run(search_query=query) + + if isinstance(result, str): + print(f"Error: {result}") + return + + print(json.dumps(result, indent=2, ensure_ascii=False)[:2000]) + print(f"\n... ({len(json.dumps(result))} total chars)") + + +def main() -> None: + if not os.getenv("SEARCHAPI_API_KEY"): + print("Set SEARCHAPI_API_KEY environment variable to run this demo.") + print("Get a free key at https://www.searchapi.io") + sys.exit(1) + + demos = [ + ("google", "CrewAI AI agents framework"), + ("google_news", "artificial intelligence 2026"), + ("google_shopping", "mechanical keyboard"), + ("youtube", "python tutorial beginners"), + ] + + for engine, query in demos: + demo_engine(engine, query) + + +if __name__ == "__main__": + main() diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index 003b3d4b92..c7db7e1a73 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -303,6 +303,7 @@ "ScrapegraphScrapeTool", "ScrapegraphScrapeToolSchema", "ScrapflyScrapeWebsiteTool", + "SearchApiSearchTool", "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 index c232ff9805..183c3aca8a 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md +++ b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md @@ -11,6 +11,7 @@ pip install 'crewai[tools]' ``` ## Supported Engines + | Engine | Description | |--------|-------------| | `google` | Google web search (default) | @@ -67,6 +68,7 @@ result = tool.run(search_query="coffee shops", location="San Francisco") ``` ## Configuration + | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `engine` | `str` | `"google"` | Search engine to use | @@ -75,6 +77,7 @@ result = tool.run(search_query="coffee shops", location="San Francisco") | `language` | `str \| None` | `None` | Language code (e.g., `"en"`, `"es"`) | ## Runtime Parameters + | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `search_query` | `str` | Yes | The search query to execute | diff --git a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_search_tool.py b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_search_tool.py index ba63e82e6e..8e59fc3136 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_search_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/searchapi_search_tool.py @@ -5,7 +5,7 @@ from typing import Any from crewai.tools import BaseTool, EnvVar -from pydantic import BaseModel, ConfigDict, Field, PrivateAttr +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator import requests @@ -70,14 +70,19 @@ class SearchApiSearchTool(BaseTool): _api_key: str | None = PrivateAttr(default=None) + @field_validator("engine") + @classmethod + def validate_engine(cls, v: str) -> str: + """Validate the engine is supported.""" + if v not in SUPPORTED_ENGINES: + raise ValueError( + f"Invalid engine: {v}. Must be one of: {', '.join(SUPPORTED_ENGINES)}" + ) + return v + def __init__(self, **kwargs: Any) -> None: """Initialize the SearchApi tool and validate configuration.""" super().__init__(**kwargs) - if self.engine not in SUPPORTED_ENGINES: - raise ValueError( - f"Invalid engine: {self.engine}. " - f"Must be one of: {', '.join(SUPPORTED_ENGINES)}" - ) api_key = os.getenv("SEARCHAPI_API_KEY") if not api_key: raise ValueError( From 261481dc2326d25e9208e4ce6f41246e514ac5f1 Mon Sep 17 00:00:00 2001 From: Isaac Hernandez Date: Thu, 2 Jul 2026 09:58:12 -0400 Subject: [PATCH 3/3] chore: remove demo script from PR scope --- examples/searchapi_tool_demo.py | 51 --------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 examples/searchapi_tool_demo.py diff --git a/examples/searchapi_tool_demo.py b/examples/searchapi_tool_demo.py deleted file mode 100644 index 5d0c5aaab5..0000000000 --- a/examples/searchapi_tool_demo.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Live demonstration of SearchApiSearchTool with multiple engines. - -Run with: SEARCHAPI_API_KEY=your_key uv run python examples/searchapi_tool_demo.py -""" - -import json -import os -import sys - -sys.path.insert(0, "lib/crewai-tools/src") -sys.path.insert(0, "lib/crewai/src") - -from crewai_tools import SearchApiSearchTool - - -def demo_engine(engine: str, query: str) -> None: - print(f"\n{'='*60}") - print(f"Engine: {engine}") - print(f"Query: {query}") - print("=" * 60) - - tool = SearchApiSearchTool(engine=engine, n_results=3) - result = tool.run(search_query=query) - - if isinstance(result, str): - print(f"Error: {result}") - return - - print(json.dumps(result, indent=2, ensure_ascii=False)[:2000]) - print(f"\n... ({len(json.dumps(result))} total chars)") - - -def main() -> None: - if not os.getenv("SEARCHAPI_API_KEY"): - print("Set SEARCHAPI_API_KEY environment variable to run this demo.") - print("Get a free key at https://www.searchapi.io") - sys.exit(1) - - demos = [ - ("google", "CrewAI AI agents framework"), - ("google_news", "artificial intelligence 2026"), - ("google_shopping", "mechanical keyboard"), - ("youtube", "python tutorial beginners"), - ] - - for engine, query in demos: - demo_engine(engine, query) - - -if __name__ == "__main__": - main()