Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
20 changes: 18 additions & 2 deletions docs/builtin-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,9 +315,9 @@ allowing it to pull up-to-date information from the web.

| Provider | Supported | Notes |
|----------|-----------|-------|
| Anthropic | ✅ | Full feature support. Uses Anthropic's [Web Fetch Tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-fetch-tool) internally to retrieve URL contents. |
Copy link
Collaborator

Choose a reason for hiding this comment

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

To answer your question in the comment: let's rename the entire thing to WebFetchTool, and keep UrlContextTool around only as an alias with a deprecation warning.

| Google | ✅ | No [`BuiltinToolCallPart`][pydantic_ai.messages.BuiltinToolCallPart] or [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] is currently generated; please submit an issue if you need this. Using built-in tools and function tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
| OpenAI | ❌ | |
| Anthropic | ❌ | |
| Groq | ❌ | |
| Bedrock | ❌ | |
| Mistral | ❌ | |
Expand All @@ -327,7 +327,23 @@ allowing it to pull up-to-date information from the web.

### Usage

```py {title="url_context_basic.py"}
```py {title="url_context_anthropic.py"}
from pydantic_ai import Agent, UrlContextTool

agent = Agent('anthropic:claude-sonnet-4-0', builtin_tools=[UrlContextTool()])

result = agent.run_sync('What is the first sentence on https://ai.pydantic.dev?')
print(result.output)
"""
Pydantic AI is a Python agent framework designed to make it less painful to build production grade applications with Generative AI.
"""
```

_(This example is complete, it can be run "as is")_

With Google, you can also use `UrlContextTool`:
Copy link
Collaborator

Choose a reason for hiding this comment

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

We just need one example as the only difference is the model name.

Per the above, let's not mention UrlContextTool anymore.

Should we support any of the options on https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-fetch-tool#tool-definition? If so, that'd warrant a new section and Anthropic-specific example. But ideally Google would also support at least some of those.

Copy link
Author

Choose a reason for hiding this comment

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

Good idea! I'll look into adding those options


```py {title="url_context_google.py"}
from pydantic_ai import Agent, UrlContextTool

agent = Agent('google-gla:gemini-2.5-flash', builtin_tools=[UrlContextTool()])
Expand Down
1 change: 1 addition & 0 deletions pydantic_ai_slim/pydantic_ai/builtin_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ class UrlContextTool(AbstractBuiltinTool):

Supported by:

* Anthropic
* Google
"""

Expand Down
59 changes: 57 additions & 2 deletions pydantic_ai_slim/pydantic_ai/models/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
from .._run_context import RunContext
from .._utils import guard_tool_call_id as _guard_tool_call_id
from ..builtin_tools import CodeExecutionTool, MCPServerTool, MemoryTool, WebSearchTool
from ..builtin_tools import CodeExecutionTool, MCPServerTool, MemoryTool, UrlContextTool, WebSearchTool
from ..exceptions import UserError
from ..messages import (
BinaryContent,
Expand Down Expand Up @@ -106,12 +106,18 @@
BetaToolUnionParam,
BetaToolUseBlock,
BetaToolUseBlockParam,
BetaWebFetchTool20250910Param,
BetaWebFetchToolResultBlock,
BetaWebFetchToolResultBlockParam,
BetaWebSearchTool20250305Param,
BetaWebSearchToolResultBlock,
BetaWebSearchToolResultBlockContent,
BetaWebSearchToolResultBlockParam,
BetaWebSearchToolResultBlockParamContentParam,
)
from anthropic.types.beta.beta_web_fetch_tool_result_block_param import (
Content as WebFetchToolResultBlockParamContent,
)
from anthropic.types.beta.beta_web_search_tool_20250305_param import UserLocation
from anthropic.types.model_param import ModelParam

Expand Down Expand Up @@ -371,6 +377,8 @@ def _process_response(self, response: BetaMessage) -> ModelResponse:
items.append(_map_web_search_tool_result_block(item, self.system))
elif isinstance(item, BetaCodeExecutionToolResultBlock):
items.append(_map_code_execution_tool_result_block(item, self.system))
elif isinstance(item, BetaWebFetchToolResultBlock):
items.append(_map_web_fetch_tool_result_block(item, self.system))
elif isinstance(item, BetaRedactedThinkingBlock):
items.append(
ThinkingPart(id='redacted_thinking', content='', signature=item.data, provider_name=self.system)
Expand Down Expand Up @@ -464,6 +472,9 @@ def _add_builtin_tools(
elif isinstance(tool, CodeExecutionTool): # pragma: no branch
tools.append(BetaCodeExecutionTool20250522Param(name='code_execution', type='code_execution_20250522'))
beta_features.append('code-execution-2025-05-22')
elif isinstance(tool, UrlContextTool): # pragma: no branch
tools.append(BetaWebFetchTool20250910Param(name='web_fetch', type='web_fetch_20250910'))
beta_features.append('web-fetch-2025-09-10')
elif isinstance(tool, MemoryTool): # pragma: no branch
if 'memory' not in model_request_parameters.tool_defs:
raise UserError("Built-in `MemoryTool` requires a 'memory' tool to be defined.")
Expand Down Expand Up @@ -542,6 +553,7 @@ async def _map_message( # noqa: C901
| BetaServerToolUseBlockParam
| BetaWebSearchToolResultBlockParam
| BetaCodeExecutionToolResultBlockParam
| BetaWebFetchToolResultBlockParam
| BetaThinkingBlockParam
| BetaRedactedThinkingBlockParam
| BetaMCPToolUseBlockParam
Expand Down Expand Up @@ -604,6 +616,14 @@ async def _map_message( # noqa: C901
input=response_part.args_as_dict(),
)
assistant_content_params.append(server_tool_use_block_param)
elif response_part.tool_name == UrlContextTool.kind:
server_tool_use_block_param = BetaServerToolUseBlockParam(
id=tool_use_id,
type='server_tool_use',
name='web_fetch',
input=response_part.args_as_dict(),
)
assistant_content_params.append(server_tool_use_block_param)
elif (
response_part.tool_name.startswith(MCPServerTool.kind)
and (server_id := response_part.tool_name.split(':', 1)[1])
Expand Down Expand Up @@ -650,6 +670,19 @@ async def _map_message( # noqa: C901
),
)
)
elif response_part.tool_name == UrlContextTool.kind and isinstance(
response_part.content, dict
):
assistant_content_params.append(
BetaWebFetchToolResultBlockParam(
tool_use_id=tool_use_id,
type='web_fetch_tool_result',
content=cast(
WebFetchToolResultBlockParamContent,
response_part.content, # pyright: ignore[reportUnknownMemberType]
),
)
)
elif response_part.tool_name.startswith(MCPServerTool.kind) and isinstance(
response_part.content, dict
): # pragma: no branch
Expand Down Expand Up @@ -866,6 +899,11 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
vendor_part_id=event.index,
part=_map_code_execution_tool_result_block(current_block, self.provider_name),
)
elif isinstance(current_block, BetaWebFetchToolResultBlock): # pragma: lax no cover
yield self._parts_manager.handle_part(
vendor_part_id=event.index,
part=_map_web_fetch_tool_result_block(current_block, self.provider_name),
)
elif isinstance(current_block, BetaMCPToolUseBlock):
call_part = _map_mcp_server_use_block(current_block, self.provider_name)
builtin_tool_calls[call_part.tool_call_id] = call_part
Expand Down Expand Up @@ -972,7 +1010,14 @@ def _map_server_tool_use_block(item: BetaServerToolUseBlock, provider_name: str)
args=cast(dict[str, Any], item.input) or None,
tool_call_id=item.id,
)
elif item.name in ('web_fetch', 'bash_code_execution', 'text_editor_code_execution'): # pragma: no cover
elif item.name == 'web_fetch':
return BuiltinToolCallPart(
provider_name=provider_name,
tool_name=UrlContextTool.kind,
args=cast(dict[str, Any], item.input) or None,
tool_call_id=item.id,
)
elif item.name in ('bash_code_execution', 'text_editor_code_execution'): # pragma: no cover
raise NotImplementedError(f'Anthropic built-in tool {item.name!r} is not currently supported.')
else:
assert_never(item.name)
Expand Down Expand Up @@ -1008,6 +1053,16 @@ def _map_code_execution_tool_result_block(
)


def _map_web_fetch_tool_result_block(item: BetaWebFetchToolResultBlock, provider_name: str) -> BuiltinToolReturnPart:
return BuiltinToolReturnPart(
provider_name=provider_name,
tool_name=UrlContextTool.kind,
# Store just the content field (BetaWebFetchBlock) which has {content, type, url, retrieved_at}
content=item.content.model_dump(mode='json'),
tool_call_id=item.tool_use_id,
)


def _map_mcp_server_use_block(item: BetaMCPToolUseBlock, provider_name: str) -> BuiltinToolCallPart:
return BuiltinToolCallPart(
provider_name=provider_name,
Expand Down
Loading