Skip to content

feat: add internet search tool #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Strands Agents Tools provides a powerful set of tools for your agents to use. It
- 🧠 **Advanced Reasoning** - Tools for complex thinking and reasoning capabilities
- 🐝 **Swarm Intelligence** - Coordinate multiple AI agents for parallel problem solving with shared memory
- 🔄 **Multiple tools in Parallel** - Call multiple other tools at the same time in parallel with Batch Tool
- 🌐 **Internet Search** - Internet search tool supporting multiple backends (DuckDuckGo, SerpAPI, Tavily, etc.)

## 📦 Installation

Expand Down Expand Up @@ -121,6 +122,7 @@ Below is a comprehensive table of all available tools, how to use them with an a
| use_llm | `agent.tool.use_llm(prompt="Analyze this data", system_prompt="You are a data analyst")` | Create nested AI loops with customized system prompts for specialized tasks |
| workflow | `agent.tool.workflow(action="create", name="data_pipeline", steps=[{"tool": "file_read"}, {"tool": "python_repl"}])` | Define, execute, and manage multi-step automated workflows |
| batch| `agent.tool.batch(invocations=[{"name": "current_time", "arguments": {"timezone": "Europe/London"}}, {"name": "stop", "arguments": {}}])` | Call multiple other tools in parallel. |
| internet_search| `agent.tool.internet_search(query="latest AI news", max_results=10, backend="serpapi", serpapi_api_key="<api_key>")` | Make search queries via search engine APIs. E.g. DuckDuckGo, SerpAPI, Tavily, etc. |

## 💻 Usage Examples

Expand Down Expand Up @@ -295,6 +297,21 @@ result = agent.tool.batch(
)
```


### Internet Search Tool

```python
from strands import Agent

from strands import Agent
from strands_tools import internet_search

# Example usage of internet search tool
agent = Agent(tools=[internet_search])

result = agent.tool.internet_search(query="latest AI news", max_results=10, backend="duckduckgo")
```

## 🌍 Environment Variables Configuration

Agents Tools provides extensive customization through environment variables. This allows you to configure tool behavior without modifying code, making it ideal for different environments (development, testing, production).
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ dependencies = [
"tenacity>=9.1.2,<10.0.0",
"watchdog>=6.0.0,<7.0.0",
"slack_bolt>=1.23.0,<2.0.0",
"duckduckgo-search",
"google-search-results",
"tavily-python",
# Note: Always want the latest tzdata
"tzdata ; platform_system == 'Windows'",
]
Expand Down
165 changes: 165 additions & 0 deletions src/strands_tools/internet_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""
Internet search tool

Internet search tool for Strands Agent supporting multiple backends (DuckDuckGo, SerpAPI, Tavily, etc).

Example usage:

# DuckDuckGo (default)
result = agent.tool.internet_search(query="latest AI news", max_results=5)

# SerpAPI (requires API key)
result = agent.tool.internet_search(
query="latest AI news",
max_results=5,
backend="serpapi",
serpapi_api_key="YOUR_SERPAPI_KEY"
)

# Tavily (requires API key)
result = agent.tool.internet_search(
query="latest AI news",
max_results=5,
backend="tavily",
tavily_api_key="YOUR_TAVILY_KEY"
)

"""

from typing import Any, Dict, List

from duckduckgo_search import DDGS
from strands.types.tools import ToolResult, ToolUse

try:
from serpapi import GoogleSearch
except ImportError:
GoogleSearch = None

try:
from tavily import TavilyClient
except ImportError:
TavilyClient = None

TOOL_SPEC = {
"name": "internet_search",
"description": "Search the internet for up-to-date information using DuckDuckGo, SerpAPI, Tavily, etc.",
"inputSchema": {
"json": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "The search query to look up on the internet."},
"max_results": {
"type": "integer",
"description": "Maximum number of search results to return.",
"default": 5,
},
"backend": {
"type": "string",
"description": "Which backend to use: 'duckduckgo', 'serpapi', or 'tavily'.",
"default": "duckduckgo",
},
"serpapi_api_key": {
"type": "string",
"description": "API key for SerpAPI (required if backend is 'serpapi').",
},
"tavily_api_key": {
"type": "string",
"description": "API key for Tavily (required if backend is 'tavily').",
},
},
"required": ["query"],
}
},
}


def _search_with_backend(
backend: str,
query: str,
max_results: int,
serpapi_api_key: str = None,
tavily_api_key: str = None,
) -> List[Dict[str, Any]]:
if backend == "duckduckgo":
results = []
with DDGS() as ddgs:
for r in ddgs.text(query, max_results=max_results):
results.append({"title": r.get("title"), "href": r.get("href"), "body": r.get("body")})
return results

elif backend == "serpapi":
if GoogleSearch is None:
raise ImportError("serpapi is not installed. Install with: pip install google-search-results")
if not serpapi_api_key:
raise ValueError("serpapi_api_key is required for SerpAPI backend.")
params = {
"q": query,
"api_key": serpapi_api_key,
"num": max_results,
"engine": "google",
"hl": "en", # Language
}
search = GoogleSearch(params)
serp_results = search.get_dict()

# Check for error in response
if "error" in serp_results:
raise ValueError(f"SerpAPI error: {serp_results['error']}")

if not serp_results or "organic_results" not in serp_results:
raise ValueError("No organic results found in SerpAPI response.")

results = []
for item in serp_results.get("organic_results", [])[:max_results]:
results.append(
{
"title": item.get("title"),
"href": item.get("link"),
"body": item.get("snippet"),
}
)
return results

elif backend == "tavily":
if TavilyClient is None:
raise ImportError("tavily-python is not installed. Install with: pip install tavily-python")
if not tavily_api_key:
raise ValueError("tavily_api_key is required for Tavily backend.")
client = TavilyClient(api_key=tavily_api_key)
tavily_results = client.search(query=query, search_depth="advanced", max_results=max_results)
results = []
for item in tavily_results.get("results", [])[:max_results]:
results.append(
{
"title": item.get("title"),
"href": item.get("url"),
"body": item.get("content"),
}
)
return results

else:
raise ValueError(f"Unknown backend: {backend}")


def internet_search(tool: ToolUse, **kwargs: Any) -> ToolResult:
tool_input = tool.get("input", tool)
tool_use_id = tool.get("toolUseId", "default_id")
query = tool_input.get("query")
max_results = tool_input.get("max_results", 5)
backend = tool_input.get("backend", "duckduckgo")
serpapi_api_key = tool_input.get("serpapi_api_key") or kwargs.get("serpapi_api_key")
tavily_api_key = tool_input.get("tavily_api_key") or kwargs.get("tavily_api_key")

try:
results = _search_with_backend(
backend=backend,
query=query,
max_results=max_results,
serpapi_api_key=serpapi_api_key,
tavily_api_key=tavily_api_key,
)
return {"toolUseId": tool_use_id, "status": "success", "content": [{"json": {"results": results}}]}
except Exception as e:
return {"toolUseId": tool_use_id, "status": "error", "content": [{"text": f"Internet search failed: {str(e)}"}]}
90 changes: 90 additions & 0 deletions tests/test_internet_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from unittest.mock import MagicMock

import pytest


@pytest.fixture
def mock_agent():
"""Fixture to create a mock agent with the internet_search tool."""
agent = MagicMock()
agent.tool.internet_search = MagicMock()
return agent


def extract_results(result):
"""Extract the results list from the agent/tool response."""
if isinstance(result, dict) and "content" in result and isinstance(result["content"], list):
for item in result["content"]:
if "json" in item and "results" in item["json"]:
return item["json"]["results"]
return []


def test_duckduckgo_search_with_mock_agent(mock_agent):
"""Test DuckDuckGo backend search using mock agent."""
mock_results = [
{"title": "AI News 1", "href": "http://a.com", "body": "Summary 1"},
{"title": "AI News 2", "href": "http://b.com", "body": "Summary 2"},
]
mock_agent.tool.internet_search.return_value = {
"toolUseId": "test-duck-id",
"status": "success",
"content": [{"json": {"results": mock_results}}],
}
result = mock_agent.tool.internet_search(query="AI news", max_results=2)
assert result["status"] == "success"
results = extract_results(result)
assert len(results) == 2
assert results[0]["title"] == "AI News 1"


def test_serpapi_search_with_mock_agent(mock_agent):
"""Test SerpAPI backend search using mock agent."""
mock_results = [
{"title": "Serp Result 1", "href": "http://serp1.com", "body": "Serp snippet 1"},
{"title": "Serp Result 2", "href": "http://serp2.com", "body": "Serp snippet 2"},
]
mock_agent.tool.internet_search.return_value = {
"toolUseId": "test-serpapi-id",
"status": "success",
"content": [{"json": {"results": mock_results}}],
}
result = mock_agent.tool.internet_search(
query="AI news", max_results=2, backend="serpapi", serpapi_api_key="dummy-key"
)
assert result["status"] == "success"
results = extract_results(result)
assert len(results) == 2
assert results[0]["title"] == "Serp Result 1"


def test_tavily_search_with_mock_agent(mock_agent):
"""Test Tavily backend search using mock agent."""
mock_results = [
{"title": "Tavily 1", "href": "http://tav1.com", "body": "Tavily content 1"},
{"title": "Tavily 2", "href": "http://tav2.com", "body": "Tavily content 2"},
]
mock_agent.tool.internet_search.return_value = {
"toolUseId": "test-tavily-id",
"status": "success",
"content": [{"json": {"results": mock_results}}],
}
result = mock_agent.tool.internet_search(
query="AI news", max_results=2, backend="tavily", tavily_api_key="dummy-key"
)
assert result["status"] == "success"
results = extract_results(result)
assert len(results) == 2
assert results[0]["title"] == "Tavily 1"


def test_invalid_backend_with_mock_agent(mock_agent):
"""Test error on unknown backend using mock agent."""
mock_agent.tool.internet_search.return_value = {
"toolUseId": "test-invalid-backend",
"status": "error",
"content": [{"text": "Unknown backend: notarealbackend"}],
}
result = mock_agent.tool.internet_search(query="AI news", backend="notarealbackend")
assert result["status"] == "error"
assert "Unknown backend" in result["content"][0]["text"]