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
8 changes: 8 additions & 0 deletions lib/crewai-tools/src/crewai_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -300,6 +306,8 @@
"ScrapegraphScrapeTool",
"ScrapegraphScrapeToolSchema",
"ScrapflyScrapeWebsiteTool",
"SearchApiGoogleSearchTool",
"SearchApiGoogleShoppingTool",
"SeleniumScrapingTool",
"SerpApiGoogleSearchTool",
"SerpApiGoogleShoppingTool",
Expand Down
8 changes: 8 additions & 0 deletions lib/crewai-tools/src/crewai_tools/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -282,6 +288,8 @@
"ScrapegraphScrapeTool",
"ScrapegraphScrapeToolSchema",
"ScrapflyScrapeWebsiteTool",
"SearchApiGoogleSearchTool",
"SearchApiGoogleShoppingTool",
"SeleniumScrapingTool",
"SerpApiGoogleSearchTool",
"SerpApiGoogleShoppingTool",
Expand Down
32 changes: 32 additions & 0 deletions lib/crewai-tools/src/crewai_tools/tools/searchapi_tool/README.md
Original file line number Diff line number Diff line change
@@ -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()
```
Empty file.
Original file line number Diff line number Diff line change
@@ -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}
Original file line number Diff line number Diff line change
@@ -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}"
Original file line number Diff line number Diff line change
@@ -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}"
81 changes: 81 additions & 0 deletions lib/crewai-tools/tests/tools/searchapi_tool_test.py
Original file line number Diff line number Diff line change
@@ -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()
Loading