Skip to content

Builtin tool #2102

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 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
57e568b
Add `builtin_tools` to `Agent`
Kludex May 14, 2025
97ab44b
make AbstractBuiltinTool serializable
Kludex May 14, 2025
e3dda9d
Add more work on it
Kludex May 14, 2025
3ad6d38
Merge remote-tracking branch 'origin/main' into add-builtin-tools
Kludex May 23, 2025
0b43f65
Add builtin tools
Kludex May 23, 2025
fa7fd11
merge
Kludex May 26, 2025
32324fa
add more built-in-tools
Kludex May 27, 2025
f33e568
Fix test
Kludex May 27, 2025
13d7433
Add support on Groq
Kludex May 27, 2025
ac85205
Add support for Google
Kludex May 28, 2025
c93633f
Add support for MCP's Streamable HTTP transport (#1716)
BrandonShar May 26, 2025
3a8b640
Timeout for initializing MCP client (#1833)
alexmojaki May 27, 2025
360de87
Require mcp 1.9.0+ (#1840)
DouweM May 27, 2025
cb4e539
Don't send empty messages to Anthropic (#1027)
oscar-broman May 27, 2025
4e3769a
Add `vendor_id` and `finish_reason` to Gemini/Google model responses …
davide-andreoli May 27, 2025
ebb536f
Fix units of `sse_read_timeout` `timedelta` (#1843)
alexmojaki May 27, 2025
c8bb611
Support functions as output_type, as well as lists of functions and o…
DouweM May 27, 2025
6bcc1a8
Enhance Gemini usage tracking to collect comprehensive token data (#1…
kiqaps May 28, 2025
97ff651
more
Kludex May 30, 2025
1d47e1e
merge
Kludex May 30, 2025
5f89444
merge
Kludex Jun 1, 2025
9512987
merge
Kludex Jun 20, 2025
800a71a
Pass tests
Kludex Jun 20, 2025
d0f4643
Merge main into builtin-tool branch and resolve conflicts
mattbrandman Jul 1, 2025
bc298d6
Fix remaining merge conflict markers in openai.py, anthropic.py, and …
mattbrandman Jul 1, 2025
46c06c2
add extra google
mattbrandman Jul 1, 2025
3496567
fix formatting
mattbrandman Jul 1, 2025
c193059
fix codespell
mattbrandman Jul 1, 2025
427dec2
fixing types
mattbrandman Jul 1, 2025
866ad21
fixing types in gemini
mattbrandman Jul 1, 2025
4c2622d
misspells are on purpose oops
mattbrandman Jul 1, 2025
c13736e
ignore misspellings
mattbrandman Jul 1, 2025
a42a75d
ignore misspellings
mattbrandman Jul 1, 2025
e2f1daa
fixing comment
mattbrandman Jul 1, 2025
3094a9a
fixing tests and coverage
mattbrandman Jul 1, 2025
6a3c987
fixing types
mattbrandman Jul 1, 2025
ac0edb6
Revert "ignore misspellings"
mattbrandman Jul 2, 2025
374e034
revert to known good
mattbrandman Jul 2, 2025
7cccdd2
merging main
mattbrandman Jul 2, 2025
2393c87
adding anthropic test coverage
mattbrandman Jul 2, 2025
21094a7
adding pragma no cover to has_content
mattbrandman Jul 2, 2025
8ac5294
Merge branch 'main' into builtin-tool
mattbrandman Jul 5, 2025
de8f32d
Merge branch 'main' into builtin-tool
mattbrandman Jul 7, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ jobs:
- run: mkdir coverage

# run tests with just `pydantic-ai-slim` dependencies
- run: uv run --package pydantic-ai-slim coverage run -m pytest
- run: uv run --package pydantic-ai-slim --extra google coverage run -m pytest
env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-slim

Expand Down
7 changes: 7 additions & 0 deletions pydantic_ai_slim/pydantic_ai/_agent_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from pydantic_ai._function_schema import _takes_ctx as is_takes_ctx # type: ignore
from pydantic_ai._utils import is_async_callable, run_in_executor
from pydantic_ai.builtin_tools import AbstractBuiltinTool
from pydantic_graph import BaseNode, Graph, GraphRunContext
from pydantic_graph.nodes import End, NodeRunEndT

Expand Down Expand Up @@ -109,6 +110,7 @@ class GraphAgentDeps(Generic[DepsT, OutputDataT]):
history_processors: Sequence[HistoryProcessor[DepsT]]

function_tools: dict[str, Tool[DepsT]] = dataclasses.field(repr=False)
builtin_tools: list[AbstractBuiltinTool] = dataclasses.field(repr=False)
mcp_servers: Sequence[MCPServer] = dataclasses.field(repr=False)
default_retries: int

Expand Down Expand Up @@ -301,6 +303,7 @@ async def add_mcp_server_tools(server: MCPServer) -> None:

return models.ModelRequestParameters(
function_tools=function_tool_defs,
builtin_tools=ctx.deps.builtin_tools,
output_mode=output_schema.mode,
output_tools=output_tools,
output_object=output_object,
Expand Down Expand Up @@ -475,6 +478,10 @@ async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]:
texts.append(part.content)
elif isinstance(part, _messages.ToolCallPart):
tool_calls.append(part)
elif isinstance(part, _messages.ServerToolCallPart):
yield _messages.ServerToolCallEvent(part)
elif isinstance(part, _messages.ServerToolReturnPart):
yield _messages.ServerToolResultEvent(part)
elif isinstance(part, _messages.ThinkingPart):
# We don't need to do anything with thinking parts in this tool-calling node.
# We need to handle text parts in case there are no tool calls and/or the desired output comes
Expand Down
8 changes: 7 additions & 1 deletion pydantic_ai_slim/pydantic_ai/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,13 @@ def now_utc() -> datetime:
return datetime.now(tz=timezone.utc)


def guard_tool_call_id(t: _messages.ToolCallPart | _messages.ToolReturnPart | _messages.RetryPromptPart) -> str:
def guard_tool_call_id(
t: _messages.ToolCallPart
| _messages.ToolReturnPart
| _messages.RetryPromptPart
| _messages.ServerToolCallPart
| _messages.ServerToolReturnPart,
) -> str:
"""Type guard that either returns the tool call id or generates a new one if it's None."""
return t.tool_call_id or generate_tool_call_id()

Expand Down
15 changes: 15 additions & 0 deletions pydantic_ai_slim/pydantic_ai/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from pydantic.json_schema import GenerateJsonSchema
from typing_extensions import Literal, Never, Self, TypeIs, TypeVar, deprecated

from pydantic_ai.builtin_tools import AbstractBuiltinTool, WebSearchTool
from pydantic_ai.profiles import ModelProfile
from pydantic_graph import End, Graph, GraphRun, GraphRunContext
from pydantic_graph._utils import get_event_loop
Expand Down Expand Up @@ -176,6 +177,7 @@ def __init__(
retries: int = 1,
output_retries: int | None = None,
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (),
builtin_tools: Sequence[Literal['web-search'] | AbstractBuiltinTool] = (),
prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
mcp_servers: Sequence[MCPServer] = (),
defer_model_check: bool = False,
Expand Down Expand Up @@ -206,6 +208,7 @@ def __init__(
result_tool_description: str | None = None,
result_retries: int | None = None,
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (),
builtin_tools: Sequence[Literal['web-search'] | AbstractBuiltinTool] = (),
prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
mcp_servers: Sequence[MCPServer] = (),
defer_model_check: bool = False,
Expand All @@ -231,6 +234,7 @@ def __init__(
retries: int = 1,
output_retries: int | None = None,
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (),
builtin_tools: Sequence[Literal['web-search'] | AbstractBuiltinTool] = (),
prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
mcp_servers: Sequence[MCPServer] = (),
defer_model_check: bool = False,
Expand Down Expand Up @@ -261,6 +265,8 @@ def __init__(
output_retries: The maximum number of retries to allow for result validation, defaults to `retries`.
tools: Tools to register with the agent, you can also register tools via the decorators
[`@agent.tool`][pydantic_ai.Agent.tool] and [`@agent.tool_plain`][pydantic_ai.Agent.tool_plain].
builtin_tools: The builtin tools that the agent will use. This depends on the model, as some models may not
support certain tools. On models that don't support certain tools, the tool will be ignored.
prepare_tools: custom method to prepare the tool definition of all tools for each step.
This is useful if you want to customize the definition of multiple tools or you want to register
a subset of tools for a given step. See [`ToolsPrepareFunc`][pydantic_ai.tools.ToolsPrepareFunc]
Expand Down Expand Up @@ -358,6 +364,14 @@ def __init__(
self._default_retries = retries
self._max_result_retries = output_retries if output_retries is not None else retries
self._mcp_servers = mcp_servers
self._builtin_tools: list[AbstractBuiltinTool] = []

for tool in builtin_tools:
if tool == 'web-search':
self._builtin_tools.append(WebSearchTool())
else:
self._builtin_tools.append(tool)

self._prepare_tools = prepare_tools
self.history_processors = history_processors or []
for tool in tools:
Expand Down Expand Up @@ -722,6 +736,7 @@ async def get_instructions(run_context: RunContext[AgentDepsT]) -> str | None:
output_validators=output_validators,
history_processors=self.history_processors,
function_tools=run_function_tools,
builtin_tools=self._builtin_tools,
mcp_servers=self._mcp_servers,
default_retries=self._default_retries,
tracer=tracer,
Expand Down
95 changes: 95 additions & 0 deletions pydantic_ai_slim/pydantic_ai/builtin_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from __future__ import annotations as _annotations

from abc import ABC
from dataclasses import dataclass
from typing import Any, Literal

from typing_extensions import TypedDict

__all__ = ('AbstractBuiltinTool', 'WebSearchTool', 'UserLocation')


@dataclass
class AbstractBuiltinTool(ABC):
"""A builtin tool that can be used by an agent.

This class is abstract and cannot be instantiated directly.

The builtin tools are passed to the model as part of the `ModelRequestParameters`.
"""

def handle_custom_tool_definition(self, model: str) -> Any: ...


@dataclass
class WebSearchTool(AbstractBuiltinTool):
"""A builtin tool that allows your agent to search the web for information.

The parameters that PydanticAI passes depend on the model, as some parameters may not be supported by certain models.
"""

search_context_size: Literal['low', 'medium', 'high'] = 'medium'
"""The `search_context_size` parameter controls how much context is retrieved from the web to help the tool formulate a response.

Supported by:
* OpenAI
"""

user_location: UserLocation | None = None
"""The `user_location` parameter allows you to localize search results based on a user's location.

Supported by:
* Anthropic
* OpenAI
"""

blocked_domains: list[str] | None = None
"""If provided, these domains will never appear in results.

With Anthropic, you can only use one of `blocked_domains` or `allowed_domains`, not both.

Supported by:
* Anthropic (https://docs.anthropic.com/en/docs/build-with-claude/tool-use/web-search-tool#domain-filtering)
* Groq (https://console.groq.com/docs/agentic-tooling#search-settings)
* MistralAI
"""

allowed_domains: list[str] | None = None
"""If provided, only these domains will be included in results.

With Anthropic, you can only use one of `blocked_domains` or `allowed_domains`, not both.

Supported by:
* Anthropic (https://docs.anthropic.com/en/docs/build-with-claude/tool-use/web-search-tool#domain-filtering)
* Groq (https://console.groq.com/docs/agentic-tooling#search-settings)
"""

max_uses: int | None = None
"""If provided, the tool will stop searching the web after the given number of uses.

Supported by:
* Anthropic
"""


class UserLocation(TypedDict, total=False):
"""Allows you to localize search results based on a user's location.

Supported by:
* Anthropic
* OpenAI
"""

city: str
country: str
region: str
timezone: str


class CodeExecutionTool(AbstractBuiltinTool):
"""A builtin tool that allows your agent to execute code.

Supported by:
* Anthropic
* OpenAI
"""
81 changes: 70 additions & 11 deletions pydantic_ai_slim/pydantic_ai/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,8 +407,8 @@ def otel_event(self, settings: InstrumentationSettings) -> Event:


@dataclass(repr=False)
class ToolReturnPart:
"""A tool return message, this encodes the result of running a tool."""
class BaseToolReturnPart:
"""Base class for tool return parts."""

tool_name: str
"""The name of the "tool" was called."""
Expand All @@ -425,9 +425,6 @@ class ToolReturnPart:
timestamp: datetime = field(default_factory=_now_utc)
"""The timestamp, when the tool returned."""

part_kind: Literal['tool-return'] = 'tool-return'
"""Part type identifier, this is available on all parts as a discriminator."""

def model_response_str(self) -> str:
"""Return a string representation of the content for the model."""
if isinstance(self.content, str):
Expand All @@ -454,9 +451,29 @@ def otel_event(self, settings: InstrumentationSettings) -> Event:
},
)

def has_content(self) -> bool:
"""Return `True` if the tool return has content."""
return self.content is not None # pragma: no cover

__repr__ = _utils.dataclasses_no_defaults_repr


@dataclass(repr=False)
class ToolReturnPart(BaseToolReturnPart):
"""A tool return message, this encodes the result of running a tool."""

part_kind: Literal['tool-return'] = 'tool-return'
"""Part type identifier, this is available on all parts as a discriminator."""


@dataclass(repr=False)
class ServerToolReturnPart(BaseToolReturnPart):
"""A tool return message from a server tool."""

part_kind: Literal['server-tool-return'] = 'server-tool-return'
"""Part type identifier, this is available on all parts as a discriminator."""


error_details_ta = pydantic.TypeAdapter(list[pydantic_core.ErrorDetails], config=pydantic.ConfigDict(defer_build=True))


Expand Down Expand Up @@ -598,7 +615,7 @@ def has_content(self) -> bool:


@dataclass(repr=False)
class ToolCallPart:
class BaseToolCallPart:
"""A tool call from a model."""

tool_name: str
Expand All @@ -616,9 +633,6 @@ class ToolCallPart:
In case the tool call id is not provided by the model, PydanticAI will generate a random one.
"""

part_kind: Literal['tool-call'] = 'tool-call'
"""Part type identifier, this is available on all parts as a discriminator."""

def args_as_dict(self) -> dict[str, Any]:
"""Return the arguments as a Python dictionary.

Expand Down Expand Up @@ -655,7 +669,29 @@ def has_content(self) -> bool:
__repr__ = _utils.dataclasses_no_defaults_repr


ModelResponsePart = Annotated[Union[TextPart, ToolCallPart, ThinkingPart], pydantic.Discriminator('part_kind')]
@dataclass(repr=False)
class ToolCallPart(BaseToolCallPart):
"""A tool call from a model."""

part_kind: Literal['tool-call'] = 'tool-call'
"""Part type identifier, this is available on all parts as a discriminator."""


@dataclass(repr=False)
class ServerToolCallPart(BaseToolCallPart):
"""A tool call from a server tool."""

model_name: str | None = None
"""The name of the model that generated the response."""

part_kind: Literal['server-tool-call'] = 'server-tool-call'
"""Part type identifier, this is available on all parts as a discriminator."""


ModelResponsePart = Annotated[
Union[TextPart, ToolCallPart, ServerToolCallPart, ServerToolReturnPart, ThinkingPart],
pydantic.Discriminator('part_kind'),
]
"""A message part returned by a model."""


Expand Down Expand Up @@ -1042,6 +1078,29 @@ def tool_call_id(self) -> str:
__repr__ = _utils.dataclasses_no_defaults_repr


@dataclass(repr=False)
class ServerToolCallEvent:
"""An event indicating the start to a call to a server tool."""

part: ServerToolCallPart
"""The server tool call to make."""

event_kind: Literal['server_tool_call'] = 'server_tool_call'
"""Event type identifier, used as a discriminator."""


@dataclass(repr=False)
class ServerToolResultEvent:
"""An event indicating the result of a server tool call."""

result: ServerToolReturnPart
"""The result of the call to the server tool."""

event_kind: Literal['server_tool_result'] = 'server_tool_result'
"""Event type identifier, used as a discriminator."""


HandleResponseEvent = Annotated[
Union[FunctionToolCallEvent, FunctionToolResultEvent], pydantic.Discriminator('event_kind')
Union[FunctionToolCallEvent, FunctionToolResultEvent, ServerToolCallEvent, ServerToolResultEvent],
pydantic.Discriminator('event_kind'),
]
2 changes: 2 additions & 0 deletions pydantic_ai_slim/pydantic_ai/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import httpx
from typing_extensions import Literal, TypeAliasType, TypedDict

from pydantic_ai.builtin_tools import AbstractBuiltinTool
from pydantic_ai.profiles import DEFAULT_PROFILE, ModelProfile, ModelProfileSpec

from .. import _utils
Expand Down Expand Up @@ -308,6 +309,7 @@ class ModelRequestParameters:
"""Configuration for an agent's request to a model, specifically related to tools and output handling."""

function_tools: list[ToolDefinition] = field(default_factory=list)
builtin_tools: list[AbstractBuiltinTool] = field(default_factory=list)

output_mode: OutputMode = 'text'
output_object: OutputObjectDefinition | None = None
Expand Down
Loading
Loading