Skip to content
Merged
Show file tree
Hide file tree
Changes from 86 commits
Commits
Show all changes
92 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
13bc865
Merge branch 'main' into builtin-tool
Kludex Jul 16, 2025
c0fc35c
Support WebSearch streaming for OpenAI
Kludex Jul 16, 2025
b4b3752
Handle Anthropic streaming
Kludex Jul 16, 2025
3a2481a
Drop extra from dependencies on test
Kludex Jul 16, 2025
306514e
move import up
Kludex Jul 16, 2025
c84be82
add coverage-n
Kludex Jul 16, 2025
a77030b
add coverage-n
Kludex Jul 16, 2025
93e26d6
drop pragma
Kludex Jul 16, 2025
bfb4eef
Update pydantic_ai_slim/pydantic_ai/builtin_tools.py
Kludex Jul 16, 2025
4376fc2
Apply suggestions from code review
mattbrandman Jul 18, 2025
c029d4b
Merge branch 'main' into builtin-tool
mattbrandman Jul 21, 2025
87f59f1
updates types
mattbrandman Jul 21, 2025
3fca6d4
updating types
mattbrandman Jul 21, 2025
37a1845
fixing event
mattbrandman Jul 21, 2025
5b031d6
Merge branch 'main' into builtin-tool
mattbrandman Jul 21, 2025
e1085fa
updates
mattbrandman Jul 21, 2025
ae1d4d9
Merge branch 'main' into builtin-tool
mattbrandman Jul 21, 2025
3ee4aa9
Merge branch 'main' into builtin-tool
mattbrandman Jul 21, 2025
9d24f5b
Merge branch 'main' into builtin-tool
mattbrandman Jul 21, 2025
07f7892
Merge branch 'main' into builtin-tool
mattbrandman Jul 22, 2025
7d867ee
Merge branch 'main' into builtin-tool
mattbrandman Jul 23, 2025
ba3f6b2
Update pydantic_ai_slim/pydantic_ai/agent.py
Kludex Jul 23, 2025
1ae6155
addressing comments
mattbrandman Jul 26, 2025
3f8cc2d
Merge branch 'main' into builtin-tool
mattbrandman Jul 26, 2025
ba962a7
updates
mattbrandman Jul 26, 2025
bfd9b21
fixing lint
mattbrandman Jul 26, 2025
e2fef42
fixing lint
mattbrandman Jul 26, 2025
b1ef2f8
fix misspellings
mattbrandman Jul 26, 2025
e9cacc3
rename servertool to builtin tool everywhere
mattbrandman Jul 26, 2025
aa91640
fixing ordering
mattbrandman Jul 28, 2025
2d451a7
Merge branch 'main' into builtin-tool
mattbrandman Jul 28, 2025
38b5b7d
addressing comments
mattbrandman Aug 6, 2025
359b9e1
Merge upstream/main into builtin-tool branch - resolved conflicts
mattbrandman Aug 6, 2025
bd6e5f7
Fix failing tests after merge - update snapshots for builtin tools
mattbrandman Aug 6, 2025
69267ca
fixing types
mattbrandman Aug 6, 2025
1a4a0f3
Merge remote-tracking branch 'origin/main' into builtin-tool
Kludex Aug 7, 2025
c9d0d18
Remove deprecated stuff from yesterday
Kludex Aug 7, 2025
a631229
Merge branch 'main' into builtin-tool
Kludex Aug 7, 2025
78fe612
Simplify cohere
Kludex Aug 7, 2025
a673c90
refactor a bit
Kludex Aug 7, 2025
df2ad47
Handle Google builtin tools
Kludex Aug 7, 2025
b4add90
Add test for unsupported builtin tools
Kludex Aug 7, 2025
dec1ba3
Add 100% coverage
Kludex Aug 7, 2025
e134d43
fix linting issue
Kludex Aug 7, 2025
ef4ab6a
Add API documentation for builtin tools
Kludex Aug 7, 2025
bb98ccf
Add documentation
Kludex Aug 7, 2025
739f142
pass docs tests
Kludex Aug 7, 2025
cc7ab39
Add Groq model profile for compound models that always have web searc…
DouweM Aug 7, 2025
5534bf4
add pragma
Kludex Aug 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ node_modules/
**.idea/
.coverage*
/test_tmp/
.mcp.json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For curiosity, why MCP servers did you use here?

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 @@ -16,6 +16,7 @@
from pydantic_ai._function_schema import _takes_ctx as is_takes_ctx # type: ignore
from pydantic_ai._tool_manager import ToolManager
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 @@ -112,6 +113,7 @@ class GraphAgentDeps(Generic[DepsT, OutputDataT]):

history_processors: Sequence[HistoryProcessor[DepsT]]

builtin_tools: list[AbstractBuiltinTool] = dataclasses.field(repr=False)
tool_manager: ToolManager[DepsT]

tracer: Tracer
Expand Down Expand Up @@ -269,6 +271,7 @@ async def _prepare_request_parameters(

return models.ModelRequestParameters(
function_tools=function_tools,
builtin_tools=ctx.deps.builtin_tools,
output_mode=output_schema.mode,
output_tools=output_tools,
output_object=output_object,
Expand Down Expand Up @@ -443,6 +446,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.BuiltinToolCallPart):
yield _messages.BuiltinToolCallEvent(part)
elif isinstance(part, _messages.BuiltinToolReturnPart):
yield _messages.BuiltinToolResultEvent(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 @@ -227,7 +227,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.BuiltinToolCallPart
| _messages.BuiltinToolReturnPart,
) -> 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
9 changes: 9 additions & 0 deletions pydantic_ai_slim/pydantic_ai/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from pydantic.json_schema import GenerateJsonSchema
from typing_extensions import Literal, Never, Self, TypeIs, TypeVar, deprecated

from pydantic_ai.builtin_tools import AbstractBuiltinTool
from pydantic_graph import End, Graph, GraphRun, GraphRunContext
from pydantic_graph._utils import get_event_loop

Expand Down Expand Up @@ -188,6 +189,7 @@ def __init__(
retries: int = 1,
output_retries: int | None = None,
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (),
builtin_tools: Sequence[AbstractBuiltinTool] = (),
prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
prepare_output_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
Expand Down Expand Up @@ -215,6 +217,7 @@ def __init__(
retries: int = 1,
output_retries: int | None = None,
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (),
builtin_tools: Sequence[AbstractBuiltinTool] = (),
prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
prepare_output_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
mcp_servers: Sequence[MCPServer] = (),
Expand All @@ -240,6 +243,7 @@ def __init__(
retries: int = 1,
output_retries: int | None = None,
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (),
builtin_tools: Sequence[AbstractBuiltinTool] = (),
prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
prepare_output_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
Expand Down Expand Up @@ -271,6 +275,8 @@ def __init__(
output_retries: The maximum number of retries to allow for output 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. If the model doesn't support the builtin tools, an error will be raised.
prepare_tools: Custom function to prepare the tool definition of all tools for each step, except output tools.
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 @@ -340,6 +346,8 @@ def __init__(
self._system_prompt_dynamic_functions = {}

self._max_result_retries = output_retries if output_retries is not None else retries
self._builtin_tools = builtin_tools

self._prepare_tools = prepare_tools
self._prepare_output_tools = prepare_output_tools

Expand Down Expand Up @@ -700,6 +708,7 @@ async def get_instructions(run_context: RunContext[AgentDepsT]) -> str | None:
output_schema=output_schema,
output_validators=output_validators,
history_processors=self.history_processors,
builtin_tools=list(self._builtin_tools),
tool_manager=run_toolset,
tracer=tracer,
get_instructions=get_instructions,
Expand Down
105 changes: 105 additions & 0 deletions pydantic_ai_slim/pydantic_ai/builtin_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from __future__ import annotations as _annotations

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

from typing_extensions import TypedDict

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


@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`.
"""


@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.

Supported by:
* Anthropic
* OpenAI
* Groq
"""

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: WebSearchUserLocation | 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)
"""

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 WebSearchUserLocation(TypedDict, total=False):
"""Allows you to localize search results based on a user's location.

Supported by:
* Anthropic
* OpenAI
"""

city: str
"""The city where the user is located."""

country: str
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check doesn't seem necessary. It seems it works fine with 3 characters.

"""The country where the user is located. For OpenAI, this must be a 2-letter country code (e.g., 'US', 'GB')."""

region: str
"""The region or state where the user is located."""

timezone: str
"""The timezone of the user's location."""


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

Supported by:
* Anthropic
* OpenAI
* Google
"""
84 changes: 73 additions & 11 deletions pydantic_ai_slim/pydantic_ai/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,8 +532,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 @@ -550,9 +550,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 @@ -579,9 +576,32 @@ 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 BuiltinToolReturnPart(BaseToolReturnPart):
"""A tool return message from a built-in tool."""

provider_name: str | None = None
"""The name of the provider that generated the response."""

part_kind: Literal['builtin-tool-return'] = 'builtin-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 @@ -723,7 +743,7 @@ def has_content(self) -> bool:


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

tool_name: str
Expand All @@ -741,9 +761,6 @@ class ToolCallPart:
In case the tool call id is not provided by the model, Pydantic AI 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 @@ -780,7 +797,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 BuiltinToolCallPart(BaseToolCallPart):
"""A tool call to a built-in tool."""

provider_name: str | None = None
"""The name of the provider that generated the response."""

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


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


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


@dataclass(repr=False)
class BuiltinToolCallEvent:
"""An event indicating the start to a call to a built-in tool."""

part: BuiltinToolCallPart
"""The built-in tool call to make."""

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


@dataclass(repr=False)
class BuiltinToolResultEvent:
"""An event indicating the result of a built-in tool call."""

result: BuiltinToolReturnPart
"""The result of the call to the built-in tool."""

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


HandleResponseEvent = Annotated[
Union[FunctionToolCallEvent, FunctionToolResultEvent], pydantic.Discriminator('event_kind')
Union[FunctionToolCallEvent, FunctionToolResultEvent, BuiltinToolCallEvent, BuiltinToolResultEvent],
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 @@ -336,6 +337,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