diff --git a/docs/builtin-tools.md b/docs/builtin-tools.md index 1e4bdfd279..1add8f899c 100644 --- a/docs/builtin-tools.md +++ b/docs/builtin-tools.md @@ -9,7 +9,7 @@ Pydantic AI supports the following built-in tools: - **[`WebSearchTool`][pydantic_ai.builtin_tools.WebSearchTool]**: Allows agents to search the web - **[`CodeExecutionTool`][pydantic_ai.builtin_tools.CodeExecutionTool]**: Enables agents to execute code in a secure environment - **[`ImageGenerationTool`][pydantic_ai.builtin_tools.ImageGenerationTool]**: Enables agents to generate images -- **[`UrlContextTool`][pydantic_ai.builtin_tools.UrlContextTool]**: Enables agents to pull URL contents into their context +- **[`WebFetchTool`][pydantic_ai.builtin_tools.WebFetchTool]**: Enables agents to fetch web pages - **[`MemoryTool`][pydantic_ai.builtin_tools.MemoryTool]**: Enables agents to use memory - **[`MCPServerTool`][pydantic_ai.builtin_tools.MCPServerTool]**: Enables agents to use remote MCP servers with communication handled by the model provider @@ -306,18 +306,18 @@ For more details, check the [API documentation][pydantic_ai.builtin_tools.ImageG | `quality` | ✅ | ❌ | | `size` | ✅ | ❌ | -## URL Context Tool +## Web Fetch Tool -The [`UrlContextTool`][pydantic_ai.builtin_tools.UrlContextTool] enables your agent to pull URL contents into its context, +The [`WebFetchTool`][pydantic_ai.builtin_tools.WebFetchTool] enables your agent to pull URL contents into its context, allowing it to pull up-to-date information from the web. ### Provider Support | 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. | | 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 | ❌ | | @@ -327,10 +327,10 @@ allowing it to pull up-to-date information from the web. ### Usage -```py {title="url_context_basic.py"} -from pydantic_ai import Agent, UrlContextTool +```py {title="web_fetch_basic.py"} +from pydantic_ai import Agent, WebFetchTool -agent = Agent('google-gla:gemini-2.5-flash', builtin_tools=[UrlContextTool()]) +agent = Agent('google-gla:gemini-2.5-flash', builtin_tools=[WebFetchTool()]) result = agent.run_sync('What is this? https://ai.pydantic.dev') print(result.output) @@ -339,6 +339,48 @@ print(result.output) _(This example is complete, it can be run "as is")_ +### Parameters + +The [`WebFetchTool`][pydantic_ai.builtin_tools.WebFetchTool] supports several configuration parameters. The parameters that are actually used depend on the model provider. + +| Parameter | Type | Description | Supported by | +|-----------|------|-------------|--------------| +| `max_uses` | `int \| None` | Limit the number of URL fetches per request | Anthropic | +| `allowed_domains` | `list[str] \| None` | Only fetch from these domains | Anthropic | +| `blocked_domains` | `list[str] \| None` | Never fetch from these domains | Anthropic | +| `citations_enabled` | `bool` | Enable citations for fetched content | Anthropic | +| `max_content_tokens` | `int \| None` | Maximum content length in tokens | Anthropic | + +!!! note + With Anthropic, you can only use one of `blocked_domains` or `allowed_domains`, not both. + +!!! note + Google's URL context tool does not support any configuration parameters. The limits are fixed at 20 URLs per request with a maximum of 34MB per URL. + +Example with parameters (Anthropic only): + +```py {title="web_fetch_with_params.py"} +from pydantic_ai import Agent, WebFetchTool + +# Configure WebFetchTool with domain filtering and limits +web_fetch = WebFetchTool( + allowed_domains=['ai.pydantic.dev', 'docs.pydantic.dev'], + max_uses=10, + citations_enabled=True, + max_content_tokens=50000, +) + +agent = Agent('anthropic:claude-sonnet-4-0', builtin_tools=[web_fetch]) + +result = agent.run_sync( + 'Compare the documentation at https://ai.pydantic.dev and https://docs.pydantic.dev' +) +print(result.output) +""" +Both sites provide comprehensive documentation for Pydantic projects. ai.pydantic.dev focuses on PydanticAI, a framework for building AI agents, while docs.pydantic.dev covers Pydantic, the data validation library. They share similar documentation styles and both emphasize type safety and developer experience. +""" +``` + ## Memory Tool The [`MemoryTool`][pydantic_ai.builtin_tools.MemoryTool] enables your agent to use memory. diff --git a/pydantic_ai_slim/pydantic_ai/__init__.py b/pydantic_ai_slim/pydantic_ai/__init__.py index ec0137f856..5534f68dde 100644 --- a/pydantic_ai_slim/pydantic_ai/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/__init__.py @@ -14,7 +14,8 @@ ImageGenerationTool, MCPServerTool, MemoryTool, - UrlContextTool, + UrlContextTool, # pyright: ignore[reportDeprecated] + WebFetchTool, WebSearchTool, WebSearchUserLocation, ) @@ -214,6 +215,7 @@ # builtin_tools 'WebSearchTool', 'WebSearchUserLocation', + 'WebFetchTool', 'UrlContextTool', 'CodeExecutionTool', 'ImageGenerationTool', diff --git a/pydantic_ai_slim/pydantic_ai/builtin_tools.py b/pydantic_ai_slim/pydantic_ai/builtin_tools.py index 5559b3124a..e6d3e6ae97 100644 --- a/pydantic_ai_slim/pydantic_ai/builtin_tools.py +++ b/pydantic_ai_slim/pydantic_ai/builtin_tools.py @@ -6,13 +6,14 @@ import pydantic from pydantic_core import core_schema -from typing_extensions import TypedDict +from typing_extensions import TypedDict, deprecated __all__ = ( 'AbstractBuiltinTool', 'WebSearchTool', 'WebSearchUserLocation', 'CodeExecutionTool', + 'WebFetchTool', 'UrlContextTool', 'ImageGenerationTool', 'MemoryTool', @@ -166,18 +167,79 @@ class CodeExecutionTool(AbstractBuiltinTool): @dataclass(kw_only=True) -class UrlContextTool(AbstractBuiltinTool): +class WebFetchTool(AbstractBuiltinTool): """Allows your agent to access contents from URLs. + The parameters that PydanticAI passes depend on the model, as some parameters may not be supported by certain models. + Supported by: + * Anthropic * Google """ - kind: str = 'url_context' + max_uses: int | None = None + """If provided, the tool will stop fetching URLs after the given number of uses. + + Supported by: + + * Anthropic + """ + + allowed_domains: list[str] | None = None + """If provided, only these domains will be fetched. + + With Anthropic, you can only use one of `blocked_domains` or `allowed_domains`, not both. + + Supported by: + + * Anthropic, see + """ + + blocked_domains: list[str] | None = None + """If provided, these domains will never be fetched. + + With Anthropic, you can only use one of `blocked_domains` or `allowed_domains`, not both. + + Supported by: + + * Anthropic, see + """ + + citations_enabled: bool = False + """If True, enables citations for fetched content. + + Supported by: + + * Anthropic + """ + + max_content_tokens: int | None = None + """Maximum content length in tokens for fetched content. + + Supported by: + + * Anthropic + """ + + kind: str = 'web_fetch' """The kind of tool.""" +@deprecated('Use `WebFetchTool` instead.') +class UrlContextTool(WebFetchTool): + """Deprecated alias for WebFetchTool. Use WebFetchTool instead.""" + + def __init_subclass__(cls, **kwargs: Any) -> None: + # Skip registration in _BUILTIN_TOOL_TYPES to avoid breaking the discriminated union + pass + + +# Remove UrlContextTool from _BUILTIN_TOOL_TYPES and restore WebFetchTool +# This ensures the discriminated union only includes WebFetchTool +_BUILTIN_TOOL_TYPES['url_context'] = WebFetchTool + + @dataclass(kw_only=True) class ImageGenerationTool(AbstractBuiltinTool): """A builtin tool that allows your agent to generate images. diff --git a/pydantic_ai_slim/pydantic_ai/models/anthropic.py b/pydantic_ai_slim/pydantic_ai/models/anthropic.py index de33a08f7a..dc4a0c238d 100644 --- a/pydantic_ai_slim/pydantic_ai/models/anthropic.py +++ b/pydantic_ai_slim/pydantic_ai/models/anthropic.py @@ -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, WebFetchTool, WebSearchTool from ..exceptions import UserError from ..messages import ( BinaryContent, @@ -60,6 +60,7 @@ BetaBase64PDFBlockParam, BetaBase64PDFSourceParam, BetaCacheControlEphemeralParam, + BetaCitationsConfigParam, BetaCitationsDelta, BetaCodeExecutionTool20250522Param, BetaCodeExecutionToolResultBlock, @@ -107,12 +108,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 @@ -412,6 +419,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) @@ -507,6 +516,20 @@ 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, WebFetchTool): # pragma: no branch + citations = BetaCitationsConfigParam(enabled=tool.citations_enabled) if tool.citations_enabled else None + tools.append( + BetaWebFetchTool20250910Param( + name='web_fetch', + type='web_fetch_20250910', + max_uses=tool.max_uses, + allowed_domains=tool.allowed_domains, + blocked_domains=tool.blocked_domains, + citations=citations, + max_content_tokens=tool.max_content_tokens, + ) + ) + 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.") @@ -616,6 +639,7 @@ async def _map_message( # noqa: C901 | BetaServerToolUseBlockParam | BetaWebSearchToolResultBlockParam | BetaCodeExecutionToolResultBlockParam + | BetaWebFetchToolResultBlockParam | BetaThinkingBlockParam | BetaRedactedThinkingBlockParam | BetaMCPToolUseBlockParam @@ -678,6 +702,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 == WebFetchTool.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]) @@ -724,6 +756,19 @@ async def _map_message( # noqa: C901 ), ) ) + elif response_part.tool_name == WebFetchTool.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 @@ -944,6 +989,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 @@ -1050,7 +1100,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=WebFetchTool.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) @@ -1086,6 +1143,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=WebFetchTool.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, diff --git a/pydantic_ai_slim/pydantic_ai/models/google.py b/pydantic_ai_slim/pydantic_ai/models/google.py index b3ee8731fa..81f407fbae 100644 --- a/pydantic_ai_slim/pydantic_ai/models/google.py +++ b/pydantic_ai_slim/pydantic_ai/models/google.py @@ -13,7 +13,7 @@ from .. import UnexpectedModelBehavior, _utils, usage from .._output import OutputObjectDefinition from .._run_context import RunContext -from ..builtin_tools import CodeExecutionTool, ImageGenerationTool, UrlContextTool, WebSearchTool +from ..builtin_tools import CodeExecutionTool, ImageGenerationTool, WebFetchTool, WebSearchTool from ..exceptions import ModelHTTPError, UserError from ..messages import ( BinaryContent, @@ -339,7 +339,7 @@ def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[T for tool in model_request_parameters.builtin_tools: if isinstance(tool, WebSearchTool): tools.append(ToolDict(google_search=GoogleSearchDict())) - elif isinstance(tool, UrlContextTool): + elif isinstance(tool, WebFetchTool): tools.append(ToolDict(url_context=UrlContextDict())) elif isinstance(tool, CodeExecutionTool): tools.append(ToolDict(code_execution=ToolCodeExecutionDict())) @@ -817,7 +817,7 @@ def _process_response_from_parts( ) -> ModelResponse: items: list[ModelResponsePart] = [] - # We don't currently turn `candidate.url_context_metadata` into BuiltinToolCallPart and BuiltinToolReturnPart for UrlContextTool. + # We don't currently turn `candidate.url_context_metadata` into BuiltinToolCallPart and BuiltinToolReturnPart for WebFetchTool. # Please file an issue if you need this. web_search_call, web_search_return = _map_grounding_metadata(grounding_metadata, provider_name) diff --git a/tests/models/cassettes/test_anthropic/test_anthropic_mcp_servers_stream.yaml b/tests/models/cassettes/test_anthropic/test_anthropic_mcp_servers_stream.yaml index b742ded9a1..bf03a340b2 100644 --- a/tests/models/cassettes/test_anthropic/test_anthropic_mcp_servers_stream.yaml +++ b/tests/models/cassettes/test_anthropic/test_anthropic_mcp_servers_stream.yaml @@ -126,7 +126,7 @@ interactions: data: {"type":"content_block_stop","index":1 } event: content_block_start - data: {"type":"content_block_start","index":2,"content_block":{"type":"mcp_tool_result","tool_use_id":"mcptoolu_01FZmJ5UspaX5BB9uU339UT1","is_error":false,"content":[{"type":"text","text":"This repository, `pydantic/pydantic-ai`, is a GenAI Agent Framework that leverages Pydantic for building Generative AI applications. Its main purpose is to provide a unified and type-safe way to interact with various large language models (LLMs) from different providers, manage agent execution flows, and integrate with external tools and services. \n\n## Main Features and Purpose\n\nThe `pydantic-ai` repository offers several core features:\n\n### 1. Agent System\nThe `Agent` class serves as the main orchestrator for managing interactions with LLMs and executing tasks. Agents can be configured with generic types for dependency injection (`Agent[AgentDepsT, OutputDataT]`) and output validation, ensuring type safety throughout the application. \n\nAgents support various execution methods:\n* `agent.run()`: An asynchronous function that returns a completed `RunResult`. \n* `agent.run_sync()`: A synchronous function that internally calls `run()` to return a completed `RunResult`. \n* `agent.run_stream()`: An asynchronous context manager for streaming text and structured output. \n* `agent.run_stream_events()`: Returns an asynchronous iterable of `AgentStreamEvent`s and a final `AgentRunResultEvent`. \n* `agent.iter()`: A context manager that provides an asynchronous iterable over the nodes of the agent's underlying `Graph`, allowing for deeper control and insight into the execution flow. \n\n### 2. Model Integration\nThe framework provides a unified interface for integrating with various LLM providers, including OpenAI, Anthropic, Google, Groq, Cohere, Mistral, Bedrock, and HuggingFace. Each model integration follows a consistent settings pattern with provider-specific prefixes (e.g., `google_*`, `anthropic_*`). \n\nExamples of supported models and their capabilities include:\n* `GoogleModel`: Integrates with Google's Gemini API, supporting both Gemini API (`google-gla`) and Vertex AI (`google-vertex`) providers. It supports token counting, streaming, built-in tools like `WebSearchTool`, `UrlContextTool`, `CodeExecutionTool`, and native JSON schema output. \n* `AnthropicModel`: Uses Anthropic's beta API for advanced features like \"Thinking Blocks\" and built-in tools. \n* `GroqModel`: Offers high-speed inference and specialized reasoning support with configurable reasoning formats. \n* `MistralModel`: Supports customizable JSON schema prompting and thinking support. \n* `BedrockConverseModel`: Utilizes AWS Bedrock's Converse API for unified access to various foundation models like Claude, Titan, Llama, and Mistral. \n* `CohereModel`: Integrates with Cohere's v2 API for chat completions, including thinking support and tool calling. \n\nThe framework also supports multimodal inputs such as `AudioUrl`, `DocumentUrl`, `ImageUrl`, and `VideoUrl`, allowing agents to process and respond to diverse content types. \n\n### 3. Graph-based Execution\nPydantic AI uses `pydantic-graph` to manage the execution flow of agents, representing it as a finite state machine. The execution typically flows through `UserPromptNode` → `ModelRequestNode` → `CallToolsNode`. This allows for detailed tracking of message history and usage. \n\n### 4. Tool System\nFunction tools enable models to perform actions and retrieve additional information. Tools can be registered using decorators like `@agent.tool` (for tools needing `RunContext` access) or `@agent.tool_plain` (for tools without `RunContext` access). The framework also supports toolsets for managing collections of tools. \n\nTools can return various types of output, including anything Pydantic can serialize to JSON, as well as multimodal content like `AudioUrl`, `VideoUrl`, `ImageUrl`, or `DocumentUrl`. The `ToolReturn` object allows for separating the `return_value` (for the model), `content` (for additional context), and `metadata` (for application-specific use). \n\nBuilt-in tools like `UrlContextTool` allow agents to pull web content into their context. \n\n### 5. Output Handling\nThe framework supports various output types:\n* `TextOutput`: Plain text responses. \n* `ToolOutput`: Structured data via tool calls. \n* `NativeOutput`: Provider-specific structured output. \n* `PromptedOutput`: Prompt-based structured extraction. \n\n### 6. Durable Execution\nPydantic AI integrates with durable execution systems like DBOS and Temporal. This allows agents to maintain state and resume execution after failures or restarts, making them suitable for long-running or fault-tolerant applications. \n\n### 7. Multi-Agent Patterns and Integrations\nThe repository supports multi-agent applications and various integrations, including:\n* Pydantic Evals: For evaluating agent performance. \n* Pydantic Graph: The underlying graph execution engine. \n* Logfire: For debugging and monitoring. \n* Agent-User Interaction (AG-UI) and Agent2Agent (A2A): For facilitating interactions between agents and users, and between agents themselves. \n* Clai: A CLI tool. \n\n## Purpose\n\nThe overarching purpose of `pydantic-ai` is to simplify the development of robust and reliable Generative AI applications by providing a structured, type-safe, and extensible framework. It aims to abstract away the complexities of interacting with different LLM providers and managing agent workflows, allowing developers to focus on application logic. \n\nNotes:\nThe `CLAUDE.md` file provides guidance for Claude Code when working with the repository, outlining development commands and project architecture. The `mkdocs.yml` file defines the structure and content of the project's documentation, further detailing the features and organization of the repository. \n\nWiki pages you might want to explore:\n- [Google, Anthropic and Other Providers (pydantic/pydantic-ai)](/wiki/pydantic/pydantic-ai#3.3)\n\nView this search on DeepWiki: https://deepwiki.com/search/what-is-this-repository-about_5104a64d-2f5e-4461-80d8-eb0892242441\n"}]} } + data: {"type":"content_block_start","index":2,"content_block":{"type":"mcp_tool_result","tool_use_id":"mcptoolu_01FZmJ5UspaX5BB9uU339UT1","is_error":false,"content":[{"type":"text","text":"This repository, `pydantic/pydantic-ai`, is a GenAI Agent Framework that leverages Pydantic for building Generative AI applications. Its main purpose is to provide a unified and type-safe way to interact with various large language models (LLMs) from different providers, manage agent execution flows, and integrate with external tools and services. \n\n## Main Features and Purpose\n\nThe `pydantic-ai` repository offers several core features:\n\n### 1. Agent System\nThe `Agent` class serves as the main orchestrator for managing interactions with LLMs and executing tasks. Agents can be configured with generic types for dependency injection (`Agent[AgentDepsT, OutputDataT]`) and output validation, ensuring type safety throughout the application. \n\nAgents support various execution methods:\n* `agent.run()`: An asynchronous function that returns a completed `RunResult`. \n* `agent.run_sync()`: A synchronous function that internally calls `run()` to return a completed `RunResult`. \n* `agent.run_stream()`: An asynchronous context manager for streaming text and structured output. \n* `agent.run_stream_events()`: Returns an asynchronous iterable of `AgentStreamEvent`s and a final `AgentRunResultEvent`. \n* `agent.iter()`: A context manager that provides an asynchronous iterable over the nodes of the agent's underlying `Graph`, allowing for deeper control and insight into the execution flow. \n\n### 2. Model Integration\nThe framework provides a unified interface for integrating with various LLM providers, including OpenAI, Anthropic, Google, Groq, Cohere, Mistral, Bedrock, and HuggingFace. Each model integration follows a consistent settings pattern with provider-specific prefixes (e.g., `google_*`, `anthropic_*`). \n\nExamples of supported models and their capabilities include:\n* `GoogleModel`: Integrates with Google's Gemini API, supporting both Gemini API (`google-gla`) and Vertex AI (`google-vertex`) providers. It supports token counting, streaming, built-in tools like `WebSearchTool`, `WebFetchTool`, `CodeExecutionTool`, and native JSON schema output. \n* `AnthropicModel`: Uses Anthropic's beta API for advanced features like \"Thinking Blocks\" and built-in tools. \n* `GroqModel`: Offers high-speed inference and specialized reasoning support with configurable reasoning formats. \n* `MistralModel`: Supports customizable JSON schema prompting and thinking support. \n* `BedrockConverseModel`: Utilizes AWS Bedrock's Converse API for unified access to various foundation models like Claude, Titan, Llama, and Mistral. \n* `CohereModel`: Integrates with Cohere's v2 API for chat completions, including thinking support and tool calling. \n\nThe framework also supports multimodal inputs such as `AudioUrl`, `DocumentUrl`, `ImageUrl`, and `VideoUrl`, allowing agents to process and respond to diverse content types. \n\n### 3. Graph-based Execution\nPydantic AI uses `pydantic-graph` to manage the execution flow of agents, representing it as a finite state machine. The execution typically flows through `UserPromptNode` → `ModelRequestNode` → `CallToolsNode`. This allows for detailed tracking of message history and usage. \n\n### 4. Tool System\nFunction tools enable models to perform actions and retrieve additional information. Tools can be registered using decorators like `@agent.tool` (for tools needing `RunContext` access) or `@agent.tool_plain` (for tools without `RunContext` access). The framework also supports toolsets for managing collections of tools. \n\nTools can return various types of output, including anything Pydantic can serialize to JSON, as well as multimodal content like `AudioUrl`, `VideoUrl`, `ImageUrl`, or `DocumentUrl`. The `ToolReturn` object allows for separating the `return_value` (for the model), `content` (for additional context), and `metadata` (for application-specific use). \n\nBuilt-in tools like `WebFetchTool` allow agents to pull web content into their context. \n\n### 5. Output Handling\nThe framework supports various output types:\n* `TextOutput`: Plain text responses. \n* `ToolOutput`: Structured data via tool calls. \n* `NativeOutput`: Provider-specific structured output. \n* `PromptedOutput`: Prompt-based structured extraction. \n\n### 6. Durable Execution\nPydantic AI integrates with durable execution systems like DBOS and Temporal. This allows agents to maintain state and resume execution after failures or restarts, making them suitable for long-running or fault-tolerant applications. \n\n### 7. Multi-Agent Patterns and Integrations\nThe repository supports multi-agent applications and various integrations, including:\n* Pydantic Evals: For evaluating agent performance. \n* Pydantic Graph: The underlying graph execution engine. \n* Logfire: For debugging and monitoring. \n* Agent-User Interaction (AG-UI) and Agent2Agent (A2A): For facilitating interactions between agents and users, and between agents themselves. \n* Clai: A CLI tool. \n\n## Purpose\n\nThe overarching purpose of `pydantic-ai` is to simplify the development of robust and reliable Generative AI applications by providing a structured, type-safe, and extensible framework. It aims to abstract away the complexities of interacting with different LLM providers and managing agent workflows, allowing developers to focus on application logic. \n\nNotes:\nThe `CLAUDE.md` file provides guidance for Claude Code when working with the repository, outlining development commands and project architecture. The `mkdocs.yml` file defines the structure and content of the project's documentation, further detailing the features and organization of the repository. \n\nWiki pages you might want to explore:\n- [Google, Anthropic and Other Providers (pydantic/pydantic-ai)](/wiki/pydantic/pydantic-ai#3.3)\n\nView this search on DeepWiki: https://deepwiki.com/search/what-is-this-repository-about_5104a64d-2f5e-4461-80d8-eb0892242441\n"}]} } event: content_block_stop data: {"type":"content_block_stop","index":2 } diff --git a/tests/models/cassettes/test_anthropic/test_anthropic_web_fetch_tool.yaml b/tests/models/cassettes/test_anthropic/test_anthropic_web_fetch_tool.yaml new file mode 100644 index 0000000000..2992be928b --- /dev/null +++ b/tests/models/cassettes/test_anthropic/test_anthropic_web_fetch_tool.yaml @@ -0,0 +1,789 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '467' + content-type: + - application/json + host: + - api.anthropic.com + method: POST + parsed_body: + max_tokens: 4096 + messages: + - content: + - text: What is the first sentence on the page https://ai.pydantic.dev? Reply with only the sentence. + type: text + role: user + model: claude-sonnet-4-0 + stream: false + thinking: + budget_tokens: 3000 + type: enabled + tool_choice: + type: auto + tools: + - allowed_domains: null + blocked_domains: null + citations: null + max_content_tokens: null + max_uses: null + name: web_fetch + type: web_fetch_20250910 + uri: https://api.anthropic.com/v1/messages?beta=true + response: + headers: + connection: + - keep-alive + content-length: + - '21807' + content-type: + - application/json + retry-after: + - '39' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + content: + - signature: EsIDCkYICRgCKkAKi/j4a8lGN12CjyS27ZXcPkXHGyTbn1vJENJz+AjinyTnsrynMEhidWT5IMNAs0TDgwSwPLNmgq4MsPkVekB8EgxetaK+Nhg8wUdhTEAaDMukODgr3JaYHZwVEiIwgKBckFLJ/C7wCD9oGCIECbqpaeEuWQ8BH3Hev6wpuc+66Wu7AJM1jGH60BpsUovnKqkCrHNq6b1SDT41cm2w7cyxZggrX6crzYh0fAkZ+VC6FBjy6mJikZtX6reKD+064KZ4F1oe4Qd40EBp/wHvD7oPV/fhGut1fzwl48ZgB8uzJb3tHr9MBjs4PVTsvKstpHKpOo6NLvCknQJ/0730OTENp/JOR6h6RUl6kMl5OrHTvsDEYpselUBPtLikm9p4t+d8CxqGm/B1kg1wN3FGJK31PD3veYIOO4hBirFPXWd+AiB1rZP++2QjToZ9lD2xqP/Q3vWEU+/Ryp6uzaRFWPVQkIr+mzpIaJsYuKDiyduxF4LD/hdMTV7IVDtconeQIPQJRhuO6nICBEuqb0uIotPDnCU6iI2l9OyEeKJM0RS6/NTNG8DZnvyVJ8gGKbtZKSHK6KKsdH0f7d+DGAE= + thinking: |- + The user is asking me to fetch the content from https://ai.pydantic.dev and return only the first sentence on that page. I need to use the web_fetch tool to get the content from this URL, then identify the first sentence and return only that sentence. + + Let me fetch the page first. + type: thinking + - id: srvtoolu_01So85wNUocinTvFfgKCfQeb + input: + url: https://ai.pydantic.dev + name: web_fetch + type: server_tool_use + - content: + content: + source: + data: |- + Pydantic AI + GenAI Agent Framework, the Pydantic way + Pydantic AI is a Python agent framework designed to help you quickly, confidently, and painlessly build production grade applications and workflows with Generative AI. + FastAPI revolutionized web development by offering an innovative and ergonomic design, built on the foundation of [Pydantic Validation](https://docs.pydantic.dev) and modern Python features like type hints. + Yet despite virtually every Python agent framework and LLM library using Pydantic Validation, when we began to use LLMs in [Pydantic Logfire](https://pydantic.dev/logfire), we couldn't find anything that gave us the same feeling. + We built Pydantic AI with one simple aim: to bring that FastAPI feeling to GenAI app and agent development. + Why use Pydantic AI + - + Built by the Pydantic Team: + [Pydantic Validation](https://docs.pydantic.dev/latest/)is the validation layer of the OpenAI SDK, the Google ADK, the Anthropic SDK, LangChain, LlamaIndex, AutoGPT, Transformers, CrewAI, Instructor and many more. Why use the derivative when you can go straight to the source? - + Model-agnostic: Supports virtually every + [model](models/overview/)and provider: OpenAI, Anthropic, Gemini, DeepSeek, Grok, Cohere, Mistral, and Perplexity; Azure AI Foundry, Amazon Bedrock, Google Vertex AI, Ollama, LiteLLM, Groq, OpenRouter, Together AI, Fireworks AI, Cerebras, Hugging Face, GitHub, Heroku, Vercel, Nebius, OVHcloud, and Outlines. If your favorite model or provider is not listed, you can easily implement a[custom model](models/overview/#custom-models). - + Seamless Observability: Tightly + [integrates](logfire/)with[Pydantic Logfire](https://pydantic.dev/logfire), our general-purpose OpenTelemetry observability platform, for real-time debugging, evals-based performance monitoring, and behavior, tracing, and cost tracking. If you already have an observability platform that supports OTel, you can[use that too](logfire/#alternative-observability-backends). - + Fully Type-safe: Designed to give your IDE or AI coding agent as much context as possible for auto-completion and + [type checking](agents/#static-type-checking), moving entire classes of errors from runtime to write-time for a bit of that Rust "if it compiles, it works" feel. - + Powerful Evals: Enables you to systematically test and + [evaluate](evals/)the performance and accuracy of the agentic systems you build, and monitor the performance over time in Pydantic Logfire. - + MCP, A2A, and UI: Integrates the + [Model Context Protocol](mcp/overview/),[Agent2Agent](a2a/), and various[UI event stream](ui/overview/)standards to give your agent access to external tools and data, let it interoperate with other agents, and build interactive applications with streaming event-based communication. - + Human-in-the-Loop Tool Approval: Easily lets you flag that certain tool calls + [require approval](deferred-tools/#human-in-the-loop-tool-approval)before they can proceed, possibly depending on tool call arguments, conversation history, or user preferences. - + Durable Execution: Enables you to build + [durable agents](durable_execution/overview/)that can preserve their progress across transient API failures and application errors or restarts, and handle long-running, asynchronous, and human-in-the-loop workflows with production-grade reliability. - + Streamed Outputs: Provides the ability to + [stream](output/#streamed-results)structured output continuously, with immediate validation, ensuring real time access to generated data. - + Graph Support: Provides a powerful way to define + [graphs](graph/)using type hints, for use in complex applications where standard control flow can degrade to spaghetti code. + Realistically though, no list is going to be as convincing as [giving it a try](#next-steps) and seeing how it makes you feel! + Sign up for our newsletter, The Pydantic Stack, with updates & tutorials on Pydantic AI, Logfire, and Pydantic: + Hello World Example + Here's a minimal example of Pydantic AI: + [Learn about Gateway](gateway)hello_world.py + from pydantic_ai import Agent + agent = Agent( # (1)! + 'gateway/anthropic:claude-sonnet-4-0', + instructions='Be concise, reply with one sentence.', # (2)! + ) + result = agent.run_sync('Where does "hello world" come from?') # (3)! + print(result.output) + """ + The first known use of "hello, world" was in a 1974 textbook about the C programming language. + """ + - We configure the agent to use + [Anthropic's Claude Sonnet 4.0](api/models/anthropic/)model, but you can also set the model when running the agent. - Register static + [instructions](agents/#instructions)using a keyword argument to the agent. [Run the agent](agents/#running-agents)synchronously, starting a conversation with the LLM. + from pydantic_ai import Agent + agent = Agent( # (1)! + 'anthropic:claude-sonnet-4-0', + instructions='Be concise, reply with one sentence.', # (2)! + ) + result = agent.run_sync('Where does "hello world" come from?') # (3)! + print(result.output) + """ + The first known use of "hello, world" was in a 1974 textbook about the C programming language. + """ + - We configure the agent to use + [Anthropic's Claude Sonnet 4.0](api/models/anthropic/)model, but you can also set the model when running the agent. - Register static + [instructions](agents/#instructions)using a keyword argument to the agent. [Run the agent](agents/#running-agents)synchronously, starting a conversation with the LLM. + (This example is complete, it can be run "as is", assuming you've [installed the pydantic_ai package](install/)) + The exchange will be very short: Pydantic AI will send the instructions and the user prompt to the LLM, and the model will return a text response. + Not very interesting yet, but we can easily add [tools](tools/), [dynamic instructions](agents/#instructions), and [structured outputs](output/) to build more powerful agents. + Tools & Dependency Injection Example + Here is a concise example using Pydantic AI to build a support agent for a bank: + [Learn about Gateway](gateway)bank_support.py + from dataclasses import dataclass + from pydantic import BaseModel, Field + from pydantic_ai import Agent, RunContext + from bank_database import DatabaseConn + @dataclass + class SupportDependencies: # (3)! + customer_id: int + db: DatabaseConn # (12)! + class SupportOutput(BaseModel): # (13)! + support_advice: str = Field(description='Advice returned to the customer') + block_card: bool = Field(description="Whether to block the customer's card") + risk: int = Field(description='Risk level of query', ge=0, le=10) + support_agent = Agent( # (1)! + 'gateway/openai:gpt-5', # (2)! + deps_type=SupportDependencies, + output_type=SupportOutput, # (9)! + instructions=( # (4)! + 'You are a support agent in our bank, give the ' + 'customer support and judge the risk level of their query.' + ), + ) + @support_agent.instructions # (5)! + async def add_customer_name(ctx: RunContext[SupportDependencies]) -> str: + customer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id) + return f"The customer's name is {customer_name!r}" + @support_agent.tool # (6)! + async def customer_balance( + ctx: RunContext[SupportDependencies], include_pending: bool + ) -> float: + """Returns the customer's current account balance.""" # (7)! + return await ctx.deps.db.customer_balance( + id=ctx.deps.customer_id, + include_pending=include_pending, + ) + ... # (11)! + async def main(): + deps = SupportDependencies(customer_id=123, db=DatabaseConn()) + result = await support_agent.run('What is my balance?', deps=deps) # (8)! + print(result.output) # (10)! + """ + support_advice='Hello John, your current account balance, including pending transactions, is $123.45.' block_card=False risk=1 + """ + result = await support_agent.run('I just lost my card!', deps=deps) + print(result.output) + """ + support_advice="I'm sorry to hear that, John. We are temporarily blocking your card to prevent unauthorized transactions." block_card=True risk=8 + """ + - This + [agent](agents/)will act as first-tier support in a bank. Agents are generic in the type of dependencies they accept and the type of output they return. In this case, the support agent has typeAgent[SupportDependencies, SupportOutput] + . - Here we configure the agent to use + [OpenAI's GPT-5 model](api/models/openai/), you can also set the model when running the agent. - The + SupportDependencies + dataclass is used to pass data, connections, and logic into the model that will be needed when running[instructions](agents/#instructions)and[tool](tools/)functions. Pydantic AI's system of dependency injection provides a[type-safe](agents/#static-type-checking)way to customise the behavior of your agents, and can be especially useful when running[unit tests](testing/)and evals. - Static + [instructions](agents/#instructions)can be registered with theto the agent.instructions + keyword argument - Dynamic + [instructions](agents/#instructions)can be registered with thedecorator, and can make use of dependency injection. Dependencies are carried via the@agent.instructions + argument, which is parameterized with theRunContext + deps_type + from above. If the type annotation here is wrong, static type checkers will catch it. - The + decorator let you register functions which the LLM may call while responding to a user. Again, dependencies are carried via@agent.tool + , any other arguments become the tool schema passed to the LLM. Pydantic is used to validate these arguments, and errors are passed back to the LLM so it can retry.RunContext + - The docstring of a tool is also passed to the LLM as the description of the tool. Parameter descriptions are + [extracted](tools/#function-tools-and-schema)from the docstring and added to the parameter schema sent to the LLM. [Run the agent](agents/#running-agents)asynchronously, conducting a conversation with the LLM until a final response is reached. Even in this fairly simple case, the agent will exchange multiple messages with the LLM as tools are called to retrieve an output.- The response from the agent will be guaranteed to be a + SupportOutput + . If validation fails[reflection](agents/#reflection-and-self-correction), the agent is prompted to try again. - The output will be validated with Pydantic to guarantee it is a + SupportOutput + , since the agent is generic, it'll also be typed as aSupportOutput + to aid with static type checking. - In a real use case, you'd add more tools and longer instructions to the agent to extend the context it's equipped with and support it can provide. + - This is a simple sketch of a database connection, used to keep the example short and readable. In reality, you'd be connecting to an external database (e.g. PostgreSQL) to get information about customers. + - This + [Pydantic](https://docs.pydantic.dev)model is used to constrain the structured data returned by the agent. From this simple definition, Pydantic builds the JSON Schema that tells the LLM how to return the data, and performs validation to guarantee the data is correct at the end of the run. + from dataclasses import dataclass + from pydantic import BaseModel, Field + from pydantic_ai import Agent, RunContext + from bank_database import DatabaseConn + @dataclass + class SupportDependencies: # (3)! + customer_id: int + db: DatabaseConn # (12)! + class SupportOutput(BaseModel): # (13)! + support_advice: str = Field(description='Advice returned to the customer') + block_card: bool = Field(description="Whether to block the customer's card") + risk: int = Field(description='Risk level of query', ge=0, le=10) + support_agent = Agent( # (1)! + 'openai:gpt-5', # (2)! + deps_type=SupportDependencies, + output_type=SupportOutput, # (9)! + instructions=( # (4)! + 'You are a support agent in our bank, give the ' + 'customer support and judge the risk level of their query.' + ), + ) + @support_agent.instructions # (5)! + async def add_customer_name(ctx: RunContext[SupportDependencies]) -> str: + customer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id) + return f"The customer's name is {customer_name!r}" + @support_agent.tool # (6)! + async def customer_balance( + ctx: RunContext[SupportDependencies], include_pending: bool + ) -> float: + """Returns the customer's current account balance.""" # (7)! + return await ctx.deps.db.customer_balance( + id=ctx.deps.customer_id, + include_pending=include_pending, + ) + ... # (11)! + async def main(): + deps = SupportDependencies(customer_id=123, db=DatabaseConn()) + result = await support_agent.run('What is my balance?', deps=deps) # (8)! + print(result.output) # (10)! + """ + support_advice='Hello John, your current account balance, including pending transactions, is $123.45.' block_card=False risk=1 + """ + result = await support_agent.run('I just lost my card!', deps=deps) + print(result.output) + """ + support_advice="I'm sorry to hear that, John. We are temporarily blocking your card to prevent unauthorized transactions." block_card=True risk=8 + """ + - This + [agent](agents/)will act as first-tier support in a bank. Agents are generic in the type of dependencies they accept and the type of output they return. In this case, the support agent has typeAgent[SupportDependencies, SupportOutput] + . - Here we configure the agent to use + [OpenAI's GPT-5 model](api/models/openai/), you can also set the model when running the agent. - The + SupportDependencies + dataclass is used to pass data, connections, and logic into the model that will be needed when running[instructions](agents/#instructions)and[tool](tools/)functions. Pydantic AI's system of dependency injection provides a[type-safe](agents/#static-type-checking)way to customise the behavior of your agents, and can be especially useful when running[unit tests](testing/)and evals. - Static + [instructions](agents/#instructions)can be registered with theto the agent.instructions + keyword argument - Dynamic + [instructions](agents/#instructions)can be registered with thedecorator, and can make use of dependency injection. Dependencies are carried via the@agent.instructions + argument, which is parameterized with theRunContext + deps_type + from above. If the type annotation here is wrong, static type checkers will catch it. - The + decorator let you register functions which the LLM may call while responding to a user. Again, dependencies are carried via@agent.tool + , any other arguments become the tool schema passed to the LLM. Pydantic is used to validate these arguments, and errors are passed back to the LLM so it can retry.RunContext + - The docstring of a tool is also passed to the LLM as the description of the tool. Parameter descriptions are + [extracted](tools/#function-tools-and-schema)from the docstring and added to the parameter schema sent to the LLM. [Run the agent](agents/#running-agents)asynchronously, conducting a conversation with the LLM until a final response is reached. Even in this fairly simple case, the agent will exchange multiple messages with the LLM as tools are called to retrieve an output.- The response from the agent will be guaranteed to be a + SupportOutput + . If validation fails[reflection](agents/#reflection-and-self-correction), the agent is prompted to try again. - The output will be validated with Pydantic to guarantee it is a + SupportOutput + , since the agent is generic, it'll also be typed as aSupportOutput + to aid with static type checking. - In a real use case, you'd add more tools and longer instructions to the agent to extend the context it's equipped with and support it can provide. + - This is a simple sketch of a database connection, used to keep the example short and readable. In reality, you'd be connecting to an external database (e.g. PostgreSQL) to get information about customers. + - This + [Pydantic](https://docs.pydantic.dev)model is used to constrain the structured data returned by the agent. From this simple definition, Pydantic builds the JSON Schema that tells the LLM how to return the data, and performs validation to guarantee the data is correct at the end of the run. + Complete bank_support.py + example + The code included here is incomplete for the sake of brevity (the definition of DatabaseConn + is missing); you can find the complete bank_support.py + example [here](examples/bank-support/). + Instrumentation with Pydantic Logfire + Even a simple agent with just a handful of tools can result in a lot of back-and-forth with the LLM, making it nearly impossible to be confident of what's going on just from reading the code. To understand the flow of the above runs, we can watch the agent in action using Pydantic Logfire. + To do this, we need to [set up Logfire](logfire/#using-logfire), and add the following to our code: + [Learn about Gateway](gateway)bank_support_with_logfire.py + ... + from pydantic_ai import Agent, RunContext + from bank_database import DatabaseConn + import logfire + logfire.configure() # (1)! + logfire.instrument_pydantic_ai() # (2)! + logfire.instrument_asyncpg() # (3)! + ... + support_agent = Agent( + 'gateway/openai:gpt-5', + deps_type=SupportDependencies, + output_type=SupportOutput, + system_prompt=( + 'You are a support agent in our bank, give the ' + 'customer support and judge the risk level of their query.' + ), + ) + - Configure the Logfire SDK, this will fail if project is not set up. + - This will instrument all Pydantic AI agents used from here on out. If you want to instrument only a specific agent, you can pass the + to the agent.instrument=True + keyword argument - In our demo, + DatabaseConn + usesto connect to a PostgreSQL database, soasyncpg + is used to log the database queries.logfire.instrument_asyncpg() + ... + from pydantic_ai import Agent, RunContext + from bank_database import DatabaseConn + import logfire + logfire.configure() # (1)! + logfire.instrument_pydantic_ai() # (2)! + logfire.instrument_asyncpg() # (3)! + ... + support_agent = Agent( + 'openai:gpt-5', + deps_type=SupportDependencies, + output_type=SupportOutput, + system_prompt=( + 'You are a support agent in our bank, give the ' + 'customer support and judge the risk level of their query.' + ), + ) + - Configure the Logfire SDK, this will fail if project is not set up. + - This will instrument all Pydantic AI agents used from here on out. If you want to instrument only a specific agent, you can pass the + to the agent.instrument=True + keyword argument - In our demo, + DatabaseConn + usesto connect to a PostgreSQL database, soasyncpg + is used to log the database queries.logfire.instrument_asyncpg() + That's enough to get the following view of your agent in action: + See [Monitoring and Performance](logfire/) to learn more. + llms.txt + The Pydantic AI documentation is available in the [llms.txt](https://llmstxt.org/) format. + This format is defined in Markdown and suited for LLMs and AI coding assistants and agents. + Two formats are available: + : a file containing a brief description of the project, along with links to the different sections of the documentation. The structure of this file is described in detailsllms.txt + [here](https://llmstxt.org/#format).: Similar to thellms-full.txt + llms.txt + file, but every link content is included. Note that this file may be too large for some LLMs. + As of today, these files are not automatically leveraged by IDEs or coding agents, but they will use it if you provide a link or the full text. + Next Steps + To try Pydantic AI for yourself, [install it](install/) and follow the instructions [in the examples](examples/setup/). + Read the [docs](agents/) to learn more about building applications with Pydantic AI. + Read the [API Reference](api/agent/) to understand Pydantic AI's interface. + Join [ Slack](https://logfire.pydantic.dev/docs/join-slack/) or file an issue on [ GitHub](https://github.com/pydantic/pydantic-ai/issues) if you have any questions. + media_type: text/plain + type: text + title: Pydantic AI + type: document + retrieved_at: '2025-11-14T23:34:21.151000+00:00' + type: web_fetch_result + url: https://ai.pydantic.dev + tool_use_id: srvtoolu_01So85wNUocinTvFfgKCfQeb + type: web_fetch_tool_result + - text: Pydantic AI is a Python agent framework designed to help you quickly, confidently, and painlessly build production + grade applications and workflows with Generative AI. + type: text + id: msg_014MfQbsguyfo8X7ffezhM5Q + model: claude-sonnet-4-20250514 + role: assistant + stop_reason: end_turn + stop_sequence: null + type: message + usage: + cache_creation: + ephemeral_1h_input_tokens: 0 + ephemeral_5m_input_tokens: 0 + cache_creation_input_tokens: 0 + cache_read_input_tokens: 0 + input_tokens: 7262 + output_tokens: 171 + server_tool_use: + web_fetch_requests: 1 + web_search_requests: 0 + service_tier: standard + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '21997' + content-type: + - application/json + host: + - api.anthropic.com + method: POST + parsed_body: + max_tokens: 4096 + messages: + - content: + - text: What is the first sentence on the page https://ai.pydantic.dev? Reply with only the sentence. + type: text + role: user + - content: + - signature: EsIDCkYICRgCKkAKi/j4a8lGN12CjyS27ZXcPkXHGyTbn1vJENJz+AjinyTnsrynMEhidWT5IMNAs0TDgwSwPLNmgq4MsPkVekB8EgxetaK+Nhg8wUdhTEAaDMukODgr3JaYHZwVEiIwgKBckFLJ/C7wCD9oGCIECbqpaeEuWQ8BH3Hev6wpuc+66Wu7AJM1jGH60BpsUovnKqkCrHNq6b1SDT41cm2w7cyxZggrX6crzYh0fAkZ+VC6FBjy6mJikZtX6reKD+064KZ4F1oe4Qd40EBp/wHvD7oPV/fhGut1fzwl48ZgB8uzJb3tHr9MBjs4PVTsvKstpHKpOo6NLvCknQJ/0730OTENp/JOR6h6RUl6kMl5OrHTvsDEYpselUBPtLikm9p4t+d8CxqGm/B1kg1wN3FGJK31PD3veYIOO4hBirFPXWd+AiB1rZP++2QjToZ9lD2xqP/Q3vWEU+/Ryp6uzaRFWPVQkIr+mzpIaJsYuKDiyduxF4LD/hdMTV7IVDtconeQIPQJRhuO6nICBEuqb0uIotPDnCU6iI2l9OyEeKJM0RS6/NTNG8DZnvyVJ8gGKbtZKSHK6KKsdH0f7d+DGAE= + thinking: |- + The user is asking me to fetch the content from https://ai.pydantic.dev and return only the first sentence on that page. I need to use the web_fetch tool to get the content from this URL, then identify the first sentence and return only that sentence. + + Let me fetch the page first. + type: thinking + - id: srvtoolu_01So85wNUocinTvFfgKCfQeb + input: + url: https://ai.pydantic.dev + name: web_fetch + type: server_tool_use + - content: + content: + citations: null + source: + data: |- + Pydantic AI + GenAI Agent Framework, the Pydantic way + Pydantic AI is a Python agent framework designed to help you quickly, confidently, and painlessly build production grade applications and workflows with Generative AI. + FastAPI revolutionized web development by offering an innovative and ergonomic design, built on the foundation of [Pydantic Validation](https://docs.pydantic.dev) and modern Python features like type hints. + Yet despite virtually every Python agent framework and LLM library using Pydantic Validation, when we began to use LLMs in [Pydantic Logfire](https://pydantic.dev/logfire), we couldn't find anything that gave us the same feeling. + We built Pydantic AI with one simple aim: to bring that FastAPI feeling to GenAI app and agent development. + Why use Pydantic AI + - + Built by the Pydantic Team: + [Pydantic Validation](https://docs.pydantic.dev/latest/)is the validation layer of the OpenAI SDK, the Google ADK, the Anthropic SDK, LangChain, LlamaIndex, AutoGPT, Transformers, CrewAI, Instructor and many more. Why use the derivative when you can go straight to the source? - + Model-agnostic: Supports virtually every + [model](models/overview/)and provider: OpenAI, Anthropic, Gemini, DeepSeek, Grok, Cohere, Mistral, and Perplexity; Azure AI Foundry, Amazon Bedrock, Google Vertex AI, Ollama, LiteLLM, Groq, OpenRouter, Together AI, Fireworks AI, Cerebras, Hugging Face, GitHub, Heroku, Vercel, Nebius, OVHcloud, and Outlines. If your favorite model or provider is not listed, you can easily implement a[custom model](models/overview/#custom-models). - + Seamless Observability: Tightly + [integrates](logfire/)with[Pydantic Logfire](https://pydantic.dev/logfire), our general-purpose OpenTelemetry observability platform, for real-time debugging, evals-based performance monitoring, and behavior, tracing, and cost tracking. If you already have an observability platform that supports OTel, you can[use that too](logfire/#alternative-observability-backends). - + Fully Type-safe: Designed to give your IDE or AI coding agent as much context as possible for auto-completion and + [type checking](agents/#static-type-checking), moving entire classes of errors from runtime to write-time for a bit of that Rust "if it compiles, it works" feel. - + Powerful Evals: Enables you to systematically test and + [evaluate](evals/)the performance and accuracy of the agentic systems you build, and monitor the performance over time in Pydantic Logfire. - + MCP, A2A, and UI: Integrates the + [Model Context Protocol](mcp/overview/),[Agent2Agent](a2a/), and various[UI event stream](ui/overview/)standards to give your agent access to external tools and data, let it interoperate with other agents, and build interactive applications with streaming event-based communication. - + Human-in-the-Loop Tool Approval: Easily lets you flag that certain tool calls + [require approval](deferred-tools/#human-in-the-loop-tool-approval)before they can proceed, possibly depending on tool call arguments, conversation history, or user preferences. - + Durable Execution: Enables you to build + [durable agents](durable_execution/overview/)that can preserve their progress across transient API failures and application errors or restarts, and handle long-running, asynchronous, and human-in-the-loop workflows with production-grade reliability. - + Streamed Outputs: Provides the ability to + [stream](output/#streamed-results)structured output continuously, with immediate validation, ensuring real time access to generated data. - + Graph Support: Provides a powerful way to define + [graphs](graph/)using type hints, for use in complex applications where standard control flow can degrade to spaghetti code. + Realistically though, no list is going to be as convincing as [giving it a try](#next-steps) and seeing how it makes you feel! + Sign up for our newsletter, The Pydantic Stack, with updates & tutorials on Pydantic AI, Logfire, and Pydantic: + Hello World Example + Here's a minimal example of Pydantic AI: + [Learn about Gateway](gateway)hello_world.py + from pydantic_ai import Agent + agent = Agent( # (1)! + 'gateway/anthropic:claude-sonnet-4-0', + instructions='Be concise, reply with one sentence.', # (2)! + ) + result = agent.run_sync('Where does "hello world" come from?') # (3)! + print(result.output) + """ + The first known use of "hello, world" was in a 1974 textbook about the C programming language. + """ + - We configure the agent to use + [Anthropic's Claude Sonnet 4.0](api/models/anthropic/)model, but you can also set the model when running the agent. - Register static + [instructions](agents/#instructions)using a keyword argument to the agent. [Run the agent](agents/#running-agents)synchronously, starting a conversation with the LLM. + from pydantic_ai import Agent + agent = Agent( # (1)! + 'anthropic:claude-sonnet-4-0', + instructions='Be concise, reply with one sentence.', # (2)! + ) + result = agent.run_sync('Where does "hello world" come from?') # (3)! + print(result.output) + """ + The first known use of "hello, world" was in a 1974 textbook about the C programming language. + """ + - We configure the agent to use + [Anthropic's Claude Sonnet 4.0](api/models/anthropic/)model, but you can also set the model when running the agent. - Register static + [instructions](agents/#instructions)using a keyword argument to the agent. [Run the agent](agents/#running-agents)synchronously, starting a conversation with the LLM. + (This example is complete, it can be run "as is", assuming you've [installed the pydantic_ai package](install/)) + The exchange will be very short: Pydantic AI will send the instructions and the user prompt to the LLM, and the model will return a text response. + Not very interesting yet, but we can easily add [tools](tools/), [dynamic instructions](agents/#instructions), and [structured outputs](output/) to build more powerful agents. + Tools & Dependency Injection Example + Here is a concise example using Pydantic AI to build a support agent for a bank: + [Learn about Gateway](gateway)bank_support.py + from dataclasses import dataclass + from pydantic import BaseModel, Field + from pydantic_ai import Agent, RunContext + from bank_database import DatabaseConn + @dataclass + class SupportDependencies: # (3)! + customer_id: int + db: DatabaseConn # (12)! + class SupportOutput(BaseModel): # (13)! + support_advice: str = Field(description='Advice returned to the customer') + block_card: bool = Field(description="Whether to block the customer's card") + risk: int = Field(description='Risk level of query', ge=0, le=10) + support_agent = Agent( # (1)! + 'gateway/openai:gpt-5', # (2)! + deps_type=SupportDependencies, + output_type=SupportOutput, # (9)! + instructions=( # (4)! + 'You are a support agent in our bank, give the ' + 'customer support and judge the risk level of their query.' + ), + ) + @support_agent.instructions # (5)! + async def add_customer_name(ctx: RunContext[SupportDependencies]) -> str: + customer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id) + return f"The customer's name is {customer_name!r}" + @support_agent.tool # (6)! + async def customer_balance( + ctx: RunContext[SupportDependencies], include_pending: bool + ) -> float: + """Returns the customer's current account balance.""" # (7)! + return await ctx.deps.db.customer_balance( + id=ctx.deps.customer_id, + include_pending=include_pending, + ) + ... # (11)! + async def main(): + deps = SupportDependencies(customer_id=123, db=DatabaseConn()) + result = await support_agent.run('What is my balance?', deps=deps) # (8)! + print(result.output) # (10)! + """ + support_advice='Hello John, your current account balance, including pending transactions, is $123.45.' block_card=False risk=1 + """ + result = await support_agent.run('I just lost my card!', deps=deps) + print(result.output) + """ + support_advice="I'm sorry to hear that, John. We are temporarily blocking your card to prevent unauthorized transactions." block_card=True risk=8 + """ + - This + [agent](agents/)will act as first-tier support in a bank. Agents are generic in the type of dependencies they accept and the type of output they return. In this case, the support agent has typeAgent[SupportDependencies, SupportOutput] + . - Here we configure the agent to use + [OpenAI's GPT-5 model](api/models/openai/), you can also set the model when running the agent. - The + SupportDependencies + dataclass is used to pass data, connections, and logic into the model that will be needed when running[instructions](agents/#instructions)and[tool](tools/)functions. Pydantic AI's system of dependency injection provides a[type-safe](agents/#static-type-checking)way to customise the behavior of your agents, and can be especially useful when running[unit tests](testing/)and evals. - Static + [instructions](agents/#instructions)can be registered with theto the agent.instructions + keyword argument - Dynamic + [instructions](agents/#instructions)can be registered with thedecorator, and can make use of dependency injection. Dependencies are carried via the@agent.instructions + argument, which is parameterized with theRunContext + deps_type + from above. If the type annotation here is wrong, static type checkers will catch it. - The + decorator let you register functions which the LLM may call while responding to a user. Again, dependencies are carried via@agent.tool + , any other arguments become the tool schema passed to the LLM. Pydantic is used to validate these arguments, and errors are passed back to the LLM so it can retry.RunContext + - The docstring of a tool is also passed to the LLM as the description of the tool. Parameter descriptions are + [extracted](tools/#function-tools-and-schema)from the docstring and added to the parameter schema sent to the LLM. [Run the agent](agents/#running-agents)asynchronously, conducting a conversation with the LLM until a final response is reached. Even in this fairly simple case, the agent will exchange multiple messages with the LLM as tools are called to retrieve an output.- The response from the agent will be guaranteed to be a + SupportOutput + . If validation fails[reflection](agents/#reflection-and-self-correction), the agent is prompted to try again. - The output will be validated with Pydantic to guarantee it is a + SupportOutput + , since the agent is generic, it'll also be typed as aSupportOutput + to aid with static type checking. - In a real use case, you'd add more tools and longer instructions to the agent to extend the context it's equipped with and support it can provide. + - This is a simple sketch of a database connection, used to keep the example short and readable. In reality, you'd be connecting to an external database (e.g. PostgreSQL) to get information about customers. + - This + [Pydantic](https://docs.pydantic.dev)model is used to constrain the structured data returned by the agent. From this simple definition, Pydantic builds the JSON Schema that tells the LLM how to return the data, and performs validation to guarantee the data is correct at the end of the run. + from dataclasses import dataclass + from pydantic import BaseModel, Field + from pydantic_ai import Agent, RunContext + from bank_database import DatabaseConn + @dataclass + class SupportDependencies: # (3)! + customer_id: int + db: DatabaseConn # (12)! + class SupportOutput(BaseModel): # (13)! + support_advice: str = Field(description='Advice returned to the customer') + block_card: bool = Field(description="Whether to block the customer's card") + risk: int = Field(description='Risk level of query', ge=0, le=10) + support_agent = Agent( # (1)! + 'openai:gpt-5', # (2)! + deps_type=SupportDependencies, + output_type=SupportOutput, # (9)! + instructions=( # (4)! + 'You are a support agent in our bank, give the ' + 'customer support and judge the risk level of their query.' + ), + ) + @support_agent.instructions # (5)! + async def add_customer_name(ctx: RunContext[SupportDependencies]) -> str: + customer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id) + return f"The customer's name is {customer_name!r}" + @support_agent.tool # (6)! + async def customer_balance( + ctx: RunContext[SupportDependencies], include_pending: bool + ) -> float: + """Returns the customer's current account balance.""" # (7)! + return await ctx.deps.db.customer_balance( + id=ctx.deps.customer_id, + include_pending=include_pending, + ) + ... # (11)! + async def main(): + deps = SupportDependencies(customer_id=123, db=DatabaseConn()) + result = await support_agent.run('What is my balance?', deps=deps) # (8)! + print(result.output) # (10)! + """ + support_advice='Hello John, your current account balance, including pending transactions, is $123.45.' block_card=False risk=1 + """ + result = await support_agent.run('I just lost my card!', deps=deps) + print(result.output) + """ + support_advice="I'm sorry to hear that, John. We are temporarily blocking your card to prevent unauthorized transactions." block_card=True risk=8 + """ + - This + [agent](agents/)will act as first-tier support in a bank. Agents are generic in the type of dependencies they accept and the type of output they return. In this case, the support agent has typeAgent[SupportDependencies, SupportOutput] + . - Here we configure the agent to use + [OpenAI's GPT-5 model](api/models/openai/), you can also set the model when running the agent. - The + SupportDependencies + dataclass is used to pass data, connections, and logic into the model that will be needed when running[instructions](agents/#instructions)and[tool](tools/)functions. Pydantic AI's system of dependency injection provides a[type-safe](agents/#static-type-checking)way to customise the behavior of your agents, and can be especially useful when running[unit tests](testing/)and evals. - Static + [instructions](agents/#instructions)can be registered with theto the agent.instructions + keyword argument - Dynamic + [instructions](agents/#instructions)can be registered with thedecorator, and can make use of dependency injection. Dependencies are carried via the@agent.instructions + argument, which is parameterized with theRunContext + deps_type + from above. If the type annotation here is wrong, static type checkers will catch it. - The + decorator let you register functions which the LLM may call while responding to a user. Again, dependencies are carried via@agent.tool + , any other arguments become the tool schema passed to the LLM. Pydantic is used to validate these arguments, and errors are passed back to the LLM so it can retry.RunContext + - The docstring of a tool is also passed to the LLM as the description of the tool. Parameter descriptions are + [extracted](tools/#function-tools-and-schema)from the docstring and added to the parameter schema sent to the LLM. [Run the agent](agents/#running-agents)asynchronously, conducting a conversation with the LLM until a final response is reached. Even in this fairly simple case, the agent will exchange multiple messages with the LLM as tools are called to retrieve an output.- The response from the agent will be guaranteed to be a + SupportOutput + . If validation fails[reflection](agents/#reflection-and-self-correction), the agent is prompted to try again. - The output will be validated with Pydantic to guarantee it is a + SupportOutput + , since the agent is generic, it'll also be typed as aSupportOutput + to aid with static type checking. - In a real use case, you'd add more tools and longer instructions to the agent to extend the context it's equipped with and support it can provide. + - This is a simple sketch of a database connection, used to keep the example short and readable. In reality, you'd be connecting to an external database (e.g. PostgreSQL) to get information about customers. + - This + [Pydantic](https://docs.pydantic.dev)model is used to constrain the structured data returned by the agent. From this simple definition, Pydantic builds the JSON Schema that tells the LLM how to return the data, and performs validation to guarantee the data is correct at the end of the run. + Complete bank_support.py + example + The code included here is incomplete for the sake of brevity (the definition of DatabaseConn + is missing); you can find the complete bank_support.py + example [here](examples/bank-support/). + Instrumentation with Pydantic Logfire + Even a simple agent with just a handful of tools can result in a lot of back-and-forth with the LLM, making it nearly impossible to be confident of what's going on just from reading the code. To understand the flow of the above runs, we can watch the agent in action using Pydantic Logfire. + To do this, we need to [set up Logfire](logfire/#using-logfire), and add the following to our code: + [Learn about Gateway](gateway)bank_support_with_logfire.py + ... + from pydantic_ai import Agent, RunContext + from bank_database import DatabaseConn + import logfire + logfire.configure() # (1)! + logfire.instrument_pydantic_ai() # (2)! + logfire.instrument_asyncpg() # (3)! + ... + support_agent = Agent( + 'gateway/openai:gpt-5', + deps_type=SupportDependencies, + output_type=SupportOutput, + system_prompt=( + 'You are a support agent in our bank, give the ' + 'customer support and judge the risk level of their query.' + ), + ) + - Configure the Logfire SDK, this will fail if project is not set up. + - This will instrument all Pydantic AI agents used from here on out. If you want to instrument only a specific agent, you can pass the + to the agent.instrument=True + keyword argument - In our demo, + DatabaseConn + usesto connect to a PostgreSQL database, soasyncpg + is used to log the database queries.logfire.instrument_asyncpg() + ... + from pydantic_ai import Agent, RunContext + from bank_database import DatabaseConn + import logfire + logfire.configure() # (1)! + logfire.instrument_pydantic_ai() # (2)! + logfire.instrument_asyncpg() # (3)! + ... + support_agent = Agent( + 'openai:gpt-5', + deps_type=SupportDependencies, + output_type=SupportOutput, + system_prompt=( + 'You are a support agent in our bank, give the ' + 'customer support and judge the risk level of their query.' + ), + ) + - Configure the Logfire SDK, this will fail if project is not set up. + - This will instrument all Pydantic AI agents used from here on out. If you want to instrument only a specific agent, you can pass the + to the agent.instrument=True + keyword argument - In our demo, + DatabaseConn + usesto connect to a PostgreSQL database, soasyncpg + is used to log the database queries.logfire.instrument_asyncpg() + That's enough to get the following view of your agent in action: + See [Monitoring and Performance](logfire/) to learn more. + llms.txt + The Pydantic AI documentation is available in the [llms.txt](https://llmstxt.org/) format. + This format is defined in Markdown and suited for LLMs and AI coding assistants and agents. + Two formats are available: + : a file containing a brief description of the project, along with links to the different sections of the documentation. The structure of this file is described in detailsllms.txt + [here](https://llmstxt.org/#format).: Similar to thellms-full.txt + llms.txt + file, but every link content is included. Note that this file may be too large for some LLMs. + As of today, these files are not automatically leveraged by IDEs or coding agents, but they will use it if you provide a link or the full text. + Next Steps + To try Pydantic AI for yourself, [install it](install/) and follow the instructions [in the examples](examples/setup/). + Read the [docs](agents/) to learn more about building applications with Pydantic AI. + Read the [API Reference](api/agent/) to understand Pydantic AI's interface. + Join [ Slack](https://logfire.pydantic.dev/docs/join-slack/) or file an issue on [ GitHub](https://github.com/pydantic/pydantic-ai/issues) if you have any questions. + media_type: text/plain + type: text + title: Pydantic AI + type: document + retrieved_at: '2025-11-14T23:34:21.151000+00:00' + type: web_fetch_result + url: https://ai.pydantic.dev + tool_use_id: srvtoolu_01So85wNUocinTvFfgKCfQeb + type: web_fetch_tool_result + - text: Pydantic AI is a Python agent framework designed to help you quickly, confidently, and painlessly build production + grade applications and workflows with Generative AI. + type: text + role: assistant + - content: + - text: Based on the page you just fetched, what framework does it mention? + type: text + role: user + model: claude-sonnet-4-0 + stream: false + thinking: + budget_tokens: 3000 + type: enabled + tool_choice: + type: auto + tools: + - allowed_domains: null + blocked_domains: null + citations: null + max_content_tokens: null + max_uses: null + name: web_fetch + type: web_fetch_20250910 + uri: https://api.anthropic.com/v1/messages?beta=true + response: + headers: + connection: + - keep-alive + content-length: + - '3150' + content-type: + - application/json + retry-after: + - '36' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + content: + - signature: ErIHCkYICRgCKkDZrwipmaxoEat4WffzPSjVzIuSQWM2sHE6FLC2wt5S2qiJN2MQh//EImuLE9I2ssZjTMxGXZV+esnf5ipnzbvnEgxfcXs2ax8vnLdroxMaDCpqvdPKpCP3Qi0txCIw55NdOjY30P3/yRL9RF8sPGioyitlzkhSpf+PuC3YXwz4N0hoy8zVY1MHecwc60vcKpkGxtZsfqmAuJwjeGRr/Ugxcxd69+0X/Y9pojMiklNHq9otW+ehDX0rR0EzfdN/2jNOs3bOrzfy9jmvYE5FU2c5e0JpMP3LH0LrFvZYkSh7RkbhYuHvrOqohlE3BhpflrszowmiozUk+aG4wSqx5Dtxo9W7jfeU4wduy6OyEFdIqdYdTMR8VVf9Qnd5bLX4rY09xcGQc4JcX2mFjdSR2WgEJM7p5lytlN5unH3selWBVPbCj7ogU8DbT9zhY3zkDW1dMt2vNbWNaY4gVrLwi42qBJvjC5eJTADckvXAt+MCT9AAe1kmH9NlsgBnRy13O4lhXv9SPNDfk2tU5Tdco4h/I/fXh+WuPe6/MKk+tJuoBQTGVQ5ryFmomsNiwhwtLbQ44fLVHhyqEKSEdo/107xvbzhjmY/MAzn1Pmc9rd+OhFsjUCvgqI8cWNc/E694eJqg3J2S+I6YRzG3d2tR7laUivf+J38c2XmwSyXfdRoJpyZ9TixubpPk04WSchdFlEkxPBGEWLDkWOVL1PG5ztY48di7EzM1tvAwiT1BOxl4WRZ78Ewc+C5BVHwT658rIrcKJXXI/zBMsoReQT9xsRhpozbb576wNXggJdZsd2ysQY0O6Pihz54emwigm+zPbO5n8HvlrGKf6dSsrwusUJ1BIY4wI6qjz7gweRryReDEvEzMT8Ul4mIrigRy4yL2w+03qAclz8oGwxinMvcu8vJzXg+uRm/WbOgyco4gTPQiN4NcXbzwhVtJlNWZYXCiiMb/i6IXuOzZmSjI7LqxLubD9RgOy/2890RLvVJQBBVnOowW8q+iE93CoVBr1l5D54opLS9fHYcM7ezV0Ul34qMu6K0uoBG0+aLVlZHKEecN2/VE4fh0zYEDaeqRZfNH2gnAGmokdmPtEHlp33pvJ0IFDAbxKq2CVFFdB+lCGlaLQuZ5v6Mhq4b6H8DjaGZqo/vcB/MK4pr/F1SRjLzSHyh7Ey4ogBYSOXWfaeXQiZZFoEfxIUG9PzofIA1CCFk+eZSG7bGY4wXe2Whhh5bs+cJ3duYI9SL+49WBABgB + thinking: |- + The user is asking about what framework is mentioned on the Pydantic AI page that I just fetched. Looking at the content, I can see several frameworks mentioned: + + 1. Pydantic AI itself - described as "a Python agent framework" + 2. FastAPI - mentioned as having "revolutionized web development by offering an innovative and ergonomic design" + 3. Various other frameworks/libraries mentioned like LangChain, LlamaIndex, AutoGPT, Transformers, CrewAI, Instructor + 4. Pydantic Validation is mentioned as being used by many frameworks + 5. OpenTelemetry is mentioned in relation to observability + + But the most prominently featured framework that seems to be the main comparison point is FastAPI, as the page talks about bringing "that FastAPI feeling to GenAI app and agent development." + type: thinking + - text: "Based on the page I fetched, the main framework it mentions and compares itself to is **FastAPI**. The page + states that \"FastAPI revolutionized web development by offering an innovative and ergonomic design\" and that Pydantic + AI was built with the aim \"to bring that FastAPI feeling to GenAI app and agent development.\"\n\nThe page also + mentions several other frameworks and libraries including:\n- LangChain\n- LlamaIndex \n- AutoGPT\n- Transformers\n- + CrewAI\n- Instructor\n\nIt notes that \"virtually every Python agent framework and LLM library\" uses Pydantic Validation, + which is the foundation that Pydantic AI builds upon." + type: text + id: msg_01SJJ9cZeR6yBpdy3Lf1Lx5i + model: claude-sonnet-4-20250514 + role: assistant + stop_reason: end_turn + stop_sequence: null + type: message + usage: + cache_creation: + ephemeral_1h_input_tokens: 0 + ephemeral_5m_input_tokens: 0 + cache_creation_input_tokens: 0 + cache_read_input_tokens: 0 + input_tokens: 6346 + output_tokens: 354 + service_tier: standard + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/cassettes/test_anthropic/test_anthropic_web_fetch_tool_stream.yaml b/tests/models/cassettes/test_anthropic/test_anthropic_web_fetch_tool_stream.yaml new file mode 100644 index 0000000000..d03bb3a332 --- /dev/null +++ b/tests/models/cassettes/test_anthropic/test_anthropic_web_fetch_tool_stream.yaml @@ -0,0 +1,211 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '361' + content-type: + - application/json + host: + - api.anthropic.com + method: POST + parsed_body: + max_tokens: 4096 + messages: + - content: + - text: What is the first sentence on the page https://ai.pydantic.dev? Reply with only the sentence. + type: text + role: user + model: claude-sonnet-4-0 + stream: true + thinking: + budget_tokens: 3000 + type: enabled + tool_choice: + type: auto + tools: + - name: web_fetch + type: web_fetch_20250910 + uri: https://api.anthropic.com/v1/messages?beta=true + response: + body: + string: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-sonnet-4-20250514","id":"msg_015eAVGKhBrs95jUkYb2BaDt","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":899,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"The user wants"} } + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" me to fetch"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" the content"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" from the URL https"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"://ai.pydantic.dev"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" and provide"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" only"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" the first sentence from"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" that page."} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" I need to use the web_fetch"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" tool to"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" get the content from"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" this URL."} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"EusCCkYICRgCKkAG/7zhRcmUoiMtml5iZUXVv3nqupp8kgk0nrq9zOoklaXzVCnrb9kwLNWGETIcCaAnLd0cd0ESwjslkVKdV9n8EgxKKdu8LlEvh9VGIWIaDAJ2Ja2NEacp1Am6jSIwyNO36tV+Sj+q6dWf79U+3KOIa1khXbIYarpkIViCuYQaZwpJ4Vtedrd7dLWTY2d5KtIB9Pug5UPuvepSOjyhxLaohtGxmdvZN8crGwBdTJYF9GHSli/rzvkR6CpH+ixd8iSopwFcsJgQ3j68fr/yD7cHmZ06jU3LaESVEBwTHnlK0ABiYnGvD3SvX6PgImMSQxQ1ThARFTA7DePoWw+z5DI0L2vgSun2qTYHkmGxzaEskhNIBlK9r7wS3tVcO0Di4lD/rhYV61tklL2NBWJqvm7ZCtJTN09CzPFJy7HDkg7bSINVL4kuu9gTWEtb/o40tw1b+sO62UcfxQTVFQ4Cj8D8XFZbGAE="} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: content_block_start + data: {"type":"content_block_start","index":1,"content_block":{"type":"server_tool_use","id":"srvtoolu_018ADaxdJjyZ8HXtF3sTBPNk","name":"web_fetch","input":{}} } + + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} } + + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"url\": \""} } + + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"https://ai"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":".p"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"yd"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"antic.dev\"}"} } + + event: content_block_stop + data: {"type":"content_block_stop","index":1 } + + event: content_block_start + data: {"type":"content_block_start","index":2,"content_block":{"type":"web_fetch_tool_result","tool_use_id":"srvtoolu_018ADaxdJjyZ8HXtF3sTBPNk","content":{"type":"web_fetch_result","url":"https://ai.pydantic.dev","retrieved_at":"2025-11-14T23:34:21.151000+00:00","content":{"type":"document","source":{"type":"text","media_type":"text/plain","data":"Pydantic AI\nGenAI Agent Framework, the Pydantic way\nPydantic AI is a Python agent framework designed to help you quickly, confidently, and painlessly build production grade applications and workflows with Generative AI.\nFastAPI revolutionized web development by offering an innovative and ergonomic design, built on the foundation of [Pydantic Validation](https://docs.pydantic.dev) and modern Python features like type hints.\nYet despite virtually every Python agent framework and LLM library using Pydantic Validation, when we began to use LLMs in [Pydantic Logfire](https://pydantic.dev/logfire), we couldn't find anything that gave us the same feeling.\nWe built Pydantic AI with one simple aim: to bring that FastAPI feeling to GenAI app and agent development.\nWhy use Pydantic AI\n-\nBuilt by the Pydantic Team:\n[Pydantic Validation](https://docs.pydantic.dev/latest/)is the validation layer of the OpenAI SDK, the Google ADK, the Anthropic SDK, LangChain, LlamaIndex, AutoGPT, Transformers, CrewAI, Instructor and many more. Why use the derivative when you can go straight to the source? -\nModel-agnostic: Supports virtually every\n[model](models/overview/)and provider: OpenAI, Anthropic, Gemini, DeepSeek, Grok, Cohere, Mistral, and Perplexity; Azure AI Foundry, Amazon Bedrock, Google Vertex AI, Ollama, LiteLLM, Groq, OpenRouter, Together AI, Fireworks AI, Cerebras, Hugging Face, GitHub, Heroku, Vercel, Nebius, OVHcloud, and Outlines. If your favorite model or provider is not listed, you can easily implement a[custom model](models/overview/#custom-models). -\nSeamless Observability: Tightly\n[integrates](logfire/)with[Pydantic Logfire](https://pydantic.dev/logfire), our general-purpose OpenTelemetry observability platform, for real-time debugging, evals-based performance monitoring, and behavior, tracing, and cost tracking. If you already have an observability platform that supports OTel, you can[use that too](logfire/#alternative-observability-backends). -\nFully Type-safe: Designed to give your IDE or AI coding agent as much context as possible for auto-completion and\n[type checking](agents/#static-type-checking), moving entire classes of errors from runtime to write-time for a bit of that Rust \"if it compiles, it works\" feel. -\nPowerful Evals: Enables you to systematically test and\n[evaluate](evals/)the performance and accuracy of the agentic systems you build, and monitor the performance over time in Pydantic Logfire. -\nMCP, A2A, and UI: Integrates the\n[Model Context Protocol](mcp/overview/),[Agent2Agent](a2a/), and various[UI event stream](ui/overview/)standards to give your agent access to external tools and data, let it interoperate with other agents, and build interactive applications with streaming event-based communication. -\nHuman-in-the-Loop Tool Approval: Easily lets you flag that certain tool calls\n[require approval](deferred-tools/#human-in-the-loop-tool-approval)before they can proceed, possibly depending on tool call arguments, conversation history, or user preferences. -\nDurable Execution: Enables you to build\n[durable agents](durable_execution/overview/)that can preserve their progress across transient API failures and application errors or restarts, and handle long-running, asynchronous, and human-in-the-loop workflows with production-grade reliability. -\nStreamed Outputs: Provides the ability to\n[stream](output/#streamed-results)structured output continuously, with immediate validation, ensuring real time access to generated data. -\nGraph Support: Provides a powerful way to define\n[graphs](graph/)using type hints, for use in complex applications where standard control flow can degrade to spaghetti code.\nRealistically though, no list is going to be as convincing as [giving it a try](#next-steps) and seeing how it makes you feel!\nSign up for our newsletter, The Pydantic Stack, with updates & tutorials on Pydantic AI, Logfire, and Pydantic:\nHello World Example\nHere's a minimal example of Pydantic AI:\n[Learn about Gateway](gateway)hello_world.py\nfrom pydantic_ai import Agent\nagent = Agent( # (1)!\n'gateway/anthropic:claude-sonnet-4-0',\ninstructions='Be concise, reply with one sentence.', # (2)!\n)\nresult = agent.run_sync('Where does \"hello world\" come from?') # (3)!\nprint(result.output)\n\"\"\"\nThe first known use of \"hello, world\" was in a 1974 textbook about the C programming language.\n\"\"\"\n- We configure the agent to use\n[Anthropic's Claude Sonnet 4.0](api/models/anthropic/)model, but you can also set the model when running the agent. - Register static\n[instructions](agents/#instructions)using a keyword argument to the agent. [Run the agent](agents/#running-agents)synchronously, starting a conversation with the LLM.\nfrom pydantic_ai import Agent\nagent = Agent( # (1)!\n'anthropic:claude-sonnet-4-0',\ninstructions='Be concise, reply with one sentence.', # (2)!\n)\nresult = agent.run_sync('Where does \"hello world\" come from?') # (3)!\nprint(result.output)\n\"\"\"\nThe first known use of \"hello, world\" was in a 1974 textbook about the C programming language.\n\"\"\"\n- We configure the agent to use\n[Anthropic's Claude Sonnet 4.0](api/models/anthropic/)model, but you can also set the model when running the agent. - Register static\n[instructions](agents/#instructions)using a keyword argument to the agent. [Run the agent](agents/#running-agents)synchronously, starting a conversation with the LLM.\n(This example is complete, it can be run \"as is\", assuming you've [installed the pydantic_ai package](install/))\nThe exchange will be very short: Pydantic AI will send the instructions and the user prompt to the LLM, and the model will return a text response.\nNot very interesting yet, but we can easily add [tools](tools/), [dynamic instructions](agents/#instructions), and [structured outputs](output/) to build more powerful agents.\nTools & Dependency Injection Example\nHere is a concise example using Pydantic AI to build a support agent for a bank:\n[Learn about Gateway](gateway)bank_support.py\nfrom dataclasses import dataclass\nfrom pydantic import BaseModel, Field\nfrom pydantic_ai import Agent, RunContext\nfrom bank_database import DatabaseConn\n@dataclass\nclass SupportDependencies: # (3)!\ncustomer_id: int\ndb: DatabaseConn # (12)!\nclass SupportOutput(BaseModel): # (13)!\nsupport_advice: str = Field(description='Advice returned to the customer')\nblock_card: bool = Field(description=\"Whether to block the customer's card\")\nrisk: int = Field(description='Risk level of query', ge=0, le=10)\nsupport_agent = Agent( # (1)!\n'gateway/openai:gpt-5', # (2)!\ndeps_type=SupportDependencies,\noutput_type=SupportOutput, # (9)!\ninstructions=( # (4)!\n'You are a support agent in our bank, give the '\n'customer support and judge the risk level of their query.'\n),\n)\n@support_agent.instructions # (5)!\nasync def add_customer_name(ctx: RunContext[SupportDependencies]) -> str:\ncustomer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id)\nreturn f\"The customer's name is {customer_name!r}\"\n@support_agent.tool # (6)!\nasync def customer_balance(\nctx: RunContext[SupportDependencies], include_pending: bool\n) -> float:\n\"\"\"Returns the customer's current account balance.\"\"\" # (7)!\nreturn await ctx.deps.db.customer_balance(\nid=ctx.deps.customer_id,\ninclude_pending=include_pending,\n)\n... # (11)!\nasync def main():\ndeps = SupportDependencies(customer_id=123, db=DatabaseConn())\nresult = await support_agent.run('What is my balance?', deps=deps) # (8)!\nprint(result.output) # (10)!\n\"\"\"\nsupport_advice='Hello John, your current account balance, including pending transactions, is $123.45.' block_card=False risk=1\n\"\"\"\nresult = await support_agent.run('I just lost my card!', deps=deps)\nprint(result.output)\n\"\"\"\nsupport_advice=\"I'm sorry to hear that, John. We are temporarily blocking your card to prevent unauthorized transactions.\" block_card=True risk=8\n\"\"\"\n- This\n[agent](agents/)will act as first-tier support in a bank. Agents are generic in the type of dependencies they accept and the type of output they return. In this case, the support agent has typeAgent[SupportDependencies, SupportOutput]\n. - Here we configure the agent to use\n[OpenAI's GPT-5 model](api/models/openai/), you can also set the model when running the agent. - The\nSupportDependencies\ndataclass is used to pass data, connections, and logic into the model that will be needed when running[instructions](agents/#instructions)and[tool](tools/)functions. Pydantic AI's system of dependency injection provides a[type-safe](agents/#static-type-checking)way to customise the behavior of your agents, and can be especially useful when running[unit tests](testing/)and evals. - Static\n[instructions](agents/#instructions)can be registered with theto the agent.instructions\nkeyword argument - Dynamic\n[instructions](agents/#instructions)can be registered with thedecorator, and can make use of dependency injection. Dependencies are carried via the@agent.instructions\nargument, which is parameterized with theRunContext\ndeps_type\nfrom above. If the type annotation here is wrong, static type checkers will catch it. - The\ndecorator let you register functions which the LLM may call while responding to a user. Again, dependencies are carried via@agent.tool\n, any other arguments become the tool schema passed to the LLM. Pydantic is used to validate these arguments, and errors are passed back to the LLM so it can retry.RunContext\n- The docstring of a tool is also passed to the LLM as the description of the tool. Parameter descriptions are\n[extracted](tools/#function-tools-and-schema)from the docstring and added to the parameter schema sent to the LLM. [Run the agent](agents/#running-agents)asynchronously, conducting a conversation with the LLM until a final response is reached. Even in this fairly simple case, the agent will exchange multiple messages with the LLM as tools are called to retrieve an output.- The response from the agent will be guaranteed to be a\nSupportOutput\n. If validation fails[reflection](agents/#reflection-and-self-correction), the agent is prompted to try again. - The output will be validated with Pydantic to guarantee it is a\nSupportOutput\n, since the agent is generic, it'll also be typed as aSupportOutput\nto aid with static type checking. - In a real use case, you'd add more tools and longer instructions to the agent to extend the context it's equipped with and support it can provide.\n- This is a simple sketch of a database connection, used to keep the example short and readable. In reality, you'd be connecting to an external database (e.g. PostgreSQL) to get information about customers.\n- This\n[Pydantic](https://docs.pydantic.dev)model is used to constrain the structured data returned by the agent. From this simple definition, Pydantic builds the JSON Schema that tells the LLM how to return the data, and performs validation to guarantee the data is correct at the end of the run.\nfrom dataclasses import dataclass\nfrom pydantic import BaseModel, Field\nfrom pydantic_ai import Agent, RunContext\nfrom bank_database import DatabaseConn\n@dataclass\nclass SupportDependencies: # (3)!\ncustomer_id: int\ndb: DatabaseConn # (12)!\nclass SupportOutput(BaseModel): # (13)!\nsupport_advice: str = Field(description='Advice returned to the customer')\nblock_card: bool = Field(description=\"Whether to block the customer's card\")\nrisk: int = Field(description='Risk level of query', ge=0, le=10)\nsupport_agent = Agent( # (1)!\n'openai:gpt-5', # (2)!\ndeps_type=SupportDependencies,\noutput_type=SupportOutput, # (9)!\ninstructions=( # (4)!\n'You are a support agent in our bank, give the '\n'customer support and judge the risk level of their query.'\n),\n)\n@support_agent.instructions # (5)!\nasync def add_customer_name(ctx: RunContext[SupportDependencies]) -> str:\ncustomer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id)\nreturn f\"The customer's name is {customer_name!r}\"\n@support_agent.tool # (6)!\nasync def customer_balance(\nctx: RunContext[SupportDependencies], include_pending: bool\n) -> float:\n\"\"\"Returns the customer's current account balance.\"\"\" # (7)!\nreturn await ctx.deps.db.customer_balance(\nid=ctx.deps.customer_id,\ninclude_pending=include_pending,\n)\n... # (11)!\nasync def main():\ndeps = SupportDependencies(customer_id=123, db=DatabaseConn())\nresult = await support_agent.run('What is my balance?', deps=deps) # (8)!\nprint(result.output) # (10)!\n\"\"\"\nsupport_advice='Hello John, your current account balance, including pending transactions, is $123.45.' block_card=False risk=1\n\"\"\"\nresult = await support_agent.run('I just lost my card!', deps=deps)\nprint(result.output)\n\"\"\"\nsupport_advice=\"I'm sorry to hear that, John. We are temporarily blocking your card to prevent unauthorized transactions.\" block_card=True risk=8\n\"\"\"\n- This\n[agent](agents/)will act as first-tier support in a bank. Agents are generic in the type of dependencies they accept and the type of output they return. In this case, the support agent has typeAgent[SupportDependencies, SupportOutput]\n. - Here we configure the agent to use\n[OpenAI's GPT-5 model](api/models/openai/), you can also set the model when running the agent. - The\nSupportDependencies\ndataclass is used to pass data, connections, and logic into the model that will be needed when running[instructions](agents/#instructions)and[tool](tools/)functions. Pydantic AI's system of dependency injection provides a[type-safe](agents/#static-type-checking)way to customise the behavior of your agents, and can be especially useful when running[unit tests](testing/)and evals. - Static\n[instructions](agents/#instructions)can be registered with theto the agent.instructions\nkeyword argument - Dynamic\n[instructions](agents/#instructions)can be registered with thedecorator, and can make use of dependency injection. Dependencies are carried via the@agent.instructions\nargument, which is parameterized with theRunContext\ndeps_type\nfrom above. If the type annotation here is wrong, static type checkers will catch it. - The\ndecorator let you register functions which the LLM may call while responding to a user. Again, dependencies are carried via@agent.tool\n, any other arguments become the tool schema passed to the LLM. Pydantic is used to validate these arguments, and errors are passed back to the LLM so it can retry.RunContext\n- The docstring of a tool is also passed to the LLM as the description of the tool. Parameter descriptions are\n[extracted](tools/#function-tools-and-schema)from the docstring and added to the parameter schema sent to the LLM. [Run the agent](agents/#running-agents)asynchronously, conducting a conversation with the LLM until a final response is reached. Even in this fairly simple case, the agent will exchange multiple messages with the LLM as tools are called to retrieve an output.- The response from the agent will be guaranteed to be a\nSupportOutput\n. If validation fails[reflection](agents/#reflection-and-self-correction), the agent is prompted to try again. - The output will be validated with Pydantic to guarantee it is a\nSupportOutput\n, since the agent is generic, it'll also be typed as aSupportOutput\nto aid with static type checking. - In a real use case, you'd add more tools and longer instructions to the agent to extend the context it's equipped with and support it can provide.\n- This is a simple sketch of a database connection, used to keep the example short and readable. In reality, you'd be connecting to an external database (e.g. PostgreSQL) to get information about customers.\n- This\n[Pydantic](https://docs.pydantic.dev)model is used to constrain the structured data returned by the agent. From this simple definition, Pydantic builds the JSON Schema that tells the LLM how to return the data, and performs validation to guarantee the data is correct at the end of the run.\nComplete bank_support.py\nexample\nThe code included here is incomplete for the sake of brevity (the definition of DatabaseConn\nis missing); you can find the complete bank_support.py\nexample [here](examples/bank-support/).\nInstrumentation with Pydantic Logfire\nEven a simple agent with just a handful of tools can result in a lot of back-and-forth with the LLM, making it nearly impossible to be confident of what's going on just from reading the code. To understand the flow of the above runs, we can watch the agent in action using Pydantic Logfire.\nTo do this, we need to [set up Logfire](logfire/#using-logfire), and add the following to our code:\n[Learn about Gateway](gateway)bank_support_with_logfire.py\n...\nfrom pydantic_ai import Agent, RunContext\nfrom bank_database import DatabaseConn\nimport logfire\nlogfire.configure() # (1)!\nlogfire.instrument_pydantic_ai() # (2)!\nlogfire.instrument_asyncpg() # (3)!\n...\nsupport_agent = Agent(\n'gateway/openai:gpt-5',\ndeps_type=SupportDependencies,\noutput_type=SupportOutput,\nsystem_prompt=(\n'You are a support agent in our bank, give the '\n'customer support and judge the risk level of their query.'\n),\n)\n- Configure the Logfire SDK, this will fail if project is not set up.\n- This will instrument all Pydantic AI agents used from here on out. If you want to instrument only a specific agent, you can pass the\nto the agent.instrument=True\nkeyword argument - In our demo,\nDatabaseConn\nusesto connect to a PostgreSQL database, soasyncpg\nis used to log the database queries.logfire.instrument_asyncpg()\n...\nfrom pydantic_ai import Agent, RunContext\nfrom bank_database import DatabaseConn\nimport logfire\nlogfire.configure() # (1)!\nlogfire.instrument_pydantic_ai() # (2)!\nlogfire.instrument_asyncpg() # (3)!\n...\nsupport_agent = Agent(\n'openai:gpt-5',\ndeps_type=SupportDependencies,\noutput_type=SupportOutput,\nsystem_prompt=(\n'You are a support agent in our bank, give the '\n'customer support and judge the risk level of their query.'\n),\n)\n- Configure the Logfire SDK, this will fail if project is not set up.\n- This will instrument all Pydantic AI agents used from here on out. If you want to instrument only a specific agent, you can pass the\nto the agent.instrument=True\nkeyword argument - In our demo,\nDatabaseConn\nusesto connect to a PostgreSQL database, soasyncpg\nis used to log the database queries.logfire.instrument_asyncpg()\nThat's enough to get the following view of your agent in action:\nSee [Monitoring and Performance](logfire/) to learn more.\nllms.txt\nThe Pydantic AI documentation is available in the [llms.txt](https://llmstxt.org/) format.\nThis format is defined in Markdown and suited for LLMs and AI coding assistants and agents.\nTwo formats are available:\n: a file containing a brief description of the project, along with links to the different sections of the documentation. The structure of this file is described in detailsllms.txt\n[here](https://llmstxt.org/#format).: Similar to thellms-full.txt\nllms.txt\nfile, but every link content is included. Note that this file may be too large for some LLMs.\nAs of today, these files are not automatically leveraged by IDEs or coding agents, but they will use it if you provide a link or the full text.\nNext Steps\nTo try Pydantic AI for yourself, [install it](install/) and follow the instructions [in the examples](examples/setup/).\nRead the [docs](agents/) to learn more about building applications with Pydantic AI.\nRead the [API Reference](api/agent/) to understand Pydantic AI's interface.\nJoin [ Slack](https://logfire.pydantic.dev/docs/join-slack/) or file an issue on [ GitHub](https://github.com/pydantic/pydantic-ai/issues) if you have any questions."},"title":"Pydantic AI"}}} } + + event: content_block_stop + data: {"type":"content_block_stop","index":2 } + + event: content_block_start + data: {"type":"content_block_start","index":3,"content_block":{"type":"text","text":""} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"P"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"ydantic AI is a"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" Python"}} + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" agent"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" framework"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" designe"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"d to help"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" you quickly"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":","} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" confi"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"dently,"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" and pain"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"lessly build production"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" grade"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" applications"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" an"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"d workflows"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" with"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" Gener"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"ative AI."} } + + event: content_block_stop + data: {"type":"content_block_stop","index":3 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":7244,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":153,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":1}} } + + event: message_stop + data: {"type":"message_stop" } + + headers: + cache-control: + - no-cache + connection: + - keep-alive + content-type: + - text/event-stream; charset=utf-8 + retry-after: + - '15' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + status: + code: 200 + message: OK +version: 1 +... diff --git a/tests/models/cassettes/test_google/test_google_model_url_context_tool.yaml b/tests/models/cassettes/test_google/test_google_model_web_fetch_tool.yaml similarity index 100% rename from tests/models/cassettes/test_google/test_google_model_url_context_tool.yaml rename to tests/models/cassettes/test_google/test_google_model_web_fetch_tool.yaml diff --git a/tests/models/cassettes/test_google/test_google_model_web_fetch_tool[False].yaml b/tests/models/cassettes/test_google/test_google_model_web_fetch_tool[False].yaml new file mode 100644 index 0000000000..d8778800aa --- /dev/null +++ b/tests/models/cassettes/test_google/test_google_model_web_fetch_tool[False].yaml @@ -0,0 +1,76 @@ +interactions: +- request: + body: '{"contents": [{"parts": [{"text": "What is the first sentence on the page + https://ai.pydantic.dev? Reply with only the sentence."}], "role": "user"}], + "systemInstruction": {"parts": [{"text": "You are a helpful chatbot."}], "role": + "user"}, "tools": [{"urlContext": {}}], "generationConfig": {}}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '295' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + response: + body: + string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\"\ + : [\n {\n \"text\": \"Pydantic AI is a Python agent framework\ + \ designed to make it less painful to build production grade applications\ + \ with Generative AI.\"\n }\n ],\n \"role\": \"model\"\ + \n },\n \"finishReason\": \"STOP\",\n \"index\": 0,\n \ + \ \"groundingMetadata\": {\n \"groundingChunks\": [\n {\n\ + \ \"web\": {\n \"uri\": \"https://ai.pydantic.dev\"\ + ,\n \"title\": \"Pydantic AI\"\n }\n }\n\ + \ ],\n \"groundingSupports\": [\n {\n \"\ + segment\": {\n \"endIndex\": 131,\n \"text\": \"\ + Pydantic AI is a Python agent framework designed to make it less painful to\ + \ build production grade applications with Generative AI.\"\n },\n\ + \ \"groundingChunkIndices\": [\n 0\n ]\n\ + \ }\n ]\n },\n \"urlContextMetadata\": {\n \ + \ \"urlMetadata\": [\n {\n \"retrievedUrl\": \"https://ai.pydantic.dev\"\ + ,\n \"urlRetrievalStatus\": \"URL_RETRIEVAL_STATUS_SUCCESS\"\n\ + \ }\n ]\n }\n }\n ],\n \"usageMetadata\": {\n \ + \ \"promptTokenCount\": 32,\n \"candidatesTokenCount\": 41,\n \"totalTokenCount\"\ + : 2515,\n \"promptTokensDetails\": [\n {\n \"modality\": \"\ + TEXT\",\n \"tokenCount\": 32\n }\n ],\n \"toolUsePromptTokenCount\"\ + : 2395,\n \"toolUsePromptTokensDetails\": [\n {\n \"modality\"\ + : \"TEXT\",\n \"tokenCount\": 2395\n }\n ],\n \"thoughtsTokenCount\"\ + : 47\n },\n \"modelVersion\": \"gemini-2.5-flash\",\n \"responseId\": \"\ + qgqkaI-iDLrTjMcP0bP24A4\"\n}\n" + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Content-Type: + - application/json; charset=UTF-8 + Date: + - Tue, 19 Aug 2025 05:24:58 GMT + Server: + - scaffolding on HTTPServer2 + Server-Timing: + - gfet4t7; dur=9182 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '1627' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/cassettes/test_google/test_google_model_web_fetch_tool[True].yaml b/tests/models/cassettes/test_google/test_google_model_web_fetch_tool[True].yaml new file mode 100644 index 0000000000..d8778800aa --- /dev/null +++ b/tests/models/cassettes/test_google/test_google_model_web_fetch_tool[True].yaml @@ -0,0 +1,76 @@ +interactions: +- request: + body: '{"contents": [{"parts": [{"text": "What is the first sentence on the page + https://ai.pydantic.dev? Reply with only the sentence."}], "role": "user"}], + "systemInstruction": {"parts": [{"text": "You are a helpful chatbot."}], "role": + "user"}, "tools": [{"urlContext": {}}], "generationConfig": {}}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '295' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + response: + body: + string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\"\ + : [\n {\n \"text\": \"Pydantic AI is a Python agent framework\ + \ designed to make it less painful to build production grade applications\ + \ with Generative AI.\"\n }\n ],\n \"role\": \"model\"\ + \n },\n \"finishReason\": \"STOP\",\n \"index\": 0,\n \ + \ \"groundingMetadata\": {\n \"groundingChunks\": [\n {\n\ + \ \"web\": {\n \"uri\": \"https://ai.pydantic.dev\"\ + ,\n \"title\": \"Pydantic AI\"\n }\n }\n\ + \ ],\n \"groundingSupports\": [\n {\n \"\ + segment\": {\n \"endIndex\": 131,\n \"text\": \"\ + Pydantic AI is a Python agent framework designed to make it less painful to\ + \ build production grade applications with Generative AI.\"\n },\n\ + \ \"groundingChunkIndices\": [\n 0\n ]\n\ + \ }\n ]\n },\n \"urlContextMetadata\": {\n \ + \ \"urlMetadata\": [\n {\n \"retrievedUrl\": \"https://ai.pydantic.dev\"\ + ,\n \"urlRetrievalStatus\": \"URL_RETRIEVAL_STATUS_SUCCESS\"\n\ + \ }\n ]\n }\n }\n ],\n \"usageMetadata\": {\n \ + \ \"promptTokenCount\": 32,\n \"candidatesTokenCount\": 41,\n \"totalTokenCount\"\ + : 2515,\n \"promptTokensDetails\": [\n {\n \"modality\": \"\ + TEXT\",\n \"tokenCount\": 32\n }\n ],\n \"toolUsePromptTokenCount\"\ + : 2395,\n \"toolUsePromptTokensDetails\": [\n {\n \"modality\"\ + : \"TEXT\",\n \"tokenCount\": 2395\n }\n ],\n \"thoughtsTokenCount\"\ + : 47\n },\n \"modelVersion\": \"gemini-2.5-flash\",\n \"responseId\": \"\ + qgqkaI-iDLrTjMcP0bP24A4\"\n}\n" + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Content-Type: + - application/json; charset=UTF-8 + Date: + - Tue, 19 Aug 2025 05:24:58 GMT + Server: + - scaffolding on HTTPServer2 + Server-Timing: + - gfet4t7; dur=9182 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '1627' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/test_anthropic.py b/tests/models/test_anthropic.py index 86ba5a68d3..6aa6172751 100644 --- a/tests/models/test_anthropic.py +++ b/tests/models/test_anthropic.py @@ -44,7 +44,7 @@ UsageLimitExceeded, UserPromptPart, ) -from pydantic_ai.builtin_tools import CodeExecutionTool, MCPServerTool, MemoryTool, WebSearchTool +from pydantic_ai.builtin_tools import CodeExecutionTool, MCPServerTool, MemoryTool, WebFetchTool, WebSearchTool from pydantic_ai.exceptions import UserError from pydantic_ai.messages import ( BuiltinToolCallEvent, # pyright: ignore[reportDeprecated] @@ -3650,6 +3650,864 @@ async def test_anthropic_model_web_search_tool_stream(allow_model_requests: None ) +@pytest.mark.vcr() +async def test_anthropic_web_fetch_tool(allow_model_requests: None, anthropic_api_key: str): + m = AnthropicModel('claude-sonnet-4-0', provider=AnthropicProvider(api_key=anthropic_api_key)) + settings = AnthropicModelSettings(anthropic_thinking={'type': 'enabled', 'budget_tokens': 3000}) + agent = Agent(m, builtin_tools=[WebFetchTool()], model_settings=settings) + + result = await agent.run( + 'What is the first sentence on the page https://ai.pydantic.dev? Reply with only the sentence.' + ) + + assert result.output == snapshot( + 'Pydantic AI is a Python agent framework designed to help you quickly, confidently, and painlessly build production grade applications and workflows with Generative AI.' + ) + + assert result.all_messages() == snapshot( + [ + ModelRequest( + parts=[ + UserPromptPart( + content='What is the first sentence on the page https://ai.pydantic.dev? Reply with only the sentence.', + timestamp=IsDatetime(), + ) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ThinkingPart( + content="""\ +The user is asking me to fetch the content from https://ai.pydantic.dev and return only the first sentence on that page. I need to use the web_fetch tool to get the content from this URL, then identify the first sentence and return only that sentence. + +Let me fetch the page first.\ +""", + signature='EsIDCkYICRgCKkAKi/j4a8lGN12CjyS27ZXcPkXHGyTbn1vJENJz+AjinyTnsrynMEhidWT5IMNAs0TDgwSwPLNmgq4MsPkVekB8EgxetaK+Nhg8wUdhTEAaDMukODgr3JaYHZwVEiIwgKBckFLJ/C7wCD9oGCIECbqpaeEuWQ8BH3Hev6wpuc+66Wu7AJM1jGH60BpsUovnKqkCrHNq6b1SDT41cm2w7cyxZggrX6crzYh0fAkZ+VC6FBjy6mJikZtX6reKD+064KZ4F1oe4Qd40EBp/wHvD7oPV/fhGut1fzwl48ZgB8uzJb3tHr9MBjs4PVTsvKstpHKpOo6NLvCknQJ/0730OTENp/JOR6h6RUl6kMl5OrHTvsDEYpselUBPtLikm9p4t+d8CxqGm/B1kg1wN3FGJK31PD3veYIOO4hBirFPXWd+AiB1rZP++2QjToZ9lD2xqP/Q3vWEU+/Ryp6uzaRFWPVQkIr+mzpIaJsYuKDiyduxF4LD/hdMTV7IVDtconeQIPQJRhuO6nICBEuqb0uIotPDnCU6iI2l9OyEeKJM0RS6/NTNG8DZnvyVJ8gGKbtZKSHK6KKsdH0f7d+DGAE=', + provider_name='anthropic', + ), + BuiltinToolCallPart( + tool_name='web_fetch', + args={'url': 'https://ai.pydantic.dev'}, + tool_call_id=IsStr(), + provider_name='anthropic', + ), + BuiltinToolReturnPart( + tool_name='web_fetch', + content={ + 'content': { + 'citations': None, + 'source': { + 'data': IsStr(), + 'media_type': 'text/plain', + 'type': 'text', + }, + 'title': 'Pydantic AI', + 'type': 'document', + }, + 'retrieved_at': IsStr(), + 'type': 'web_fetch_result', + 'url': 'https://ai.pydantic.dev', + }, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='anthropic', + ), + TextPart( + content='Pydantic AI is a Python agent framework designed to help you quickly, confidently, and painlessly build production grade applications and workflows with Generative AI.' + ), + ], + usage=RequestUsage( + input_tokens=7262, + output_tokens=171, + details={ + 'cache_creation_input_tokens': 0, + 'cache_read_input_tokens': 0, + 'input_tokens': 7262, + 'output_tokens': 171, + }, + ), + model_name='claude-sonnet-4-20250514', + timestamp=IsDatetime(), + provider_name='anthropic', + provider_details={'finish_reason': 'end_turn'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + # Second run to test message replay (multi-turn conversation) + result2 = await agent.run( + 'Based on the page you just fetched, what framework does it mention?', + message_history=result.all_messages(), + ) + + assert 'Pydantic AI' in result2.output or 'pydantic' in result2.output.lower() + assert result2.all_messages() == snapshot( + [ + ModelRequest( + parts=[ + UserPromptPart( + content='What is the first sentence on the page https://ai.pydantic.dev? Reply with only the sentence.', + timestamp=IsDatetime(), + ) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ThinkingPart( + content="""\ +The user is asking me to fetch the content from https://ai.pydantic.dev and return only the first sentence on that page. I need to use the web_fetch tool to get the content from this URL, then identify the first sentence and return only that sentence. + +Let me fetch the page first.\ +""", + signature='EsIDCkYICRgCKkAKi/j4a8lGN12CjyS27ZXcPkXHGyTbn1vJENJz+AjinyTnsrynMEhidWT5IMNAs0TDgwSwPLNmgq4MsPkVekB8EgxetaK+Nhg8wUdhTEAaDMukODgr3JaYHZwVEiIwgKBckFLJ/C7wCD9oGCIECbqpaeEuWQ8BH3Hev6wpuc+66Wu7AJM1jGH60BpsUovnKqkCrHNq6b1SDT41cm2w7cyxZggrX6crzYh0fAkZ+VC6FBjy6mJikZtX6reKD+064KZ4F1oe4Qd40EBp/wHvD7oPV/fhGut1fzwl48ZgB8uzJb3tHr9MBjs4PVTsvKstpHKpOo6NLvCknQJ/0730OTENp/JOR6h6RUl6kMl5OrHTvsDEYpselUBPtLikm9p4t+d8CxqGm/B1kg1wN3FGJK31PD3veYIOO4hBirFPXWd+AiB1rZP++2QjToZ9lD2xqP/Q3vWEU+/Ryp6uzaRFWPVQkIr+mzpIaJsYuKDiyduxF4LD/hdMTV7IVDtconeQIPQJRhuO6nICBEuqb0uIotPDnCU6iI2l9OyEeKJM0RS6/NTNG8DZnvyVJ8gGKbtZKSHK6KKsdH0f7d+DGAE=', + provider_name='anthropic', + ), + BuiltinToolCallPart( + tool_name='web_fetch', + args={'url': 'https://ai.pydantic.dev'}, + tool_call_id=IsStr(), + provider_name='anthropic', + ), + BuiltinToolReturnPart( + tool_name='web_fetch', + content={ + 'content': { + 'citations': None, + 'source': { + 'data': IsStr(), + 'media_type': 'text/plain', + 'type': 'text', + }, + 'title': 'Pydantic AI', + 'type': 'document', + }, + 'retrieved_at': IsStr(), + 'type': 'web_fetch_result', + 'url': 'https://ai.pydantic.dev', + }, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='anthropic', + ), + TextPart( + content='Pydantic AI is a Python agent framework designed to help you quickly, confidently, and painlessly build production grade applications and workflows with Generative AI.' + ), + ], + usage=RequestUsage( + input_tokens=7262, + output_tokens=171, + details={ + 'cache_creation_input_tokens': 0, + 'cache_read_input_tokens': 0, + 'input_tokens': 7262, + 'output_tokens': 171, + }, + ), + model_name='claude-sonnet-4-20250514', + timestamp=IsDatetime(), + provider_name='anthropic', + provider_details={'finish_reason': 'end_turn'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ModelRequest( + parts=[ + UserPromptPart( + content='Based on the page you just fetched, what framework does it mention?', + timestamp=IsDatetime(), + ) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ThinkingPart( + content="""\ +The user is asking about what framework is mentioned on the Pydantic AI page that I just fetched. Looking at the content, I can see several frameworks mentioned: + +1. Pydantic AI itself - described as "a Python agent framework" +2. FastAPI - mentioned as having "revolutionized web development by offering an innovative and ergonomic design" +3. Various other frameworks/libraries mentioned like LangChain, LlamaIndex, AutoGPT, Transformers, CrewAI, Instructor +4. Pydantic Validation is mentioned as being used by many frameworks +5. OpenTelemetry is mentioned in relation to observability + +But the most prominently featured framework that seems to be the main comparison point is FastAPI, as the page talks about bringing "that FastAPI feeling to GenAI app and agent development."\ +""", + signature='ErIHCkYICRgCKkDZrwipmaxoEat4WffzPSjVzIuSQWM2sHE6FLC2wt5S2qiJN2MQh//EImuLE9I2ssZjTMxGXZV+esnf5ipnzbvnEgxfcXs2ax8vnLdroxMaDCpqvdPKpCP3Qi0txCIw55NdOjY30P3/yRL9RF8sPGioyitlzkhSpf+PuC3YXwz4N0hoy8zVY1MHecwc60vcKpkGxtZsfqmAuJwjeGRr/Ugxcxd69+0X/Y9pojMiklNHq9otW+ehDX0rR0EzfdN/2jNOs3bOrzfy9jmvYE5FU2c5e0JpMP3LH0LrFvZYkSh7RkbhYuHvrOqohlE3BhpflrszowmiozUk+aG4wSqx5Dtxo9W7jfeU4wduy6OyEFdIqdYdTMR8VVf9Qnd5bLX4rY09xcGQc4JcX2mFjdSR2WgEJM7p5lytlN5unH3selWBVPbCj7ogU8DbT9zhY3zkDW1dMt2vNbWNaY4gVrLwi42qBJvjC5eJTADckvXAt+MCT9AAe1kmH9NlsgBnRy13O4lhXv9SPNDfk2tU5Tdco4h/I/fXh+WuPe6/MKk+tJuoBQTGVQ5ryFmomsNiwhwtLbQ44fLVHhyqEKSEdo/107xvbzhjmY/MAzn1Pmc9rd+OhFsjUCvgqI8cWNc/E694eJqg3J2S+I6YRzG3d2tR7laUivf+J38c2XmwSyXfdRoJpyZ9TixubpPk04WSchdFlEkxPBGEWLDkWOVL1PG5ztY48di7EzM1tvAwiT1BOxl4WRZ78Ewc+C5BVHwT658rIrcKJXXI/zBMsoReQT9xsRhpozbb576wNXggJdZsd2ysQY0O6Pihz54emwigm+zPbO5n8HvlrGKf6dSsrwusUJ1BIY4wI6qjz7gweRryReDEvEzMT8Ul4mIrigRy4yL2w+03qAclz8oGwxinMvcu8vJzXg+uRm/WbOgyco4gTPQiN4NcXbzwhVtJlNWZYXCiiMb/i6IXuOzZmSjI7LqxLubD9RgOy/2890RLvVJQBBVnOowW8q+iE93CoVBr1l5D54opLS9fHYcM7ezV0Ul34qMu6K0uoBG0+aLVlZHKEecN2/VE4fh0zYEDaeqRZfNH2gnAGmokdmPtEHlp33pvJ0IFDAbxKq2CVFFdB+lCGlaLQuZ5v6Mhq4b6H8DjaGZqo/vcB/MK4pr/F1SRjLzSHyh7Ey4ogBYSOXWfaeXQiZZFoEfxIUG9PzofIA1CCFk+eZSG7bGY4wXe2Whhh5bs+cJ3duYI9SL+49WBABgB', + provider_name='anthropic', + ), + TextPart( + content="""\ +Based on the page I fetched, the main framework it mentions and compares itself to is **FastAPI**. The page states that "FastAPI revolutionized web development by offering an innovative and ergonomic design" and that Pydantic AI was built with the aim "to bring that FastAPI feeling to GenAI app and agent development." + +The page also mentions several other frameworks and libraries including: +- LangChain +- LlamaIndex \n\ +- AutoGPT +- Transformers +- CrewAI +- Instructor + +It notes that "virtually every Python agent framework and LLM library" uses Pydantic Validation, which is the foundation that Pydantic AI builds upon.\ +""" + ), + ], + usage=RequestUsage( + input_tokens=6346, + output_tokens=354, + details={ + 'cache_creation_input_tokens': 0, + 'cache_read_input_tokens': 0, + 'input_tokens': 6346, + 'output_tokens': 354, + }, + ), + model_name='claude-sonnet-4-20250514', + timestamp=IsDatetime(), + provider_name='anthropic', + provider_details={'finish_reason': 'end_turn'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + +@pytest.mark.vcr() +async def test_anthropic_web_fetch_tool_stream( + allow_model_requests: None, anthropic_api_key: str +): # pragma: lax no cover + from pydantic_ai.messages import PartDeltaEvent, PartStartEvent + + m = AnthropicModel('claude-sonnet-4-0', provider=AnthropicProvider(api_key=anthropic_api_key)) + settings = AnthropicModelSettings(anthropic_thinking={'type': 'enabled', 'budget_tokens': 3000}) + agent = Agent(m, builtin_tools=[WebFetchTool()], model_settings=settings) + + # Iterate through the stream to ensure streaming code paths are covered + event_parts: list[Any] = [] + async with agent.iter( # pragma: lax no cover + user_prompt='What is the first sentence on the page https://ai.pydantic.dev? Reply with only the sentence.' + ) as agent_run: + async for node in agent_run: # pragma: lax no cover + if Agent.is_model_request_node(node) or Agent.is_call_tools_node(node): # pragma: lax no cover + async with node.stream(agent_run.ctx) as request_stream: # pragma: lax no cover + async for event in request_stream: # pragma: lax no cover + if ( # pragma: lax no cover + isinstance(event, PartStartEvent) + and isinstance(event.part, BuiltinToolCallPart | BuiltinToolReturnPart) + ) or isinstance(event, PartDeltaEvent): + event_parts.append(event) + + assert agent_run.result is not None + assert agent_run.result.output == snapshot( + 'Pydantic AI is a Python agent framework designed to help you quickly, confidently, and painlessly build production grade applications and workflows with Generative AI.' + ) + + assert agent_run.result.all_messages() == snapshot( + [ + ModelRequest( + parts=[ + UserPromptPart( + content='What is the first sentence on the page https://ai.pydantic.dev? Reply with only the sentence.', + timestamp=IsDatetime(), + ) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ThinkingPart( + content='The user wants me to fetch the content from the URL https://ai.pydantic.dev and provide only the first sentence from that page. I need to use the web_fetch tool to get the content from this URL.', + signature='EusCCkYICRgCKkAG/7zhRcmUoiMtml5iZUXVv3nqupp8kgk0nrq9zOoklaXzVCnrb9kwLNWGETIcCaAnLd0cd0ESwjslkVKdV9n8EgxKKdu8LlEvh9VGIWIaDAJ2Ja2NEacp1Am6jSIwyNO36tV+Sj+q6dWf79U+3KOIa1khXbIYarpkIViCuYQaZwpJ4Vtedrd7dLWTY2d5KtIB9Pug5UPuvepSOjyhxLaohtGxmdvZN8crGwBdTJYF9GHSli/rzvkR6CpH+ixd8iSopwFcsJgQ3j68fr/yD7cHmZ06jU3LaESVEBwTHnlK0ABiYnGvD3SvX6PgImMSQxQ1ThARFTA7DePoWw+z5DI0L2vgSun2qTYHkmGxzaEskhNIBlK9r7wS3tVcO0Di4lD/rhYV61tklL2NBWJqvm7ZCtJTN09CzPFJy7HDkg7bSINVL4kuu9gTWEtb/o40tw1b+sO62UcfxQTVFQ4Cj8D8XFZbGAE=', + provider_name='anthropic', + ), + BuiltinToolCallPart( + tool_name='web_fetch', + args='{"url": "https://ai.pydantic.dev"}', + tool_call_id=IsStr(), + provider_name='anthropic', + ), + BuiltinToolReturnPart( + tool_name='web_fetch', + content={ + 'content': { + 'citations': None, + 'source': { + 'data': IsStr(), + 'media_type': 'text/plain', + 'type': 'text', + }, + 'title': 'Pydantic AI', + 'type': 'document', + }, + 'retrieved_at': IsStr(), + 'type': 'web_fetch_result', + 'url': 'https://ai.pydantic.dev', + }, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='anthropic', + ), + TextPart( + content='Pydantic AI is a Python agent framework designed to help you quickly, confidently, and painlessly build production grade applications and workflows with Generative AI.' + ), + ], + usage=RequestUsage( + input_tokens=7244, + output_tokens=153, + details={ + 'cache_creation_input_tokens': 0, + 'cache_read_input_tokens': 0, + 'input_tokens': 7244, + 'output_tokens': 153, + }, + ), + model_name='claude-sonnet-4-20250514', + timestamp=IsDatetime(), + provider_name='anthropic', + provider_details={'finish_reason': 'end_turn'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + assert event_parts == snapshot( + [ + PartDeltaEvent(index=0, delta=ThinkingPartDelta(content_delta='The user wants', provider_name='anthropic')), + PartDeltaEvent(index=0, delta=ThinkingPartDelta(content_delta=' me to fetch', provider_name='anthropic')), + PartDeltaEvent(index=0, delta=ThinkingPartDelta(content_delta=' the content', provider_name='anthropic')), + PartDeltaEvent( + index=0, delta=ThinkingPartDelta(content_delta=' from the URL https', provider_name='anthropic') + ), + PartDeltaEvent( + index=0, delta=ThinkingPartDelta(content_delta='://ai.pydantic.dev', provider_name='anthropic') + ), + PartDeltaEvent(index=0, delta=ThinkingPartDelta(content_delta=' and provide', provider_name='anthropic')), + PartDeltaEvent(index=0, delta=ThinkingPartDelta(content_delta=' only', provider_name='anthropic')), + PartDeltaEvent( + index=0, delta=ThinkingPartDelta(content_delta=' the first sentence from', provider_name='anthropic') + ), + PartDeltaEvent(index=0, delta=ThinkingPartDelta(content_delta=' that page.', provider_name='anthropic')), + PartDeltaEvent( + index=0, + delta=ThinkingPartDelta(content_delta=' I need to use the web_fetch', provider_name='anthropic'), + ), + PartDeltaEvent(index=0, delta=ThinkingPartDelta(content_delta=' tool to', provider_name='anthropic')), + PartDeltaEvent( + index=0, delta=ThinkingPartDelta(content_delta=' get the content from', provider_name='anthropic') + ), + PartDeltaEvent(index=0, delta=ThinkingPartDelta(content_delta=' this URL.', provider_name='anthropic')), + PartDeltaEvent( + index=0, + delta=ThinkingPartDelta( + signature_delta='EusCCkYICRgCKkAG/7zhRcmUoiMtml5iZUXVv3nqupp8kgk0nrq9zOoklaXzVCnrb9kwLNWGETIcCaAnLd0cd0ESwjslkVKdV9n8EgxKKdu8LlEvh9VGIWIaDAJ2Ja2NEacp1Am6jSIwyNO36tV+Sj+q6dWf79U+3KOIa1khXbIYarpkIViCuYQaZwpJ4Vtedrd7dLWTY2d5KtIB9Pug5UPuvepSOjyhxLaohtGxmdvZN8crGwBdTJYF9GHSli/rzvkR6CpH+ixd8iSopwFcsJgQ3j68fr/yD7cHmZ06jU3LaESVEBwTHnlK0ABiYnGvD3SvX6PgImMSQxQ1ThARFTA7DePoWw+z5DI0L2vgSun2qTYHkmGxzaEskhNIBlK9r7wS3tVcO0Di4lD/rhYV61tklL2NBWJqvm7ZCtJTN09CzPFJy7HDkg7bSINVL4kuu9gTWEtb/o40tw1b+sO62UcfxQTVFQ4Cj8D8XFZbGAE=', + provider_name='anthropic', + ), + ), + PartStartEvent( + index=1, + part=BuiltinToolCallPart(tool_name='web_fetch', tool_call_id=IsStr(), provider_name='anthropic'), + previous_part_kind='thinking', + ), + PartDeltaEvent( + index=1, delta=ToolCallPartDelta(args_delta='', tool_call_id='srvtoolu_018ADaxdJjyZ8HXtF3sTBPNk') + ), + PartDeltaEvent( + index=1, + delta=ToolCallPartDelta(args_delta='{"url": "', tool_call_id='srvtoolu_018ADaxdJjyZ8HXtF3sTBPNk'), + ), + PartDeltaEvent( + index=1, + delta=ToolCallPartDelta(args_delta='https://ai', tool_call_id='srvtoolu_018ADaxdJjyZ8HXtF3sTBPNk'), + ), + PartDeltaEvent( + index=1, delta=ToolCallPartDelta(args_delta='.p', tool_call_id='srvtoolu_018ADaxdJjyZ8HXtF3sTBPNk') + ), + PartDeltaEvent( + index=1, delta=ToolCallPartDelta(args_delta='yd', tool_call_id='srvtoolu_018ADaxdJjyZ8HXtF3sTBPNk') + ), + PartDeltaEvent( + index=1, + delta=ToolCallPartDelta(args_delta='antic.dev"}', tool_call_id='srvtoolu_018ADaxdJjyZ8HXtF3sTBPNk'), + ), + PartStartEvent( + index=2, + part=BuiltinToolReturnPart( + tool_name='web_fetch', + content={ + 'content': { + 'citations': None, + 'source': { + 'data': '''\ +Pydantic AI +GenAI Agent Framework, the Pydantic way +Pydantic AI is a Python agent framework designed to help you quickly, confidently, and painlessly build production grade applications and workflows with Generative AI. +FastAPI revolutionized web development by offering an innovative and ergonomic design, built on the foundation of [Pydantic Validation](https://docs.pydantic.dev) and modern Python features like type hints. +Yet despite virtually every Python agent framework and LLM library using Pydantic Validation, when we began to use LLMs in [Pydantic Logfire](https://pydantic.dev/logfire), we couldn't find anything that gave us the same feeling. +We built Pydantic AI with one simple aim: to bring that FastAPI feeling to GenAI app and agent development. +Why use Pydantic AI +- +Built by the Pydantic Team: +[Pydantic Validation](https://docs.pydantic.dev/latest/)is the validation layer of the OpenAI SDK, the Google ADK, the Anthropic SDK, LangChain, LlamaIndex, AutoGPT, Transformers, CrewAI, Instructor and many more. Why use the derivative when you can go straight to the source? - +Model-agnostic: Supports virtually every +[model](models/overview/)and provider: OpenAI, Anthropic, Gemini, DeepSeek, Grok, Cohere, Mistral, and Perplexity; Azure AI Foundry, Amazon Bedrock, Google Vertex AI, Ollama, LiteLLM, Groq, OpenRouter, Together AI, Fireworks AI, Cerebras, Hugging Face, GitHub, Heroku, Vercel, Nebius, OVHcloud, and Outlines. If your favorite model or provider is not listed, you can easily implement a[custom model](models/overview/#custom-models). - +Seamless Observability: Tightly +[integrates](logfire/)with[Pydantic Logfire](https://pydantic.dev/logfire), our general-purpose OpenTelemetry observability platform, for real-time debugging, evals-based performance monitoring, and behavior, tracing, and cost tracking. If you already have an observability platform that supports OTel, you can[use that too](logfire/#alternative-observability-backends). - +Fully Type-safe: Designed to give your IDE or AI coding agent as much context as possible for auto-completion and +[type checking](agents/#static-type-checking), moving entire classes of errors from runtime to write-time for a bit of that Rust "if it compiles, it works" feel. - +Powerful Evals: Enables you to systematically test and +[evaluate](evals/)the performance and accuracy of the agentic systems you build, and monitor the performance over time in Pydantic Logfire. - +MCP, A2A, and UI: Integrates the +[Model Context Protocol](mcp/overview/),[Agent2Agent](a2a/), and various[UI event stream](ui/overview/)standards to give your agent access to external tools and data, let it interoperate with other agents, and build interactive applications with streaming event-based communication. - +Human-in-the-Loop Tool Approval: Easily lets you flag that certain tool calls +[require approval](deferred-tools/#human-in-the-loop-tool-approval)before they can proceed, possibly depending on tool call arguments, conversation history, or user preferences. - +Durable Execution: Enables you to build +[durable agents](durable_execution/overview/)that can preserve their progress across transient API failures and application errors or restarts, and handle long-running, asynchronous, and human-in-the-loop workflows with production-grade reliability. - +Streamed Outputs: Provides the ability to +[stream](output/#streamed-results)structured output continuously, with immediate validation, ensuring real time access to generated data. - +Graph Support: Provides a powerful way to define +[graphs](graph/)using type hints, for use in complex applications where standard control flow can degrade to spaghetti code. +Realistically though, no list is going to be as convincing as [giving it a try](#next-steps) and seeing how it makes you feel! +Sign up for our newsletter, The Pydantic Stack, with updates & tutorials on Pydantic AI, Logfire, and Pydantic: +Hello World Example +Here's a minimal example of Pydantic AI: +[Learn about Gateway](gateway)hello_world.py +from pydantic_ai import Agent +agent = Agent( # (1)! +'gateway/anthropic:claude-sonnet-4-0', +instructions='Be concise, reply with one sentence.', # (2)! +) +result = agent.run_sync('Where does "hello world" come from?') # (3)! +print(result.output) +""" +The first known use of "hello, world" was in a 1974 textbook about the C programming language. +""" +- We configure the agent to use +[Anthropic's Claude Sonnet 4.0](api/models/anthropic/)model, but you can also set the model when running the agent. - Register static +[instructions](agents/#instructions)using a keyword argument to the agent. [Run the agent](agents/#running-agents)synchronously, starting a conversation with the LLM. +from pydantic_ai import Agent +agent = Agent( # (1)! +'anthropic:claude-sonnet-4-0', +instructions='Be concise, reply with one sentence.', # (2)! +) +result = agent.run_sync('Where does "hello world" come from?') # (3)! +print(result.output) +""" +The first known use of "hello, world" was in a 1974 textbook about the C programming language. +""" +- We configure the agent to use +[Anthropic's Claude Sonnet 4.0](api/models/anthropic/)model, but you can also set the model when running the agent. - Register static +[instructions](agents/#instructions)using a keyword argument to the agent. [Run the agent](agents/#running-agents)synchronously, starting a conversation with the LLM. +(This example is complete, it can be run "as is", assuming you've [installed the pydantic_ai package](install/)) +The exchange will be very short: Pydantic AI will send the instructions and the user prompt to the LLM, and the model will return a text response. +Not very interesting yet, but we can easily add [tools](tools/), [dynamic instructions](agents/#instructions), and [structured outputs](output/) to build more powerful agents. +Tools & Dependency Injection Example +Here is a concise example using Pydantic AI to build a support agent for a bank: +[Learn about Gateway](gateway)bank_support.py +from dataclasses import dataclass +from pydantic import BaseModel, Field +from pydantic_ai import Agent, RunContext +from bank_database import DatabaseConn +@dataclass +class SupportDependencies: # (3)! +customer_id: int +db: DatabaseConn # (12)! +class SupportOutput(BaseModel): # (13)! +support_advice: str = Field(description='Advice returned to the customer') +block_card: bool = Field(description="Whether to block the customer's card") +risk: int = Field(description='Risk level of query', ge=0, le=10) +support_agent = Agent( # (1)! +'gateway/openai:gpt-5', # (2)! +deps_type=SupportDependencies, +output_type=SupportOutput, # (9)! +instructions=( # (4)! +'You are a support agent in our bank, give the ' +'customer support and judge the risk level of their query.' +), +) +@support_agent.instructions # (5)! +async def add_customer_name(ctx: RunContext[SupportDependencies]) -> str: +customer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id) +return f"The customer's name is {customer_name!r}" +@support_agent.tool # (6)! +async def customer_balance( +ctx: RunContext[SupportDependencies], include_pending: bool +) -> float: +"""Returns the customer's current account balance.""" # (7)! +return await ctx.deps.db.customer_balance( +id=ctx.deps.customer_id, +include_pending=include_pending, +) +... # (11)! +async def main(): +deps = SupportDependencies(customer_id=123, db=DatabaseConn()) +result = await support_agent.run('What is my balance?', deps=deps) # (8)! +print(result.output) # (10)! +""" +support_advice='Hello John, your current account balance, including pending transactions, is $123.45.' block_card=False risk=1 +""" +result = await support_agent.run('I just lost my card!', deps=deps) +print(result.output) +""" +support_advice="I'm sorry to hear that, John. We are temporarily blocking your card to prevent unauthorized transactions." block_card=True risk=8 +""" +- This +[agent](agents/)will act as first-tier support in a bank. Agents are generic in the type of dependencies they accept and the type of output they return. In this case, the support agent has typeAgent[SupportDependencies, SupportOutput] +. - Here we configure the agent to use +[OpenAI's GPT-5 model](api/models/openai/), you can also set the model when running the agent. - The +SupportDependencies +dataclass is used to pass data, connections, and logic into the model that will be needed when running[instructions](agents/#instructions)and[tool](tools/)functions. Pydantic AI's system of dependency injection provides a[type-safe](agents/#static-type-checking)way to customise the behavior of your agents, and can be especially useful when running[unit tests](testing/)and evals. - Static +[instructions](agents/#instructions)can be registered with theto the agent.instructions +keyword argument - Dynamic +[instructions](agents/#instructions)can be registered with thedecorator, and can make use of dependency injection. Dependencies are carried via the@agent.instructions +argument, which is parameterized with theRunContext +deps_type +from above. If the type annotation here is wrong, static type checkers will catch it. - The +decorator let you register functions which the LLM may call while responding to a user. Again, dependencies are carried via@agent.tool +, any other arguments become the tool schema passed to the LLM. Pydantic is used to validate these arguments, and errors are passed back to the LLM so it can retry.RunContext +- The docstring of a tool is also passed to the LLM as the description of the tool. Parameter descriptions are +[extracted](tools/#function-tools-and-schema)from the docstring and added to the parameter schema sent to the LLM. [Run the agent](agents/#running-agents)asynchronously, conducting a conversation with the LLM until a final response is reached. Even in this fairly simple case, the agent will exchange multiple messages with the LLM as tools are called to retrieve an output.- The response from the agent will be guaranteed to be a +SupportOutput +. If validation fails[reflection](agents/#reflection-and-self-correction), the agent is prompted to try again. - The output will be validated with Pydantic to guarantee it is a +SupportOutput +, since the agent is generic, it'll also be typed as aSupportOutput +to aid with static type checking. - In a real use case, you'd add more tools and longer instructions to the agent to extend the context it's equipped with and support it can provide. +- This is a simple sketch of a database connection, used to keep the example short and readable. In reality, you'd be connecting to an external database (e.g. PostgreSQL) to get information about customers. +- This +[Pydantic](https://docs.pydantic.dev)model is used to constrain the structured data returned by the agent. From this simple definition, Pydantic builds the JSON Schema that tells the LLM how to return the data, and performs validation to guarantee the data is correct at the end of the run. +from dataclasses import dataclass +from pydantic import BaseModel, Field +from pydantic_ai import Agent, RunContext +from bank_database import DatabaseConn +@dataclass +class SupportDependencies: # (3)! +customer_id: int +db: DatabaseConn # (12)! +class SupportOutput(BaseModel): # (13)! +support_advice: str = Field(description='Advice returned to the customer') +block_card: bool = Field(description="Whether to block the customer's card") +risk: int = Field(description='Risk level of query', ge=0, le=10) +support_agent = Agent( # (1)! +'openai:gpt-5', # (2)! +deps_type=SupportDependencies, +output_type=SupportOutput, # (9)! +instructions=( # (4)! +'You are a support agent in our bank, give the ' +'customer support and judge the risk level of their query.' +), +) +@support_agent.instructions # (5)! +async def add_customer_name(ctx: RunContext[SupportDependencies]) -> str: +customer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id) +return f"The customer's name is {customer_name!r}" +@support_agent.tool # (6)! +async def customer_balance( +ctx: RunContext[SupportDependencies], include_pending: bool +) -> float: +"""Returns the customer's current account balance.""" # (7)! +return await ctx.deps.db.customer_balance( +id=ctx.deps.customer_id, +include_pending=include_pending, +) +... # (11)! +async def main(): +deps = SupportDependencies(customer_id=123, db=DatabaseConn()) +result = await support_agent.run('What is my balance?', deps=deps) # (8)! +print(result.output) # (10)! +""" +support_advice='Hello John, your current account balance, including pending transactions, is $123.45.' block_card=False risk=1 +""" +result = await support_agent.run('I just lost my card!', deps=deps) +print(result.output) +""" +support_advice="I'm sorry to hear that, John. We are temporarily blocking your card to prevent unauthorized transactions." block_card=True risk=8 +""" +- This +[agent](agents/)will act as first-tier support in a bank. Agents are generic in the type of dependencies they accept and the type of output they return. In this case, the support agent has typeAgent[SupportDependencies, SupportOutput] +. - Here we configure the agent to use +[OpenAI's GPT-5 model](api/models/openai/), you can also set the model when running the agent. - The +SupportDependencies +dataclass is used to pass data, connections, and logic into the model that will be needed when running[instructions](agents/#instructions)and[tool](tools/)functions. Pydantic AI's system of dependency injection provides a[type-safe](agents/#static-type-checking)way to customise the behavior of your agents, and can be especially useful when running[unit tests](testing/)and evals. - Static +[instructions](agents/#instructions)can be registered with theto the agent.instructions +keyword argument - Dynamic +[instructions](agents/#instructions)can be registered with thedecorator, and can make use of dependency injection. Dependencies are carried via the@agent.instructions +argument, which is parameterized with theRunContext +deps_type +from above. If the type annotation here is wrong, static type checkers will catch it. - The +decorator let you register functions which the LLM may call while responding to a user. Again, dependencies are carried via@agent.tool +, any other arguments become the tool schema passed to the LLM. Pydantic is used to validate these arguments, and errors are passed back to the LLM so it can retry.RunContext +- The docstring of a tool is also passed to the LLM as the description of the tool. Parameter descriptions are +[extracted](tools/#function-tools-and-schema)from the docstring and added to the parameter schema sent to the LLM. [Run the agent](agents/#running-agents)asynchronously, conducting a conversation with the LLM until a final response is reached. Even in this fairly simple case, the agent will exchange multiple messages with the LLM as tools are called to retrieve an output.- The response from the agent will be guaranteed to be a +SupportOutput +. If validation fails[reflection](agents/#reflection-and-self-correction), the agent is prompted to try again. - The output will be validated with Pydantic to guarantee it is a +SupportOutput +, since the agent is generic, it'll also be typed as aSupportOutput +to aid with static type checking. - In a real use case, you'd add more tools and longer instructions to the agent to extend the context it's equipped with and support it can provide. +- This is a simple sketch of a database connection, used to keep the example short and readable. In reality, you'd be connecting to an external database (e.g. PostgreSQL) to get information about customers. +- This +[Pydantic](https://docs.pydantic.dev)model is used to constrain the structured data returned by the agent. From this simple definition, Pydantic builds the JSON Schema that tells the LLM how to return the data, and performs validation to guarantee the data is correct at the end of the run. +Complete bank_support.py +example +The code included here is incomplete for the sake of brevity (the definition of DatabaseConn +is missing); you can find the complete bank_support.py +example [here](examples/bank-support/). +Instrumentation with Pydantic Logfire +Even a simple agent with just a handful of tools can result in a lot of back-and-forth with the LLM, making it nearly impossible to be confident of what's going on just from reading the code. To understand the flow of the above runs, we can watch the agent in action using Pydantic Logfire. +To do this, we need to [set up Logfire](logfire/#using-logfire), and add the following to our code: +[Learn about Gateway](gateway)bank_support_with_logfire.py +... +from pydantic_ai import Agent, RunContext +from bank_database import DatabaseConn +import logfire +logfire.configure() # (1)! +logfire.instrument_pydantic_ai() # (2)! +logfire.instrument_asyncpg() # (3)! +... +support_agent = Agent( +'gateway/openai:gpt-5', +deps_type=SupportDependencies, +output_type=SupportOutput, +system_prompt=( +'You are a support agent in our bank, give the ' +'customer support and judge the risk level of their query.' +), +) +- Configure the Logfire SDK, this will fail if project is not set up. +- This will instrument all Pydantic AI agents used from here on out. If you want to instrument only a specific agent, you can pass the +to the agent.instrument=True +keyword argument - In our demo, +DatabaseConn +usesto connect to a PostgreSQL database, soasyncpg +is used to log the database queries.logfire.instrument_asyncpg() +... +from pydantic_ai import Agent, RunContext +from bank_database import DatabaseConn +import logfire +logfire.configure() # (1)! +logfire.instrument_pydantic_ai() # (2)! +logfire.instrument_asyncpg() # (3)! +... +support_agent = Agent( +'openai:gpt-5', +deps_type=SupportDependencies, +output_type=SupportOutput, +system_prompt=( +'You are a support agent in our bank, give the ' +'customer support and judge the risk level of their query.' +), +) +- Configure the Logfire SDK, this will fail if project is not set up. +- This will instrument all Pydantic AI agents used from here on out. If you want to instrument only a specific agent, you can pass the +to the agent.instrument=True +keyword argument - In our demo, +DatabaseConn +usesto connect to a PostgreSQL database, soasyncpg +is used to log the database queries.logfire.instrument_asyncpg() +That's enough to get the following view of your agent in action: +See [Monitoring and Performance](logfire/) to learn more. +llms.txt +The Pydantic AI documentation is available in the [llms.txt](https://llmstxt.org/) format. +This format is defined in Markdown and suited for LLMs and AI coding assistants and agents. +Two formats are available: +: a file containing a brief description of the project, along with links to the different sections of the documentation. The structure of this file is described in detailsllms.txt +[here](https://llmstxt.org/#format).: Similar to thellms-full.txt +llms.txt +file, but every link content is included. Note that this file may be too large for some LLMs. +As of today, these files are not automatically leveraged by IDEs or coding agents, but they will use it if you provide a link or the full text. +Next Steps +To try Pydantic AI for yourself, [install it](install/) and follow the instructions [in the examples](examples/setup/). +Read the [docs](agents/) to learn more about building applications with Pydantic AI. +Read the [API Reference](api/agent/) to understand Pydantic AI's interface. +Join [ Slack](https://logfire.pydantic.dev/docs/join-slack/) or file an issue on [ GitHub](https://github.com/pydantic/pydantic-ai/issues) if you have any questions.\ +''', + 'media_type': 'text/plain', + 'type': 'text', + }, + 'title': 'Pydantic AI', + 'type': 'document', + }, + 'retrieved_at': IsStr(), + 'type': 'web_fetch_result', + 'url': 'https://ai.pydantic.dev', + }, + tool_call_id=IsStr(), + timestamp=IsDatetime(), + provider_name='anthropic', + ), + previous_part_kind='builtin-tool-call', + ), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta='ydantic AI is a')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=' Python')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=' agent')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=' framework')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=' designe')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta='d to help')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=' you quickly')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=',')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=' confi')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta='dently,')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=' and pain')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta='lessly build production')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=' grade')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=' applications')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=' an')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta='d workflows')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=' with')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta=' Gener')), + PartDeltaEvent(index=3, delta=TextPartDelta(content_delta='ative AI.')), + ] + ) + + +async def test_anthropic_web_fetch_tool_message_replay(): + """Test that BuiltinToolCallPart and BuiltinToolReturnPart for WebFetchTool are correctly serialized.""" + from typing import cast + + from pydantic_ai.models.anthropic import AnthropicModel + from pydantic_ai.providers.anthropic import AnthropicProvider + + # Create a model instance + m = AnthropicModel('claude-sonnet-4-0', provider=AnthropicProvider(api_key='test-key')) + + # Create message history with BuiltinToolCallPart and BuiltinToolReturnPart + messages = [ + ModelRequest(parts=[UserPromptPart(content='Test')]), + ModelResponse( + parts=[ + BuiltinToolCallPart( + provider_name=m.system, + tool_name=WebFetchTool.kind, + args={'url': 'https://example.com'}, + tool_call_id='test_id_1', + ), + BuiltinToolReturnPart( + provider_name=m.system, + tool_name=WebFetchTool.kind, + content={ + 'content': {'type': 'document'}, + 'type': 'web_fetch_result', + 'url': 'https://example.com', + 'retrieved_at': '2025-01-01T00:00:00Z', + }, + tool_call_id='test_id_1', + ), + ], + model_name='claude-sonnet-4-0', + ), + ] + + # Call _map_message to trigger serialization + model_settings = {} + model_request_parameters = ModelRequestParameters( + function_tools=[], + builtin_tools=[WebFetchTool()], + output_tools=[], + ) + + system_prompt, anthropic_messages = await m._map_message(messages, model_request_parameters, model_settings) # pyright: ignore[reportPrivateUsage,reportArgumentType] + + # Verify the messages were serialized correctly + assert system_prompt is None or isinstance(system_prompt, (list | str)) + assert len(anthropic_messages) == 2 + assert anthropic_messages[1]['role'] == 'assistant' + + # Check that server_tool_use block is present + content = anthropic_messages[1]['content'] + assert any( + isinstance(item, dict) and item.get('type') == 'server_tool_use' and item.get('name') == 'web_fetch' + for item in content + ) + + # Check that web_fetch_tool_result block is present and contains URL and retrieved_at + web_fetch_result = next( + item for item in content if isinstance(item, dict) and item.get('type') == 'web_fetch_tool_result' + ) + assert 'content' in web_fetch_result + result_content = web_fetch_result['content'] + assert isinstance(result_content, dict) # Type narrowing for mypy + assert result_content['type'] == 'web_fetch_result' # type: ignore[typeddict-item] + assert result_content['url'] == 'https://example.com' # type: ignore[typeddict-item] + # retrieved_at is optional - cast to avoid complex union type issues + assert cast(dict, result_content).get('retrieved_at') == '2025-01-01T00:00:00Z' # pyright: ignore[reportUnknownMemberType,reportMissingTypeArgument] + assert 'content' in result_content # The actual document content + + +async def test_anthropic_web_fetch_tool_with_parameters(): + """Test that WebFetchTool parameters are correctly passed to Anthropic API.""" + from pydantic_ai.models.anthropic import AnthropicModel + from pydantic_ai.providers.anthropic import AnthropicProvider + + # Create a model instance + m = AnthropicModel('claude-sonnet-4-0', provider=AnthropicProvider(api_key='test-key')) + + # Create WebFetchTool with all parameters + web_fetch_tool = WebFetchTool( + max_uses=5, + allowed_domains=['example.com', 'ai.pydantic.dev'], + citations_enabled=True, + max_content_tokens=50000, + ) + + model_request_parameters = ModelRequestParameters( + function_tools=[], + builtin_tools=[web_fetch_tool], + output_tools=[], + ) + + # Get tools from model + tools, _, _ = m._add_builtin_tools([], model_request_parameters) # pyright: ignore[reportPrivateUsage] + + # Find the web_fetch tool + web_fetch_tool_param = next((t for t in tools if t.get('name') == 'web_fetch'), None) + assert web_fetch_tool_param is not None + + # Verify all parameters are passed correctly + assert web_fetch_tool_param.get('type') == 'web_fetch_20250910' + assert web_fetch_tool_param.get('max_uses') == 5 + assert web_fetch_tool_param.get('allowed_domains') == ['example.com', 'ai.pydantic.dev'] + assert web_fetch_tool_param.get('blocked_domains') is None + assert web_fetch_tool_param.get('citations') == {'enabled': True} + assert web_fetch_tool_param.get('max_content_tokens') == 50000 + + +async def test_anthropic_web_fetch_tool_domain_filtering(): + """Test that blocked_domains work and are mutually exclusive with allowed_domains.""" + from pydantic_ai.models.anthropic import AnthropicModel + from pydantic_ai.providers.anthropic import AnthropicProvider + + # Create a model instance + m = AnthropicModel('claude-sonnet-4-0', provider=AnthropicProvider(api_key='test-key')) + + # Test with blocked_domains + web_fetch_tool = WebFetchTool(blocked_domains=['private.example.com', 'internal.example.com']) + + model_request_parameters = ModelRequestParameters( + function_tools=[], + builtin_tools=[web_fetch_tool], + output_tools=[], + ) + + # Get tools from model + tools, _, _ = m._add_builtin_tools([], model_request_parameters) # pyright: ignore[reportPrivateUsage] + + # Find the web_fetch tool + web_fetch_tool_param = next((t for t in tools if t.get('name') == 'web_fetch'), None) + assert web_fetch_tool_param is not None + + # Verify blocked_domains is passed correctly + assert web_fetch_tool_param.get('blocked_domains') == ['private.example.com', 'internal.example.com'] + assert web_fetch_tool_param.get('allowed_domains') is None + + +@pytest.mark.vcr() async def test_anthropic_mcp_servers(allow_model_requests: None, anthropic_api_key: str): m = AnthropicModel('claude-sonnet-4-0', provider=AnthropicProvider(api_key=anthropic_api_key)) settings = AnthropicModelSettings(anthropic_thinking={'type': 'enabled', 'budget_tokens': 3000}) @@ -3748,8 +4606,10 @@ async def test_anthropic_mcp_servers(allow_model_requests: None, anthropic_api_k ] ) - result = await agent.run('How about the pydantic repo in the same org?', message_history=messages) - messages = result.new_messages() + result = await agent.run( + 'How about the pydantic repo in the same org?', message_history=messages + ) # pragma: lax no cover + messages = result.new_messages() # pragma: lax no cover assert messages == snapshot( [ ModelRequest( @@ -4106,7 +4966,7 @@ async def test_anthropic_mcp_servers_stream(allow_model_requests: None, anthropi The framework provides a unified interface for integrating with various LLM providers, including OpenAI, Anthropic, Google, Groq, Cohere, Mistral, Bedrock, and HuggingFace. Each model integration follows a consistent settings pattern with provider-specific prefixes (e.g., `google_*`, `anthropic_*`). \n\ Examples of supported models and their capabilities include: -* `GoogleModel`: Integrates with Google's Gemini API, supporting both Gemini API (`google-gla`) and Vertex AI (`google-vertex`) providers. It supports token counting, streaming, built-in tools like `WebSearchTool`, `UrlContextTool`, `CodeExecutionTool`, and native JSON schema output. \n\ +* `GoogleModel`: Integrates with Google's Gemini API, supporting both Gemini API (`google-gla`) and Vertex AI (`google-vertex`) providers. It supports token counting, streaming, built-in tools like `WebSearchTool`, `WebFetchTool`, `CodeExecutionTool`, and native JSON schema output. \n\ * `AnthropicModel`: Uses Anthropic's beta API for advanced features like "Thinking Blocks" and built-in tools. \n\ * `GroqModel`: Offers high-speed inference and specialized reasoning support with configurable reasoning formats. \n\ * `MistralModel`: Supports customizable JSON schema prompting and thinking support. \n\ @@ -4123,7 +4983,7 @@ async def test_anthropic_mcp_servers_stream(allow_model_requests: None, anthropi Tools can return various types of output, including anything Pydantic can serialize to JSON, as well as multimodal content like `AudioUrl`, `VideoUrl`, `ImageUrl`, or `DocumentUrl`. The `ToolReturn` object allows for separating the `return_value` (for the model), `content` (for additional context), and `metadata` (for application-specific use). \n\ -Built-in tools like `UrlContextTool` allow agents to pull web content into their context. \n\ +Built-in tools like `WebFetchTool` allow agents to pull web content into their context. \n\ ### 5. Output Handling The framework supports various output types: @@ -5216,6 +6076,7 @@ async def get_user_country() -> str: ) +@pytest.mark.vcr() async def test_anthropic_prompted_output(allow_model_requests: None, anthropic_api_key: str): m = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(api_key=anthropic_api_key)) @@ -5357,7 +6218,7 @@ class CountryLanguage(BaseModel): ) -async def test_anthropic_native_output(allow_model_requests: None, anthropic_api_key: str): +async def test_anthropic_native_output(allow_model_requests: None, anthropic_api_key: str): # pragma: lax no cover m = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(api_key=anthropic_api_key)) class CityLocation(BaseModel): @@ -6371,8 +7232,10 @@ async def test_anthropic_text_parts_ahead_of_built_in_tool_call(allow_model_requ "Based on yesterday's date (September 16, 2025), Asian markets rose higher as Federal Reserve rate cut hopes lifted global market sentiment. Additionally, there were severe rain and gales impacting parts of New Zealand, and a notable court case involving a British aristocrat." ) - async with agent.run_stream('Briefly mention 1 event that happened the day after tomorrow in history?') as result: - chunks = [c async for c in result.stream_text(debounce_by=None, delta=True)] + async with agent.run_stream( + 'Briefly mention 1 event that happened the day after tomorrow in history?' + ) as result: # pragma: lax no cover + chunks = [c async for c in result.stream_text(debounce_by=None, delta=True)] # pragma: lax no cover assert chunks == snapshot( [ 'Let', diff --git a/tests/models/test_google.py b/tests/models/test_google.py index 7b5f3a9aac..bde162b7e4 100644 --- a/tests/models/test_google.py +++ b/tests/models/test_google.py @@ -43,7 +43,13 @@ VideoUrl, ) from pydantic_ai.agent import Agent -from pydantic_ai.builtin_tools import CodeExecutionTool, ImageGenerationTool, UrlContextTool, WebSearchTool +from pydantic_ai.builtin_tools import ( + CodeExecutionTool, + ImageGenerationTool, + UrlContextTool, # pyright: ignore[reportDeprecated] + WebFetchTool, + WebSearchTool, +) from pydantic_ai.exceptions import ModelHTTPError, ModelRetry, UnexpectedModelBehavior, UserError from pydantic_ai.messages import ( BuiltinToolCallEvent, # pyright: ignore[reportDeprecated] @@ -1344,9 +1350,19 @@ async def test_google_model_web_search_tool_stream(allow_model_requests: None, g ) -async def test_google_model_url_context_tool(allow_model_requests: None, google_provider: GoogleProvider): +@pytest.mark.parametrize('use_deprecated_url_context_tool', [False, True]) +async def test_google_model_web_fetch_tool( + allow_model_requests: None, google_provider: GoogleProvider, use_deprecated_url_context_tool: bool +): m = GoogleModel('gemini-2.5-flash', provider=google_provider) - agent = Agent(m, system_prompt='You are a helpful chatbot.', builtin_tools=[UrlContextTool()]) + + if use_deprecated_url_context_tool: + with pytest.warns(DeprecationWarning, match='Use `WebFetchTool` instead.'): + tool = UrlContextTool() # pyright: ignore[reportDeprecated] + else: + tool = WebFetchTool() + + agent = Agent(m, system_prompt='You are a helpful chatbot.', builtin_tools=[tool]) result = await agent.run( 'What is the first sentence on the page https://ai.pydantic.dev? Reply with only the sentence.' @@ -2764,7 +2780,7 @@ def test_map_usage(): async def test_google_builtin_tools_with_other_tools(allow_model_requests: None, google_provider: GoogleProvider): m = GoogleModel('gemini-2.5-flash', provider=google_provider) - agent = Agent(m, builtin_tools=[UrlContextTool()]) + agent = Agent(m, builtin_tools=[WebFetchTool()]) @agent.tool_plain async def get_user_country() -> str: @@ -2780,7 +2796,7 @@ class CityLocation(BaseModel): city: str country: str - agent = Agent(m, output_type=ToolOutput(CityLocation), builtin_tools=[UrlContextTool()]) + agent = Agent(m, output_type=ToolOutput(CityLocation), builtin_tools=[WebFetchTool()]) with pytest.raises( UserError, @@ -2791,7 +2807,7 @@ class CityLocation(BaseModel): await agent.run('What is the largest city in Mexico?') # Will default to prompted output - agent = Agent(m, output_type=CityLocation, builtin_tools=[UrlContextTool()]) + agent = Agent(m, output_type=CityLocation, builtin_tools=[WebFetchTool()]) result = await agent.run('What is the largest city in Mexico?') assert result.output == snapshot(CityLocation(city='Mexico City', country='Mexico')) diff --git a/tests/models/test_model_request_parameters.py b/tests/models/test_model_request_parameters.py index 1c8d0780e5..0fc62af92c 100644 --- a/tests/models/test_model_request_parameters.py +++ b/tests/models/test_model_request_parameters.py @@ -6,7 +6,7 @@ ImageGenerationTool, MCPServerTool, MemoryTool, - UrlContextTool, + WebFetchTool, WebSearchTool, WebSearchUserLocation, ) @@ -44,7 +44,7 @@ def test_model_request_parameters_are_serializable(): builtin_tools=[ WebSearchTool(user_location=WebSearchUserLocation(city='New York', country='US')), CodeExecutionTool(), - UrlContextTool(), + WebFetchTool(), ImageGenerationTool(size='1024x1024'), MemoryTool(), MCPServerTool(id='deepwiki', url='https://mcp.deepwiki.com/mcp'), @@ -80,7 +80,14 @@ def test_model_request_parameters_are_serializable(): 'max_uses': None, }, {'kind': 'code_execution'}, - {'kind': 'url_context'}, + { + 'kind': 'web_fetch', + 'max_uses': None, + 'allowed_domains': None, + 'blocked_domains': None, + 'citations_enabled': False, + 'max_content_tokens': None, + }, { 'kind': 'image_generation', 'background': 'auto', diff --git a/tests/test_builtin_tools.py b/tests/test_builtin_tools.py index 2f636b3e4e..c83de7d094 100644 --- a/tests/test_builtin_tools.py +++ b/tests/test_builtin_tools.py @@ -3,7 +3,11 @@ import pytest from pydantic_ai.agent import Agent -from pydantic_ai.builtin_tools import CodeExecutionTool, WebSearchTool +from pydantic_ai.builtin_tools import ( + CodeExecutionTool, + UrlContextTool, # pyright: ignore[reportDeprecated] + WebSearchTool, +) from pydantic_ai.exceptions import UserError from pydantic_ai.models import Model @@ -40,3 +44,9 @@ async def test_builtin_tools_not_supported_code_execution_stream(model: Model, a with pytest.raises(UserError): async with agent.run_stream('What day is tomorrow?'): ... # pragma: no cover + + +def test_url_context_tool_is_deprecated(): + """Test that UrlContextTool is deprecated and warns users to use WebFetchTool instead.""" + with pytest.warns(DeprecationWarning, match='Use `WebFetchTool` instead.'): + UrlContextTool() # pyright: ignore[reportDeprecated] diff --git a/tests/test_examples.py b/tests/test_examples.py index 85bae688d0..0955ab0fb4 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -342,6 +342,12 @@ async def call_tool( 'What was his most famous equation?': "Albert Einstein's most famous equation is (E = mc^2).", 'What is the date?': 'Hello Frank, the date today is 2032-01-02.', 'What is this? https://ai.pydantic.dev': 'A Python agent framework for building Generative AI applications.', + 'Compare the documentation at https://ai.pydantic.dev and https://docs.pydantic.dev': ( + 'Both sites provide comprehensive documentation for Pydantic projects. ' + 'ai.pydantic.dev focuses on PydanticAI, a framework for building AI agents, ' + 'while docs.pydantic.dev covers Pydantic, the data validation library. ' + 'They share similar documentation styles and both emphasize type safety and developer experience.' + ), 'Give me some examples of my products.': 'Here are some examples of my data: Pen, Paper, Pencil.', 'Put my money on square eighteen': ToolCallPart( tool_name='roulette_wheel', args={'square': 18}, tool_call_id='pyd_ai_tool_call_id' @@ -526,6 +532,7 @@ async def call_tool( 'Tell me about the pydantic/pydantic-ai repo.': 'The pydantic/pydantic-ai repo is a Python agent framework for building Generative AI applications.', 'What do I have on my calendar today?': "You're going to spend all day playing with Pydantic AI.", 'Write a long story about a cat': 'Once upon a time, there was a curious cat named Whiskers who loved to explore the world around him...', + 'What is the first sentence on https://ai.pydantic.dev?': 'Pydantic AI is a Python agent framework designed to make it less painful to build production grade applications with Generative AI.', } tool_responses: dict[tuple[str, str], str] = {