Skip to content

Commit 38e3f6c

Browse files
authored
Add DuckDuckGoSearch tool (#997)
1 parent 7303040 commit 38e3f6c

File tree

7 files changed

+290
-31
lines changed

7 files changed

+290
-31
lines changed

docs/common_tools.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
PydanticAI ships with native tools that can be used to enhance your agent's capabilities.
2+
3+
## DuckDuckGo Search Tool
4+
5+
The DuckDuckGo search tool allows you to search the web for information. It is built on top of the
6+
[DuckDuckGo API](https://github.com/deedy5/duckduckgo_search).
7+
8+
```py {title="main.py" test="skip"}
9+
from pydantic_ai import Agent
10+
from pydantic_ai.toolsets.duckduckgo import duckduckgo_search_tool
11+
12+
agent = Agent(
13+
'openai:o3-mini',
14+
tools=[duckduckgo_search_tool()],
15+
system_prompt='Search DuckDuckGo for the given query and return the results.',
16+
)
17+
18+
result = agent.run_sync(
19+
'Can you list the top five highest-grossing animated films of 2025?'
20+
)
21+
print(result.data)
22+
"""
23+
I looked into several sources on animated box‐office performance in 2025, and while detailed
24+
rankings can shift as more money is tallied, multiple independent reports have already
25+
highlighted a couple of record‐breaking shows. For example:
26+
27+
• Ne Zha 2 – News outlets (Variety, Wikipedia's "List of animated feature films of 2025", and others)
28+
have reported that this Chinese title not only became the highest‑grossing animated film of 2025
29+
but also broke records as the highest‑grossing non‑English animated film ever. One article noted
30+
its run exceeded US$1.7 billion.
31+
• Inside Out 2 – According to data shared on Statista and in industry news, this Pixar sequel has been
32+
on pace to set new records (with some sources even noting it as the highest‑grossing animated film
33+
ever, as of January 2025).
34+
35+
Beyond those two, some entertainment trade sites (for example, a Just Jared article titled
36+
"Top 10 Highest-Earning Animated Films at the Box Office Revealed") have begun listing a broader
37+
top‑10. Although full consolidated figures can sometimes differ by source and are updated daily during
38+
a box‑office run, many of the industry trackers have begun to single out five films as the biggest
39+
earners so far in 2025.
40+
41+
Unfortunately, although multiple articles discuss the "top animated films" of 2025, there isn't yet a
42+
single, universally accepted list with final numbers that names the complete top five. (Box‑office
43+
rankings, especially mid‑year, can be fluid as films continue to add to their totals.)
44+
45+
Based on what several sources note so far, the two undisputed leaders are:
46+
1. Ne Zha 2
47+
2. Inside Out 2
48+
49+
The remaining top spots (3–5) are reported by some outlets in their "Top‑10 Animated Films"
50+
lists for 2025 but the titles and order can vary depending on the source and the exact cut‑off
51+
date of the data. For the most up‑to‑date and detailed ranking (including the 3rd, 4th, and 5th
52+
highest‑grossing films), I recommend checking resources like:
53+
• Wikipedia's "List of animated feature films of 2025" page
54+
• Box‑office tracking sites (such as Box Office Mojo or The Numbers)
55+
• Trade articles like the one on Just Jared
56+
57+
To summarize with what is clear from the current reporting:
58+
1. Ne Zha 2
59+
2. Inside Out 2
60+
3–5. Other animated films (yet to be definitively finalized across all reporting outlets)
61+
62+
If you're looking for a final, consensus list of the top five, it may be best to wait until
63+
the 2025 year‑end box‑office tallies are in or to consult a regularly updated entertainment industry source.
64+
65+
Would you like help finding a current source or additional details on where to look for the complete updated list?
66+
"""
67+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ nav:
2020
- models.md
2121
- dependencies.md
2222
- tools.md
23+
- common_tools.md
2324
- results.md
2425
- message-history.md
2526
- testing-evals.md

pydantic_ai_slim/pydantic_ai/common_tools/__init__.py

Whitespace-only changes.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from dataclasses import dataclass
2+
from typing import TypedDict
3+
4+
import anyio
5+
import anyio.to_thread
6+
from pydantic import TypeAdapter
7+
8+
from pydantic_ai.tools import Tool
9+
10+
try:
11+
from duckduckgo_search import DDGS
12+
except ImportError as _import_error:
13+
raise ImportError(
14+
'Please install `duckduckgo-search` to use the DuckDuckGo search tool, '
15+
"you can use the `duckduckgo` optional group — `pip install 'pydantic-ai-slim[duckduckgo]'`"
16+
) from _import_error
17+
18+
__all__ = ('duckduckgo_search_tool',)
19+
20+
21+
class DuckDuckGoResult(TypedDict):
22+
"""A DuckDuckGo search result."""
23+
24+
title: str
25+
"""The title of the search result."""
26+
href: str
27+
"""The URL of the search result."""
28+
body: str
29+
"""The body of the search result."""
30+
31+
32+
duckduckgo_ta = TypeAdapter(list[DuckDuckGoResult])
33+
34+
35+
@dataclass
36+
class DuckDuckGoSearchTool:
37+
"""The DuckDuckGo search tool."""
38+
39+
client: DDGS
40+
"""The DuckDuckGo search client."""
41+
42+
async def __call__(self, query: str) -> list[DuckDuckGoResult]:
43+
"""Searches DuckDuckGo for the given query and returns the results.
44+
45+
Args:
46+
query: The query to search for.
47+
48+
Returns:
49+
The search results.
50+
"""
51+
results = await anyio.to_thread.run_sync(self.client.text, query)
52+
if len(results) == 0:
53+
raise RuntimeError('No search results found.')
54+
return duckduckgo_ta.validate_python(results)
55+
56+
57+
def duckduckgo_search_tool(duckduckgo_client: DDGS | None = None):
58+
"""Creates a DuckDuckGo search tool."""
59+
return Tool(
60+
DuckDuckGoSearchTool(client=duckduckgo_client or DDGS()).__call__,
61+
name='duckduckgo_search',
62+
description='Searches DuckDuckGo for the given query and returns the results.',
63+
)

pydantic_ai_slim/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,15 @@ dependencies = [
4444
[project.optional-dependencies]
4545
# WARNING if you add optional groups, please update docs/install.md
4646
logfire = ["logfire>=2.3"]
47+
# Models
4748
openai = ["openai>=1.61.0"]
4849
cohere = ["cohere>=5.13.11"]
4950
vertexai = ["google-auth>=2.36.0", "requests>=2.32.3"]
5051
anthropic = ["anthropic>=0.40.0"]
5152
groq = ["groq>=0.12.0"]
5253
mistral = ["mistralai>=1.2.5"]
54+
# Tools
55+
duckduckgo = ["duckduckgo-search>=7.0.0"]
5356

5457
[dependency-groups]
5558
dev = [

pyproject.toml

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ build-backend = "hatchling.build"
66
name = "pydantic-ai"
77
version = "0.0.27"
88
description = "Agent Framework / shim to use Pydantic with LLMs"
9-
authors = [
10-
{ name = "Samuel Colvin", email = "[email protected]" },
11-
]
9+
authors = [{ name = "Samuel Colvin", email = "[email protected]" }]
1210
license = "MIT"
1311
readme = "README.md"
1412
classifiers = [
@@ -33,7 +31,9 @@ classifiers = [
3331
]
3432
requires-python = ">=3.9"
3533

36-
dependencies = ["pydantic-ai-slim[openai,vertexai,groq,anthropic,mistral,cohere]==0.0.27"]
34+
dependencies = [
35+
"pydantic-ai-slim[openai,vertexai,groq,anthropic,mistral,cohere]==0.0.27",
36+
]
3737

3838
[project.urls]
3939
Homepage = "https://ai.pydantic.dev"
@@ -55,11 +55,7 @@ members = ["pydantic_ai_slim", "pydantic_graph", "examples"]
5555

5656
[dependency-groups]
5757
# dev dependencies are defined in `pydantic-ai-slim/pyproject.toml` to allow for minimal testing
58-
lint = [
59-
"mypy>=1.11.2",
60-
"pyright>=1.1.388,<1.1.390",
61-
"ruff>=0.6.9",
62-
]
58+
lint = ["mypy>=1.11.2", "pyright>=1.1.388,<1.1.390", "ruff>=0.6.9"]
6359
docs = [
6460
"black>=24.10.0",
6561
"bs4>=0.0.2",
@@ -69,10 +65,7 @@ docs = [
6965
"mkdocs-material[imaging]>=9.5.45",
7066
"mkdocstrings-python>=1.12.2",
7167
]
72-
docs-upload = [
73-
"algoliasearch>=4.12.0",
74-
"pydantic>=2.10.1",
75-
]
68+
docs-upload = ["algoliasearch>=4.12.0", "pydantic>=2.10.1"]
7669

7770
[tool.hatch.build.targets.wheel]
7871
only-include = ["/README.md"]
@@ -92,14 +85,7 @@ include = [
9285
]
9386

9487
[tool.ruff.lint]
95-
extend-select = [
96-
"Q",
97-
"RUF100",
98-
"C90",
99-
"UP",
100-
"I",
101-
"D",
102-
]
88+
extend-select = ["Q", "RUF100", "C90", "UP", "I", "D"]
10389
flake8-quotes = { inline-quotes = "single", multiline-quotes = "double" }
10490
isort = { combine-as-imports = true, known-first-party = ["pydantic_ai"] }
10591
mccabe = { max-complexity = 15 }
@@ -133,9 +119,7 @@ include = ["pydantic_ai_slim", "pydantic_graph", "tests", "examples"]
133119
venvPath = ".venv"
134120
# see https://github.com/microsoft/pyright/issues/7771 - we don't want to error on decorated functions in tests
135121
# which are not otherwise used
136-
executionEnvironments = [
137-
{ root = "tests", reportUnusedFunction = false },
138-
]
122+
executionEnvironments = [{ root = "tests", reportUnusedFunction = false }]
139123
exclude = ["examples/pydantic_ai_examples/weather_agent_gradio.py"]
140124

141125
[tool.mypy]
@@ -145,14 +129,16 @@ strict = true
145129
[tool.pytest.ini_options]
146130
testpaths = "tests"
147131
xfail_strict = true
148-
filterwarnings = [
149-
"error",
150-
]
132+
filterwarnings = ["error"]
151133

152134
# https://coverage.readthedocs.io/en/latest/config.html#run
153135
[tool.coverage.run]
154136
# required to avoid warnings about files created by create_module fixture
155-
include = ["pydantic_ai_slim/**/*.py", "pydantic_graph/**/*.py","tests/**/*.py"]
137+
include = [
138+
"pydantic_ai_slim/**/*.py",
139+
"pydantic_graph/**/*.py",
140+
"tests/**/*.py",
141+
]
156142
omit = ["tests/test_live.py", "tests/example_modules/*.py"]
157143
branch = true
158144

@@ -180,8 +166,8 @@ exclude_lines = [
180166
ignore_no_config = true
181167

182168
[tool.inline-snapshot.shortcuts]
183-
snap-fix=["create", "fix"]
184-
snap=["create"]
169+
snap-fix = ["create", "fix"]
170+
snap = ["create"]
185171

186172
[tool.codespell]
187173
# Ref: https://github.com/codespell-project/codespell#using-a-config-file

0 commit comments

Comments
 (0)